@jmoyers/harness 0.1.8 → 0.1.10
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/README.md +33 -155
- package/package.json +5 -1
- package/packages/harness-ai/src/anthropic-client.ts +99 -0
- package/packages/harness-ai/src/anthropic-protocol.ts +581 -0
- package/packages/harness-ai/src/anthropic-provider.ts +82 -0
- package/packages/harness-ai/src/async-iterable-stream.ts +65 -0
- package/packages/harness-ai/src/index.ts +36 -0
- package/packages/harness-ai/src/json-parse.ts +66 -0
- package/packages/harness-ai/src/sse.ts +80 -0
- package/packages/harness-ai/src/stream-object.ts +96 -0
- package/packages/harness-ai/src/stream-text.ts +1340 -0
- package/packages/harness-ai/src/types.ts +330 -0
- package/packages/harness-ai/src/ui-stream.ts +217 -0
- package/scripts/codex-live-mux-runtime.ts +123 -7
- package/scripts/control-plane-daemon.ts +20 -3
- package/scripts/harness.ts +566 -133
- package/src/cli/gateway-record.ts +16 -1
- package/src/control-plane/agent-realtime-api.ts +4 -0
- package/src/control-plane/prompt/agent-prompt-extractor.ts +191 -0
- package/src/control-plane/prompt/extractors/claude-prompt-extractor.ts +53 -0
- package/src/control-plane/prompt/extractors/codex-prompt-extractor.ts +50 -0
- package/src/control-plane/prompt/extractors/cursor-prompt-extractor.ts +56 -0
- package/src/control-plane/prompt/session-prompt-engine.ts +69 -0
- package/src/control-plane/prompt/thread-title-namer.ts +290 -0
- package/src/control-plane/stream-command-parser.ts +12 -0
- package/src/control-plane/stream-protocol.ts +109 -0
- package/src/control-plane/stream-server-command.ts +14 -0
- package/src/control-plane/stream-server-session-runtime.ts +12 -0
- package/src/control-plane/stream-server.ts +485 -19
- package/src/mux/input-shortcuts.ts +9 -0
- package/src/mux/live-mux/critique-review.ts +5 -1
- package/src/mux/live-mux/git-parsing.ts +24 -0
- package/src/mux/live-mux/global-shortcut-handlers.ts +8 -0
- package/src/mux/render-frame.ts +1 -1
- package/src/pty/pty_host.ts +46 -1
- package/src/services/control-plane.ts +22 -0
- package/src/services/runtime-control-actions.ts +69 -0
- package/src/services/runtime-navigation-input.ts +4 -0
- package/src/services/runtime-rail-input.ts +4 -0
- package/src/services/runtime-workspace-actions.ts +5 -0
- package/src/ui/global-shortcut-input.ts +2 -0
package/README.md
CHANGED
|
@@ -1,75 +1,40 @@
|
|
|
1
1
|
# Harness
|
|
2
2
|
|
|
3
|
-
Harness is a terminal-native workspace for running
|
|
3
|
+
Harness is a terminal-native workspace for running multiple coding-agent threads in parallel, without losing project context.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It is built for people who want to move faster than a single chat window: implementation, review, and follow-up work can run side by side in one keyboard-first interface.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## What matters most
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- Open `Set a Theme` from the command palette to launch a second autocomplete picker of canonical OpenCode presets (plus a `default` reset option), with live preview while you navigate.
|
|
16
|
-
- Open or create a GitHub pull request for the currently tracked project branch directly from the command palette.
|
|
9
|
+
- Parallel threads across `codex`, `claude`, `cursor`, `terminal`, and `critique`.
|
|
10
|
+
- One command palette (`ctrl+p` / `cmd+p`) to jump threads, run actions, and control workflow quickly.
|
|
11
|
+
- Long-running work survives reconnects through a detached gateway.
|
|
12
|
+
- Gateway control is resilient: lifecycle operations are lock-serialized per session, and missing stale records can be recovered automatically.
|
|
13
|
+
- Fast left-rail navigation with automatic, readable thread titles.
|
|
14
|
+
- Built-in GitHub PR actions (`Open PR` / `Create PR`) from inside Harness.
|
|
17
15
|
|
|
18
16
|
## Demo
|
|
19
17
|
|
|
20
|
-

