@jmoyers/harness 0.1.9 → 0.1.11
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 +36 -155
- package/package.json +3 -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 +265 -14
- package/scripts/control-plane-daemon.ts +33 -5
- package/scripts/harness.ts +579 -134
- package/src/cli/default-gateway-pointer.ts +193 -0
- package/src/cli/gateway-record.ts +16 -1
- package/src/config/config-core.ts +13 -2
- package/src/config/harness-paths.ts +4 -7
- package/src/config/harness-runtime-migration.ts +142 -19
- package/src/config/secrets-core.ts +92 -4
- package/src/control-plane/prompt/thread-title-namer.ts +316 -0
- package/src/control-plane/stream-command-parser.ts +12 -0
- package/src/control-plane/stream-protocol.ts +6 -0
- package/src/control-plane/stream-server-background.ts +18 -2
- package/src/control-plane/stream-server-command.ts +14 -0
- package/src/control-plane/stream-server.ts +460 -28
- package/src/domain/conversations.ts +11 -7
- package/src/domain/workspace.ts +9 -0
- package/src/mux/input-shortcuts.ts +38 -1
- package/src/mux/live-mux/git-parsing.ts +40 -0
- package/src/mux/live-mux/global-shortcut-handlers.ts +8 -0
- package/src/mux/live-mux/left-rail-conversation-click.ts +6 -3
- package/src/mux/live-mux/modal-input-reducers.ts +34 -1
- package/src/mux/live-mux/modal-overlays.ts +45 -0
- package/src/mux/live-mux/modal-prompt-handlers.ts +85 -0
- package/src/mux/render-frame.ts +1 -1
- package/src/mux/task-screen-keybindings.ts +29 -1
- package/src/services/control-plane.ts +22 -0
- package/src/services/runtime-control-actions.ts +69 -0
- package/src/services/runtime-conversation-activation.ts +25 -0
- package/src/services/runtime-conversation-starter.ts +31 -7
- package/src/services/runtime-input-router.ts +6 -0
- package/src/services/runtime-modal-input.ts +18 -0
- package/src/services/runtime-navigation-input.ts +4 -0
- package/src/services/runtime-rail-input.ts +5 -0
- package/src/services/runtime-repository-actions.ts +2 -0
- package/src/services/runtime-workspace-actions.ts +5 -0
- package/src/store/control-plane-store.ts +36 -0
- package/src/store/event-store.ts +36 -0
- package/src/ui/global-shortcut-input.ts +2 -0
- package/src/ui/input.ts +31 -0
- package/src/ui/modals/manager.ts +26 -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 actions (`Open GitHub`, `Show My Open Pull Requests`, `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,121 +44,37 @@ 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: Unstaged Changes`
|
|
96
|
-
- `Critique AI Review: Staged Changes`
|
|
97
|
-
- `Critique AI Review: Current Branch vs Base`
|
|
98
|
-
- These start a terminal thread and run `critique review ...`, preferring `claude` when available and otherwise using `opencode` when installed.
|
|
99
|
-
- `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.
|
|
100
55
|
|
|
101
|
-
|
|
56
|
+
## User details
|
|
102
57
|
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
|
|
106
|
-
`
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
When GitHub auth is available (`GITHUB_TOKEN` or an authenticated `gh` CLI), Harness can:
|
|
111
|
-
|
|
112
|
-
- Detect the tracked branch for the active project and show `Open PR` (if an open PR exists) or `Create PR` in the command palette.
|
|
113
|
-
- Continuously sync open PR CI/check status into the control-plane store for realtime clients.
|
|
114
|
-
- If auth is unavailable, PR actions fail quietly and show a lightweight hint instead of surfacing hard errors.
|
|
115
|
-
|
|
116
|
-
## API for Automation
|
|
117
|
-
|
|
118
|
-
Harness exposes a typed realtime client for orchestrators, policy agents, and dashboards:
|
|
119
|
-
|
|
120
|
-
```ts
|
|
121
|
-
import { connectHarnessAgentRealtimeClient } from './src/control-plane/agent-realtime-api.ts';
|
|
122
|
-
|
|
123
|
-
const client = await connectHarnessAgentRealtimeClient({
|
|
124
|
-
host: '127.0.0.1',
|
|
125
|
-
port: 7777,
|
|
126
|
-
subscription: { includeOutput: false },
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
client.on('session.status', ({ observed }) => {
|
|
130
|
-
console.log(observed.sessionId, observed.status);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
await client.close();
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Key orchestration calls are available in the same client:
|
|
137
|
-
|
|
138
|
-
- `client.tasks.pull(...)`
|
|
139
|
-
- `client.projects.status(projectId)`
|
|
140
|
-
- `client.projects.settings.get(projectId)` / `client.projects.settings.update(projectId, update)`
|
|
141
|
-
- `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
|
+
- `ctrl` and `cmd` shortcut chords are mirrored in both directions when your terminal/OS does not reserve the combination.
|
|
62
|
+
- Theme selection is built in (`Set a Theme`) with OpenCode-compatible presets and live preview.
|
|
63
|
+
- API keys can be set directly from `ctrl+p` / `cmd+p` (`Set Anthropic API Key`, `Set OpenAI API Key`), with overwrite warning and paste-friendly entry.
|
|
64
|
+
- `Create PR` uses either `GITHUB_TOKEN` or an authenticated `gh` CLI session.
|
|
142
65
|
|
|
143
66
|
## Configuration
|
|
144
67
|
|
|
145
|
-
Runtime behavior is
|
|
146
|
-
GitHub project/PR integration is enabled by default and configured under `github.*`.
|
|
147
|
-
|
|
148
|
-
Example (install commands + critique defaults + hotkey override + OpenCode theme selection):
|
|
149
|
-
|
|
150
|
-
```jsonc
|
|
151
|
-
{
|
|
152
|
-
"codex": {
|
|
153
|
-
"install": {
|
|
154
|
-
"command": "bunx @openai/codex@latest"
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
"claude": {
|
|
158
|
-
"install": {
|
|
159
|
-
"command": "bunx @anthropic-ai/claude-code@latest"
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
"cursor": {
|
|
163
|
-
"install": {
|
|
164
|
-
"command": null
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
"critique": {
|
|
168
|
-
"launch": {
|
|
169
|
-
"defaultArgs": ["--watch"]
|
|
170
|
-
},
|
|
171
|
-
"install": {
|
|
172
|
-
"command": "bun add --global critique@latest"
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
"mux": {
|
|
176
|
-
"ui": {
|
|
177
|
-
"theme": {
|
|
178
|
-
"preset": "tokyonight",
|
|
179
|
-
"mode": "dark",
|
|
180
|
-
"customThemePath": null
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
"keybindings": {
|
|
184
|
-
"mux.conversation.critique.open-or-create": ["ctrl+g"]
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
```
|
|
68
|
+
Runtime behavior is controlled by `harness.config.jsonc`.
|
|
189
69
|
|
|
190
|
-
|
|
191
|
-
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.
|
|
70
|
+
When upgrading from a workspace-local `.harness`, Harness automatically migrates legacy config into the global config location if that global config is still uninitialized (missing, empty, or default template), then removes stale local `.harness` folders once migration targets are confirmed.
|
|
192
71
|
|
|
193
|
-
|
|
72
|
+
Common customizations:
|
|
194
73
|
|
|
195
|
-
- `
|
|
196
|
-
-
|
|
74
|
+
- Set install commands for `codex`, `claude`, `cursor`, and `critique`.
|
|
75
|
+
- Configure critique launch defaults.
|
|
76
|
+
- Customize keybindings.
|
|
77
|
+
- Choose a theme preset or custom OpenCode-compatible theme file.
|
|
197
78
|
|
|
198
79
|
## License
|
|
199
80
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jmoyers/harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
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",
|
|
@@ -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
|
+
}
|