@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
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import type { StreamSessionEvent } from '../control-plane/stream-protocol.ts';
|
|
2
|
+
|
|
3
|
+
const CODEX_EXPLICIT_SUBCOMMANDS = new Set([
|
|
4
|
+
'exec',
|
|
5
|
+
'review',
|
|
6
|
+
'login',
|
|
7
|
+
'logout',
|
|
8
|
+
'mcp',
|
|
9
|
+
'mcp-server',
|
|
10
|
+
'app-server',
|
|
11
|
+
'app',
|
|
12
|
+
'completion',
|
|
13
|
+
'sandbox',
|
|
14
|
+
'debug',
|
|
15
|
+
'apply',
|
|
16
|
+
'resume',
|
|
17
|
+
'fork',
|
|
18
|
+
'cloud',
|
|
19
|
+
'features',
|
|
20
|
+
'help',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const CLAUDE_EXPLICIT_SUBCOMMANDS = new Set([
|
|
24
|
+
'doctor',
|
|
25
|
+
'install',
|
|
26
|
+
'mcp',
|
|
27
|
+
'plugin',
|
|
28
|
+
'setup-token',
|
|
29
|
+
'update',
|
|
30
|
+
'help',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
type CodexLaunchMode = 'yolo' | 'standard';
|
|
34
|
+
type ClaudeLaunchMode = 'yolo' | 'standard';
|
|
35
|
+
type CursorLaunchMode = 'yolo' | 'standard';
|
|
36
|
+
|
|
37
|
+
interface BuildAgentSessionStartArgsOptions {
|
|
38
|
+
readonly directoryPath?: string | null;
|
|
39
|
+
readonly codexLaunchDefaultMode?: CodexLaunchMode;
|
|
40
|
+
readonly codexLaunchModeByDirectoryPath?: Readonly<Record<string, CodexLaunchMode>>;
|
|
41
|
+
readonly claudeLaunchDefaultMode?: ClaudeLaunchMode;
|
|
42
|
+
readonly claudeLaunchModeByDirectoryPath?: Readonly<Record<string, ClaudeLaunchMode>>;
|
|
43
|
+
readonly cursorLaunchDefaultMode?: CursorLaunchMode;
|
|
44
|
+
readonly cursorLaunchModeByDirectoryPath?: Readonly<Record<string, CursorLaunchMode>>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
48
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return value as Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readString(value: unknown): string | null {
|
|
55
|
+
return typeof value === 'string' ? value : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readNonEmptyString(value: unknown): string | null {
|
|
59
|
+
const raw = readString(value);
|
|
60
|
+
if (raw === null) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const trimmed = raw.trim();
|
|
64
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function firstNonOptionArg(args: readonly string[]): string | null {
|
|
68
|
+
for (const arg of args) {
|
|
69
|
+
if (arg === '--') {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (arg.startsWith('-')) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
return arg;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function normalizeAdapterState(value: unknown): Record<string, unknown> {
|
|
81
|
+
const normalized = asRecord(value);
|
|
82
|
+
if (normalized === null) {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
...normalized,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function codexResumeSessionIdFromAdapterState(
|
|
91
|
+
adapterState: Record<string, unknown>,
|
|
92
|
+
): string | null {
|
|
93
|
+
const codex = asRecord(adapterState['codex']);
|
|
94
|
+
if (codex === null) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const resumeSessionId = readNonEmptyString(codex['resumeSessionId']);
|
|
98
|
+
if (resumeSessionId !== null) {
|
|
99
|
+
return resumeSessionId;
|
|
100
|
+
}
|
|
101
|
+
const legacyThreadId = readNonEmptyString(codex['threadId']);
|
|
102
|
+
if (legacyThreadId !== null) {
|
|
103
|
+
return legacyThreadId;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function claudeResumeSessionIdFromAdapterState(
|
|
109
|
+
adapterState: Record<string, unknown>,
|
|
110
|
+
): string | null {
|
|
111
|
+
const claude = asRecord(adapterState['claude']);
|
|
112
|
+
if (claude === null) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const resumeSessionId = readNonEmptyString(claude['resumeSessionId']);
|
|
116
|
+
if (resumeSessionId !== null) {
|
|
117
|
+
return resumeSessionId;
|
|
118
|
+
}
|
|
119
|
+
return readNonEmptyString(claude['sessionId']);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function cursorResumeSessionIdFromAdapterState(
|
|
123
|
+
adapterState: Record<string, unknown>,
|
|
124
|
+
): string | null {
|
|
125
|
+
const cursor = asRecord(adapterState['cursor']);
|
|
126
|
+
if (cursor === null) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const resumeSessionId = readNonEmptyString(cursor['resumeSessionId']);
|
|
130
|
+
if (resumeSessionId !== null) {
|
|
131
|
+
return resumeSessionId;
|
|
132
|
+
}
|
|
133
|
+
const conversationId = readNonEmptyString(cursor['conversationId']);
|
|
134
|
+
if (conversationId !== null) {
|
|
135
|
+
return conversationId;
|
|
136
|
+
}
|
|
137
|
+
return readNonEmptyString(cursor['sessionId']);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function mergeAdapterStateFromSessionEvent(
|
|
141
|
+
agentType: string,
|
|
142
|
+
_currentState?: Record<string, unknown>,
|
|
143
|
+
_event?: StreamSessionEvent,
|
|
144
|
+
_observedAt?: string,
|
|
145
|
+
): Record<string, unknown> | null {
|
|
146
|
+
if (_event?.type !== 'notify') {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const payload = asRecord(_event.record.payload);
|
|
150
|
+
if (agentType === 'claude') {
|
|
151
|
+
const sessionId =
|
|
152
|
+
readNonEmptyString(payload?.['session_id']) ?? readNonEmptyString(payload?.['sessionId']);
|
|
153
|
+
if (sessionId === null) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const currentState = normalizeAdapterState(_currentState ?? {});
|
|
158
|
+
const claude = asRecord(currentState['claude']) ?? {};
|
|
159
|
+
const currentResumeSessionId = readNonEmptyString(claude['resumeSessionId']);
|
|
160
|
+
const lastObservedAt = readNonEmptyString(claude['lastObservedAt']);
|
|
161
|
+
const nextObservedAt = readNonEmptyString(_observedAt) ?? lastObservedAt ?? null;
|
|
162
|
+
if (currentResumeSessionId === sessionId && nextObservedAt === lastObservedAt) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
...currentState,
|
|
168
|
+
claude: {
|
|
169
|
+
...claude,
|
|
170
|
+
resumeSessionId: sessionId,
|
|
171
|
+
...(nextObservedAt === null ? {} : { lastObservedAt: nextObservedAt }),
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (agentType !== 'cursor') {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const conversationId =
|
|
179
|
+
readNonEmptyString(payload?.['conversation_id']) ??
|
|
180
|
+
readNonEmptyString(payload?.['conversationId']);
|
|
181
|
+
const rawSessionId =
|
|
182
|
+
readNonEmptyString(payload?.['session_id']) ?? readNonEmptyString(payload?.['sessionId']);
|
|
183
|
+
const harnessSessionId =
|
|
184
|
+
readNonEmptyString(payload?.['harness_session_id']) ??
|
|
185
|
+
readNonEmptyString(payload?.['harnessSessionId']);
|
|
186
|
+
const sessionId =
|
|
187
|
+
conversationId ??
|
|
188
|
+
(rawSessionId !== null && rawSessionId !== harnessSessionId ? rawSessionId : null);
|
|
189
|
+
if (sessionId === null) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const currentState = normalizeAdapterState(_currentState ?? {});
|
|
193
|
+
const cursor = asRecord(currentState['cursor']) ?? {};
|
|
194
|
+
const currentResumeSessionId = readNonEmptyString(cursor['resumeSessionId']);
|
|
195
|
+
const lastObservedAt = readNonEmptyString(cursor['lastObservedAt']);
|
|
196
|
+
const nextObservedAt = readNonEmptyString(_observedAt) ?? lastObservedAt ?? null;
|
|
197
|
+
if (currentResumeSessionId === sessionId && nextObservedAt === lastObservedAt) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
...currentState,
|
|
202
|
+
cursor: {
|
|
203
|
+
...cursor,
|
|
204
|
+
resumeSessionId: sessionId,
|
|
205
|
+
...(nextObservedAt === null ? {} : { lastObservedAt: nextObservedAt }),
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function hasCursorResumeArg(args: readonly string[]): boolean {
|
|
211
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
212
|
+
const arg = args[index];
|
|
213
|
+
if (arg === '--resume' || arg === '-r') {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function hasCursorPrintOrHeadlessArg(args: readonly string[]): boolean {
|
|
221
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
222
|
+
const arg = args[index];
|
|
223
|
+
if (arg === '--print' || arg === '--headless') {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
if (arg === '--mode') {
|
|
227
|
+
const next = args[index + 1];
|
|
228
|
+
if (next === 'headless') {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (arg?.startsWith('--mode=')) {
|
|
234
|
+
const mode = arg.slice('--mode='.length);
|
|
235
|
+
if (mode === 'headless') {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function hasClaudeResumeArg(args: readonly string[]): boolean {
|
|
244
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
245
|
+
const arg = args[index];
|
|
246
|
+
if (
|
|
247
|
+
arg === '--resume' ||
|
|
248
|
+
arg === '-r' ||
|
|
249
|
+
arg === '--continue' ||
|
|
250
|
+
arg === '-c' ||
|
|
251
|
+
arg === '--session-id'
|
|
252
|
+
) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function buildAgentStartArgs(
|
|
260
|
+
agentType: string,
|
|
261
|
+
baseArgs: readonly string[],
|
|
262
|
+
adapterState: Record<string, unknown>,
|
|
263
|
+
options?: {
|
|
264
|
+
codexLaunchMode?: CodexLaunchMode;
|
|
265
|
+
claudeLaunchMode?: ClaudeLaunchMode;
|
|
266
|
+
cursorLaunchMode?: CursorLaunchMode;
|
|
267
|
+
},
|
|
268
|
+
): string[] {
|
|
269
|
+
if (agentType === 'codex') {
|
|
270
|
+
const firstArg = firstNonOptionArg(baseArgs);
|
|
271
|
+
if (firstArg !== null && CODEX_EXPLICIT_SUBCOMMANDS.has(firstArg)) {
|
|
272
|
+
return [...baseArgs];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const codexLaunchMode = options?.codexLaunchMode ?? 'standard';
|
|
276
|
+
const argsWithLaunchMode =
|
|
277
|
+
codexLaunchMode === 'yolo' && !baseArgs.includes('--yolo')
|
|
278
|
+
? [...baseArgs, '--yolo']
|
|
279
|
+
: [...baseArgs];
|
|
280
|
+
|
|
281
|
+
const resumeSessionId = codexResumeSessionIdFromAdapterState(adapterState);
|
|
282
|
+
if (resumeSessionId === null) {
|
|
283
|
+
return argsWithLaunchMode;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return ['resume', resumeSessionId, ...argsWithLaunchMode];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (agentType === 'claude') {
|
|
290
|
+
const firstArg = firstNonOptionArg(baseArgs);
|
|
291
|
+
if (firstArg !== null && CLAUDE_EXPLICIT_SUBCOMMANDS.has(firstArg)) {
|
|
292
|
+
return [...baseArgs];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const claudeLaunchMode = options?.claudeLaunchMode ?? 'standard';
|
|
296
|
+
const argsWithLaunchMode =
|
|
297
|
+
claudeLaunchMode === 'yolo' && !baseArgs.includes('--dangerously-skip-permissions')
|
|
298
|
+
? [...baseArgs, '--dangerously-skip-permissions']
|
|
299
|
+
: [...baseArgs];
|
|
300
|
+
|
|
301
|
+
if (hasClaudeResumeArg(baseArgs)) {
|
|
302
|
+
return argsWithLaunchMode;
|
|
303
|
+
}
|
|
304
|
+
const resumeSessionId = claudeResumeSessionIdFromAdapterState(adapterState);
|
|
305
|
+
if (resumeSessionId === null) {
|
|
306
|
+
return argsWithLaunchMode;
|
|
307
|
+
}
|
|
308
|
+
return ['--resume', resumeSessionId, ...argsWithLaunchMode];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (agentType === 'cursor') {
|
|
312
|
+
const cursorLaunchMode = options?.cursorLaunchMode ?? 'standard';
|
|
313
|
+
const argsWithLaunchMode = [...baseArgs];
|
|
314
|
+
if (
|
|
315
|
+
cursorLaunchMode === 'yolo' &&
|
|
316
|
+
!argsWithLaunchMode.includes('--yolo') &&
|
|
317
|
+
!argsWithLaunchMode.includes('--force')
|
|
318
|
+
) {
|
|
319
|
+
argsWithLaunchMode.push('--yolo');
|
|
320
|
+
}
|
|
321
|
+
if (
|
|
322
|
+
cursorLaunchMode === 'yolo' &&
|
|
323
|
+
hasCursorPrintOrHeadlessArg(argsWithLaunchMode) &&
|
|
324
|
+
!argsWithLaunchMode.includes('--trust')
|
|
325
|
+
) {
|
|
326
|
+
argsWithLaunchMode.push('--trust');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (hasCursorResumeArg(baseArgs)) {
|
|
330
|
+
return argsWithLaunchMode;
|
|
331
|
+
}
|
|
332
|
+
const resumeSessionId = cursorResumeSessionIdFromAdapterState(adapterState);
|
|
333
|
+
if (resumeSessionId === null) {
|
|
334
|
+
return argsWithLaunchMode;
|
|
335
|
+
}
|
|
336
|
+
return ['--resume', resumeSessionId, ...argsWithLaunchMode];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return [...baseArgs];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function buildAgentSessionStartArgs(
|
|
343
|
+
agentType: string,
|
|
344
|
+
baseArgs: readonly string[],
|
|
345
|
+
adapterState: Record<string, unknown>,
|
|
346
|
+
options: BuildAgentSessionStartArgsOptions = {},
|
|
347
|
+
): string[] {
|
|
348
|
+
const normalizedDirectoryPath = options.directoryPath?.trim() ?? '';
|
|
349
|
+
|
|
350
|
+
if (agentType === 'codex') {
|
|
351
|
+
const defaultMode = options.codexLaunchDefaultMode ?? 'standard';
|
|
352
|
+
const directoryModes = options.codexLaunchModeByDirectoryPath ?? {};
|
|
353
|
+
const codexLaunchMode =
|
|
354
|
+
normalizedDirectoryPath.length > 0
|
|
355
|
+
? (directoryModes[normalizedDirectoryPath] ?? defaultMode)
|
|
356
|
+
: defaultMode;
|
|
357
|
+
|
|
358
|
+
return buildAgentStartArgs(agentType, baseArgs, adapterState, {
|
|
359
|
+
codexLaunchMode,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (agentType === 'claude') {
|
|
364
|
+
const defaultMode = options.claudeLaunchDefaultMode ?? 'standard';
|
|
365
|
+
const directoryModes = options.claudeLaunchModeByDirectoryPath ?? {};
|
|
366
|
+
const claudeLaunchMode =
|
|
367
|
+
normalizedDirectoryPath.length > 0
|
|
368
|
+
? (directoryModes[normalizedDirectoryPath] ?? defaultMode)
|
|
369
|
+
: defaultMode;
|
|
370
|
+
|
|
371
|
+
return buildAgentStartArgs(agentType, baseArgs, adapterState, {
|
|
372
|
+
claudeLaunchMode,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (agentType === 'cursor') {
|
|
377
|
+
const defaultMode = options.cursorLaunchDefaultMode ?? 'standard';
|
|
378
|
+
const directoryModes = options.cursorLaunchModeByDirectoryPath ?? {};
|
|
379
|
+
const cursorLaunchMode =
|
|
380
|
+
normalizedDirectoryPath.length > 0
|
|
381
|
+
? (directoryModes[normalizedDirectoryPath] ?? defaultMode)
|
|
382
|
+
: defaultMode;
|
|
383
|
+
|
|
384
|
+
return buildAgentStartArgs(agentType, baseArgs, adapterState, {
|
|
385
|
+
cursorLaunchMode,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return buildAgentStartArgs(agentType, baseArgs, adapterState);
|
|
390
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
|
|
3
|
+
export const GATEWAY_RECORD_VERSION = 1;
|
|
4
|
+
export const DEFAULT_GATEWAY_HOST = '127.0.0.1';
|
|
5
|
+
export const DEFAULT_GATEWAY_PORT = 7777;
|
|
6
|
+
export const DEFAULT_GATEWAY_DB_PATH = '.harness/control-plane.sqlite';
|
|
7
|
+
export const DEFAULT_GATEWAY_RECORD_PATH = '.harness/gateway.json';
|
|
8
|
+
export const DEFAULT_GATEWAY_LOG_PATH = '.harness/gateway.log';
|
|
9
|
+
|
|
10
|
+
export interface GatewayRecord {
|
|
11
|
+
readonly version: number;
|
|
12
|
+
readonly pid: number;
|
|
13
|
+
readonly host: string;
|
|
14
|
+
readonly port: number;
|
|
15
|
+
readonly authToken: string | null;
|
|
16
|
+
readonly stateDbPath: string;
|
|
17
|
+
readonly startedAt: string;
|
|
18
|
+
readonly workspaceRoot: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
22
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return value as Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readNonEmptyString(value: unknown): string | null {
|
|
29
|
+
if (typeof value !== 'string') {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const trimmed = value.trim();
|
|
33
|
+
if (trimmed.length === 0) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return trimmed;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readPort(value: unknown): number | null {
|
|
40
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (!Number.isInteger(value) || value <= 0 || value > 65535) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readPid(value: unknown): number | null {
|
|
50
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveInvocationDirectory(env: NodeJS.ProcessEnv, cwd: string): string {
|
|
60
|
+
return env.HARNESS_INVOKE_CWD ?? env.INIT_CWD ?? cwd;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveGatewayRecordPath(workspaceRoot: string): string {
|
|
64
|
+
return resolve(workspaceRoot, DEFAULT_GATEWAY_RECORD_PATH);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function resolveGatewayLogPath(workspaceRoot: string): string {
|
|
68
|
+
return resolve(workspaceRoot, DEFAULT_GATEWAY_LOG_PATH);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function normalizeGatewayHost(
|
|
72
|
+
input: string | null | undefined,
|
|
73
|
+
fallback = DEFAULT_GATEWAY_HOST,
|
|
74
|
+
): string {
|
|
75
|
+
if (typeof input !== 'string') {
|
|
76
|
+
return fallback;
|
|
77
|
+
}
|
|
78
|
+
const trimmed = input.trim();
|
|
79
|
+
if (trimmed.length === 0) {
|
|
80
|
+
return fallback;
|
|
81
|
+
}
|
|
82
|
+
return trimmed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function normalizeGatewayPort(
|
|
86
|
+
input: number | string | null | undefined,
|
|
87
|
+
fallback = DEFAULT_GATEWAY_PORT,
|
|
88
|
+
): number {
|
|
89
|
+
if (typeof input === 'number') {
|
|
90
|
+
return readPort(input) ?? fallback;
|
|
91
|
+
}
|
|
92
|
+
if (typeof input !== 'string') {
|
|
93
|
+
return fallback;
|
|
94
|
+
}
|
|
95
|
+
const trimmed = input.trim();
|
|
96
|
+
if (trimmed.length === 0) {
|
|
97
|
+
return fallback;
|
|
98
|
+
}
|
|
99
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
100
|
+
return readPort(parsed) ?? fallback;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function normalizeGatewayStateDbPath(
|
|
104
|
+
input: string | null | undefined,
|
|
105
|
+
fallback = DEFAULT_GATEWAY_DB_PATH,
|
|
106
|
+
): string {
|
|
107
|
+
if (typeof input !== 'string') {
|
|
108
|
+
return fallback;
|
|
109
|
+
}
|
|
110
|
+
const trimmed = input.trim();
|
|
111
|
+
if (trimmed.length === 0) {
|
|
112
|
+
return fallback;
|
|
113
|
+
}
|
|
114
|
+
return trimmed;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function isLoopbackHost(host: string): boolean {
|
|
118
|
+
const normalized = host.trim().toLowerCase();
|
|
119
|
+
return normalized === '127.0.0.1' || normalized === 'localhost' || normalized === '::1';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function parseGatewayRecordText(text: string): GatewayRecord | null {
|
|
123
|
+
let parsed: unknown;
|
|
124
|
+
try {
|
|
125
|
+
parsed = JSON.parse(text);
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const record = asRecord(parsed);
|
|
130
|
+
if (record === null) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const version = record['version'];
|
|
135
|
+
if (version !== GATEWAY_RECORD_VERSION) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const pid = readPid(record['pid']);
|
|
139
|
+
const host = readNonEmptyString(record['host']);
|
|
140
|
+
const port = readPort(record['port']);
|
|
141
|
+
const stateDbPath = readNonEmptyString(record['stateDbPath']);
|
|
142
|
+
const startedAt = readNonEmptyString(record['startedAt']);
|
|
143
|
+
const workspaceRoot = readNonEmptyString(record['workspaceRoot']);
|
|
144
|
+
const authTokenRaw = record['authToken'];
|
|
145
|
+
const authToken = authTokenRaw === null ? null : readNonEmptyString(authTokenRaw);
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
pid === null ||
|
|
149
|
+
host === null ||
|
|
150
|
+
port === null ||
|
|
151
|
+
stateDbPath === null ||
|
|
152
|
+
startedAt === null ||
|
|
153
|
+
workspaceRoot === null ||
|
|
154
|
+
(authToken === null && authTokenRaw !== null)
|
|
155
|
+
) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
version,
|
|
161
|
+
pid,
|
|
162
|
+
host,
|
|
163
|
+
port,
|
|
164
|
+
authToken,
|
|
165
|
+
stateDbPath,
|
|
166
|
+
startedAt,
|
|
167
|
+
workspaceRoot,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function serializeGatewayRecord(record: GatewayRecord): string {
|
|
172
|
+
return `${JSON.stringify(record, null, 2)}\n`;
|
|
173
|
+
}
|