|
|
21
19
|
|
|
22
20
|
## Quick start
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
Prerequisites:
|
|
25
23
|
|
|
26
24
|
- Bun `1.3.9+`
|
|
27
|
-
-
|
|
28
|
-
- At least one installed agent CLI (`codex`, `claude`, `cursor`, or `critique`)
|
|
25
|
+
- At least one agent CLI (`codex`, `claude`, `cursor`, or `critique`)
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
> Note: Harness requires Bun. It does not work with Node.js alone.
|
|
27
|
+
Install and run:
|
|
33
28
|
|
|
34
29
|
```bash
|
|
35
|
-
#
|
|
30
|
+
# Bootstrap install
|
|
36
31
|
curl -fsSL https://raw.githubusercontent.com/jmoyers/harness/main/install.sh | bash
|
|
37
32
|
|
|
38
|
-
#
|
|
33
|
+
# Or run directly (no global install)
|
|
39
34
|
bunx @jmoyers/harness@latest
|
|
40
35
|
|
|
41
36
|
# Or install globally
|
|
42
37
|
bun add -g --trust @jmoyers/harness
|
|
43
|
-
|
|
44
|
-
# Upgrade an existing global install
|
|
45
|
-
harness update
|
|
46
|
-
# alias: harness upgrade
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### Install (from source)
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
bun install
|
|
53
|
-
bun link
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### Uninstall
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
# Remove global Harness package
|
|
60
|
-
bun remove --global @jmoyers/harness
|
|
61
|
-
|
|
62
|
-
# Optional: remove workspace runtime artifacts (keeps harness.config.jsonc)
|
|
63
|
-
if [ -n "${XDG_CONFIG_HOME:-}" ]; then
|
|
64
|
-
rm -rf "$XDG_CONFIG_HOME/harness/workspaces"
|
|
65
|
-
else
|
|
66
|
-
rm -rf "$HOME/.harness/workspaces"
|
|
67
|
-
fi
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Run
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
38
|
harness
|
|
74
39
|
```
|
|
75
40
|
|
|
@@ -79,120 +44,33 @@ Use a named session when you want isolated state:
|
|
|
79
44
|
harness --session my-session
|
|
80
45
|
```
|
|
81
46
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
1. Open Harness in your repo.
|
|
85
|
-
2. Start parallel threads for implementation and review.
|
|
86
|
-
3. Use the command palette (`ctrl+p` / `cmd+p`) to jump, run actions, and manage project context.
|
|
87
|
-
4. Open the repo or PR actions from inside Harness when GitHub auth is available.
|
|
47
|
+
For restart/load diagnostics, use a named session with a non-default gateway port so you do not disrupt your active workspace gateway.
|
|
88
48
|
|
|
89
|
-
##
|
|
49
|
+
## Typical workflow
|
|
90
50
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
- `Critique AI Review: Staged Changes`
|
|
96
|
-
- `Critique AI Review: Current Branch vs Base`
|
|
97
|
-
- These start a terminal thread and run `critique review ...`, preferring `claude` when available and otherwise using `opencode` when installed.
|
|
98
|
-
- `mux.conversation.critique.open-or-create` is bound to `ctrl+g` by default.
|
|
51
|
+
1. Open Harness in your repository.
|
|
52
|
+
2. Start separate threads for implementation and review.
|
|
53
|
+
3. Use `ctrl+p` / `cmd+p` to switch context and run project actions.
|
|
54
|
+
4. Open or create a PR from the same workspace.
|
|
99
55
|
|
|
100
|
-
|
|
56
|
+
## User details
|
|
101
57
|
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
## GitHub PR Integration
|
|
108
|
-
|
|
109
|
-
When GitHub auth is available (`GITHUB_TOKEN` or an authenticated `gh` CLI), Harness can:
|
|
110
|
-
|
|
111
|
-
- Detect the tracked branch for the active project and show `Open PR` (if an open PR exists) or `Create PR` in the command palette.
|
|
112
|
-
- Continuously sync open PR CI/check status into the control-plane store for realtime clients.
|
|
113
|
-
- If auth is unavailable, PR actions fail quietly and show a lightweight hint instead of surfacing hard errors.
|
|
114
|
-
|
|
115
|
-
## API for Automation
|
|
116
|
-
|
|
117
|
-
Harness exposes a typed realtime client for orchestrators, policy agents, and dashboards:
|
|
118
|
-
|
|
119
|
-
```ts
|
|
120
|
-
import { connectHarnessAgentRealtimeClient } from './src/control-plane/agent-realtime-api.ts';
|
|
121
|
-
|
|
122
|
-
const client = await connectHarnessAgentRealtimeClient({
|
|
123
|
-
host: '127.0.0.1',
|
|
124
|
-
port: 7777,
|
|
125
|
-
subscription: { includeOutput: false },
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
client.on('session.status', ({ observed }) => {
|
|
129
|
-
console.log(observed.sessionId, observed.status);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
await client.close();
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Key orchestration calls are available in the same client:
|
|
136
|
-
|
|
137
|
-
- `client.tasks.pull(...)`
|
|
138
|
-
- `client.projects.status(projectId)`
|
|
139
|
-
- `client.projects.settings.get(projectId)` / `client.projects.settings.update(projectId, update)`
|
|
140
|
-
- `client.automation.getPolicy(...)` / `client.automation.setPolicy(...)`
|
|
58
|
+
- Thread-scoped command palette (`[+ thread]`) can launch/install supported agent CLIs per project.
|
|
59
|
+
- Critique review actions are available from the global palette and run in a terminal thread.
|
|
60
|
+
- `ctrl+g` opens the project’s critique thread (or creates one if needed).
|
|
61
|
+
- Theme selection is built in (`Set a Theme`) with OpenCode-compatible presets and live preview.
|
|
62
|
+
- PR actions use either `GITHUB_TOKEN` or an authenticated `gh` CLI session.
|
|
141
63
|
|
|
142
64
|
## Configuration
|
|
143
65
|
|
|
144
|
-
Runtime behavior is
|
|
145
|
-
GitHub project/PR integration is enabled by default and configured under `github.*`.
|
|
146
|
-
|
|
147
|
-
Example (install commands + critique defaults + hotkey override + OpenCode theme selection):
|
|
148
|
-
|
|
149
|
-
```jsonc
|
|
150
|
-
{
|
|
151
|
-
"codex": {
|
|
152
|
-
"install": {
|
|
153
|
-
"command": "bunx @openai/codex@latest"
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
"claude": {
|
|
157
|
-
"install": {
|
|
158
|
-
"command": "bunx @anthropic-ai/claude-code@latest"
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
"cursor": {
|
|
162
|
-
"install": {
|
|
163
|
-
"command": null
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
"critique": {
|
|
167
|
-
"launch": {
|
|
168
|
-
"defaultArgs": ["--watch"]
|
|
169
|
-
},
|
|
170
|
-
"install": {
|
|
171
|
-
"command": "bun add --global critique@latest"
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
"mux": {
|
|
175
|
-
"ui": {
|
|
176
|
-
"theme": {
|
|
177
|
-
"preset": "tokyonight",
|
|
178
|
-
"mode": "dark",
|
|
179
|
-
"customThemePath": null
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
"keybindings": {
|
|
183
|
-
"mux.conversation.critique.open-or-create": ["ctrl+g"]
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
`mux.ui.theme.customThemePath` can point to any local JSON file that follows the OpenCode theme schema (`https://opencode.ai/theme.json`).
|
|
190
|
-
Built-in presets now mirror the canonical OpenCode set (for example `aura`, `ayu`, `carbonfox`, `catppuccin`, `dracula`, `everforest`, `github`, `gruvbox`, `nightowl`, `nord`, `one-dark`, `opencode`, `tokyonight`, `vesper`, `zenburn`, and more), plus a special `default` picker option for the legacy default mux theme.
|
|
66
|
+
Runtime behavior is controlled by `harness.config.jsonc`.
|
|
191
67
|
|
|
192
|
-
|
|
68
|
+
Common customizations:
|
|
193
69
|
|
|
194
|
-
- `
|
|
195
|
-
-
|
|
70
|
+
- Set install commands for `codex`, `claude`, `cursor`, and `critique`.
|
|
71
|
+
- Configure critique launch defaults.
|
|
72
|
+
- Customize keybindings.
|
|
73
|
+
- Choose a theme preset or custom OpenCode-compatible theme file.
|
|
196
74
|
|
|
197
75
|
## License
|
|
198
76
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jmoyers/harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"scripts/harness-core.ts",
|
|
23
23
|
"scripts/harness-inspector.ts",
|
|
24
24
|
"scripts/harness.ts",
|
|
25
|
+
"packages/harness-ai/src",
|
|
25
26
|
"scripts/terminal-recording-gif-lib.ts",
|
|
26
27
|
"scripts/require-bun.js",
|
|
27
28
|
"native/ptyd/Cargo.lock",
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"codex:live:tail": "bun scripts/codex-live-tail.ts",
|
|
54
55
|
"codex:live:snapshot": "bun scripts/codex-live-snapshot.ts",
|
|
55
56
|
"control-plane:daemon": "bun scripts/control-plane-daemon.ts",
|
|
57
|
+
"gateway:stress": "bun scripts/gateway-restart-stress.ts",
|
|
56
58
|
"terminal:parity": "bun scripts/terminal-parity.ts",
|
|
57
59
|
"terminal:differential": "bun scripts/terminal-differential-checkpoints.ts",
|
|
58
60
|
"previm:passthrough": "bun run build:ptyd",
|
|
@@ -77,6 +79,8 @@
|
|
|
77
79
|
"test": "bun run build:ptyd && bun test",
|
|
78
80
|
"test:integration:codex-status": "bun scripts/integration-codex-status-sequence.ts",
|
|
79
81
|
"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.\"",
|
|
82
|
+
"test:integration:agent-prompts": "bun scripts/integration-agent-prompt-parity.ts",
|
|
83
|
+
"test:integration:agent-prompts:long": "bun scripts/integration-agent-prompt-parity.ts --timeout-ms 180000",
|
|
80
84
|
"smoke:harness-ai": "bun scripts/harness-ai-smoke.ts",
|
|
81
85
|
"smoke:harness-ai:parity": "bun scripts/harness-ai-parity-smoke.mts",
|
|
82
86
|
"test:coverage": "bun run build:ptyd && bun test --coverage --coverage-reporter=lcov --coverage-dir .harness/coverage-bun && bun run coverage:check",
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { parseAnthropicStreamChunk, type AnthropicStreamChunk } from './anthropic-protocol.ts';
|
|
2
|
+
import { createSseEventStream } from './sse.ts';
|
|
3
|
+
import type { HarnessAnthropicModel } from './types.ts';
|
|
4
|
+
|
|
5
|
+
export interface AnthropicMessagesRequestBody {
|
|
6
|
+
readonly model: string;
|
|
7
|
+
readonly max_tokens?: number;
|
|
8
|
+
readonly temperature?: number;
|
|
9
|
+
readonly top_p?: number;
|
|
10
|
+
readonly stop_sequences?: string[];
|
|
11
|
+
readonly system?: string;
|
|
12
|
+
readonly messages: unknown[];
|
|
13
|
+
readonly tools?: unknown[];
|
|
14
|
+
readonly stream: true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ParsedAnthropicStreamEvent {
|
|
18
|
+
readonly rawValue: unknown;
|
|
19
|
+
readonly chunk: AnthropicStreamChunk | null;
|
|
20
|
+
readonly parseError?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface AnthropicStreamResponse {
|
|
24
|
+
readonly requestBody: AnthropicMessagesRequestBody;
|
|
25
|
+
readonly responseHeaders: Headers;
|
|
26
|
+
readonly stream: ReadableStream<ParsedAnthropicStreamEvent>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function readErrorBody(response: Response): Promise<string> {
|
|
30
|
+
try {
|
|
31
|
+
return await response.text();
|
|
32
|
+
} catch {
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function postAnthropicMessagesStream(
|
|
38
|
+
model: HarnessAnthropicModel,
|
|
39
|
+
requestBody: AnthropicMessagesRequestBody,
|
|
40
|
+
abortSignal?: AbortSignal,
|
|
41
|
+
): Promise<AnthropicStreamResponse> {
|
|
42
|
+
const url = `${model.baseUrl}/messages`;
|
|
43
|
+
const requestInit: RequestInit = {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
'content-type': 'application/json',
|
|
47
|
+
'x-api-key': model.apiKey,
|
|
48
|
+
'anthropic-version': '2023-06-01',
|
|
49
|
+
...model.headers,
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify(requestBody),
|
|
52
|
+
};
|
|
53
|
+
if (abortSignal !== undefined) {
|
|
54
|
+
requestInit.signal = abortSignal;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const response = await model.fetch(url, requestInit);
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
const errorBody = await readErrorBody(response);
|
|
61
|
+
throw new Error(`anthropic request failed (${response.status}): ${errorBody}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (response.body === null) {
|
|
65
|
+
throw new Error('anthropic response body was empty');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const sseStream = createSseEventStream(response.body);
|
|
69
|
+
const parsedStream = sseStream.pipeThrough(
|
|
70
|
+
new TransformStream({
|
|
71
|
+
transform(event, controller) {
|
|
72
|
+
if (event.data === '[DONE]') {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const raw = JSON.parse(event.data) as unknown;
|
|
78
|
+
const parsed = parseAnthropicStreamChunk(raw);
|
|
79
|
+
controller.enqueue({
|
|
80
|
+
rawValue: raw,
|
|
81
|
+
chunk: parsed,
|
|
82
|
+
} satisfies ParsedAnthropicStreamEvent);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
controller.enqueue({
|
|
85
|
+
rawValue: event.data,
|
|
86
|
+
chunk: null,
|
|
87
|
+
parseError: error instanceof Error ? error.message : String(error),
|
|
88
|
+
} satisfies ParsedAnthropicStreamEvent);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
requestBody,
|
|
96
|
+
responseHeaders: response.headers,
|
|
97
|
+
stream: parsedStream,
|
|
98
|
+
};
|
|
99
|
+
}
|