@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,1359 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { dirname, resolve } from 'node:path';
|
|
11
|
+
|
|
12
|
+
export const HARNESS_CONFIG_FILE_NAME = 'harness.config.jsonc';
|
|
13
|
+
|
|
14
|
+
const HARNESS_LIFECYCLE_EVENT_TYPES = [
|
|
15
|
+
'thread.created',
|
|
16
|
+
'thread.updated',
|
|
17
|
+
'thread.archived',
|
|
18
|
+
'thread.deleted',
|
|
19
|
+
'session.started',
|
|
20
|
+
'session.exited',
|
|
21
|
+
'turn.started',
|
|
22
|
+
'turn.completed',
|
|
23
|
+
'turn.failed',
|
|
24
|
+
'input.required',
|
|
25
|
+
'tool.started',
|
|
26
|
+
'tool.completed',
|
|
27
|
+
'tool.failed',
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
export type HarnessLifecycleEventType = (typeof HARNESS_LIFECYCLE_EVENT_TYPES)[number];
|
|
31
|
+
|
|
32
|
+
interface HarnessMuxConfig {
|
|
33
|
+
readonly keybindings: Readonly<Record<string, readonly string[]>>;
|
|
34
|
+
readonly ui: HarnessMuxUiConfig;
|
|
35
|
+
readonly git: HarnessMuxGitConfig;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type HarnessMuxThemeMode = 'dark' | 'light';
|
|
39
|
+
|
|
40
|
+
export interface HarnessMuxThemeConfig {
|
|
41
|
+
readonly preset: string;
|
|
42
|
+
readonly mode: HarnessMuxThemeMode;
|
|
43
|
+
readonly customThemePath: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface HarnessMuxUiConfig {
|
|
47
|
+
readonly paneWidthPercent: number | null;
|
|
48
|
+
readonly repositoriesCollapsed: boolean;
|
|
49
|
+
readonly shortcutsCollapsed: boolean;
|
|
50
|
+
readonly theme: HarnessMuxThemeConfig | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface HarnessMuxGitConfig {
|
|
54
|
+
readonly enabled: boolean;
|
|
55
|
+
readonly activePollMs: number;
|
|
56
|
+
readonly idlePollMs: number;
|
|
57
|
+
readonly burstPollMs: number;
|
|
58
|
+
readonly burstWindowMs: number;
|
|
59
|
+
readonly triggerDebounceMs: number;
|
|
60
|
+
readonly maxConcurrency: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface HarnessPerfConfig {
|
|
64
|
+
readonly enabled: boolean;
|
|
65
|
+
readonly filePath: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface HarnessDebugMuxConfig {
|
|
69
|
+
readonly debugPath: string | null;
|
|
70
|
+
readonly validateAnsi: boolean;
|
|
71
|
+
readonly resizeMinIntervalMs: number;
|
|
72
|
+
readonly ptyResizeSettleMs: number;
|
|
73
|
+
readonly startupSettleQuietMs: number;
|
|
74
|
+
readonly serverSnapshotModelEnabled: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface HarnessDebugInspectConfig {
|
|
78
|
+
readonly enabled: boolean;
|
|
79
|
+
readonly gatewayPort: number;
|
|
80
|
+
readonly clientPort: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface HarnessDebugConfig {
|
|
84
|
+
readonly enabled: boolean;
|
|
85
|
+
readonly overwriteArtifactsOnStart: boolean;
|
|
86
|
+
readonly perf: HarnessPerfConfig;
|
|
87
|
+
readonly mux: HarnessDebugMuxConfig;
|
|
88
|
+
readonly inspect: HarnessDebugInspectConfig;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface HarnessCodexTelemetryConfig {
|
|
92
|
+
readonly enabled: boolean;
|
|
93
|
+
readonly host: string;
|
|
94
|
+
readonly port: number;
|
|
95
|
+
readonly logUserPrompt: boolean;
|
|
96
|
+
readonly captureLogs: boolean;
|
|
97
|
+
readonly captureMetrics: boolean;
|
|
98
|
+
readonly captureTraces: boolean;
|
|
99
|
+
readonly captureVerboseEvents: boolean;
|
|
100
|
+
readonly ingestMode: 'lifecycle-fast' | 'full';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface HarnessCodexHistoryConfig {
|
|
104
|
+
readonly enabled: boolean;
|
|
105
|
+
readonly filePath: string;
|
|
106
|
+
readonly pollMs: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
type HarnessCodexLaunchMode = 'yolo' | 'standard';
|
|
110
|
+
|
|
111
|
+
interface HarnessCodexLaunchConfig {
|
|
112
|
+
readonly defaultMode: HarnessCodexLaunchMode;
|
|
113
|
+
readonly directoryModes: Readonly<Record<string, HarnessCodexLaunchMode>>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface HarnessCodexConfig {
|
|
117
|
+
readonly telemetry: HarnessCodexTelemetryConfig;
|
|
118
|
+
readonly history: HarnessCodexHistoryConfig;
|
|
119
|
+
readonly launch: HarnessCodexLaunchConfig;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type HarnessClaudeLaunchMode = 'yolo' | 'standard';
|
|
123
|
+
|
|
124
|
+
interface HarnessClaudeLaunchConfig {
|
|
125
|
+
readonly defaultMode: HarnessClaudeLaunchMode;
|
|
126
|
+
readonly directoryModes: Readonly<Record<string, HarnessClaudeLaunchMode>>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface HarnessClaudeConfig {
|
|
130
|
+
readonly launch: HarnessClaudeLaunchConfig;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
type HarnessCursorLaunchMode = 'yolo' | 'standard';
|
|
134
|
+
|
|
135
|
+
interface HarnessCursorLaunchConfig {
|
|
136
|
+
readonly defaultMode: HarnessCursorLaunchMode;
|
|
137
|
+
readonly directoryModes: Readonly<Record<string, HarnessCursorLaunchMode>>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface HarnessCursorConfig {
|
|
141
|
+
readonly launch: HarnessCursorLaunchConfig;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface HarnessCritiqueLaunchConfig {
|
|
145
|
+
readonly defaultArgs: readonly string[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface HarnessCritiqueInstallConfig {
|
|
149
|
+
readonly autoInstall: boolean;
|
|
150
|
+
readonly package: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface HarnessCritiqueConfig {
|
|
154
|
+
readonly launch: HarnessCritiqueLaunchConfig;
|
|
155
|
+
readonly install: HarnessCritiqueInstallConfig;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface HarnessLifecycleProviderConfig {
|
|
159
|
+
readonly codex: boolean;
|
|
160
|
+
readonly claude: boolean;
|
|
161
|
+
readonly cursor: boolean;
|
|
162
|
+
readonly controlPlane: boolean;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface HarnessLifecyclePeonPingConfig {
|
|
166
|
+
readonly enabled: boolean;
|
|
167
|
+
readonly baseUrl: string;
|
|
168
|
+
readonly timeoutMs: number;
|
|
169
|
+
readonly eventCategoryMap: Readonly<Partial<Record<HarnessLifecycleEventType, string>>>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface HarnessLifecycleWebhookConfig {
|
|
173
|
+
readonly name: string;
|
|
174
|
+
readonly enabled: boolean;
|
|
175
|
+
readonly url: string;
|
|
176
|
+
readonly method: string;
|
|
177
|
+
readonly timeoutMs: number;
|
|
178
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
179
|
+
readonly eventTypes: readonly HarnessLifecycleEventType[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface HarnessLifecycleHooksConfig {
|
|
183
|
+
readonly enabled: boolean;
|
|
184
|
+
readonly providers: HarnessLifecycleProviderConfig;
|
|
185
|
+
readonly peonPing: HarnessLifecyclePeonPingConfig;
|
|
186
|
+
readonly webhooks: readonly HarnessLifecycleWebhookConfig[];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface HarnessHooksConfig {
|
|
190
|
+
readonly lifecycle: HarnessLifecycleHooksConfig;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
interface HarnessConfig {
|
|
194
|
+
readonly mux: HarnessMuxConfig;
|
|
195
|
+
readonly debug: HarnessDebugConfig;
|
|
196
|
+
readonly codex: HarnessCodexConfig;
|
|
197
|
+
readonly claude: HarnessClaudeConfig;
|
|
198
|
+
readonly cursor: HarnessCursorConfig;
|
|
199
|
+
readonly critique: HarnessCritiqueConfig;
|
|
200
|
+
readonly hooks: HarnessHooksConfig;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
interface LoadedHarnessConfig {
|
|
204
|
+
readonly filePath: string;
|
|
205
|
+
readonly config: HarnessConfig;
|
|
206
|
+
readonly fromLastKnownGood: boolean;
|
|
207
|
+
readonly error: string | null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export const DEFAULT_HARNESS_CONFIG: HarnessConfig = {
|
|
211
|
+
mux: {
|
|
212
|
+
keybindings: {},
|
|
213
|
+
ui: {
|
|
214
|
+
paneWidthPercent: null,
|
|
215
|
+
repositoriesCollapsed: false,
|
|
216
|
+
shortcutsCollapsed: false,
|
|
217
|
+
theme: null,
|
|
218
|
+
},
|
|
219
|
+
git: {
|
|
220
|
+
enabled: true,
|
|
221
|
+
activePollMs: 1000,
|
|
222
|
+
idlePollMs: 5000,
|
|
223
|
+
burstPollMs: 400,
|
|
224
|
+
burstWindowMs: 2500,
|
|
225
|
+
triggerDebounceMs: 180,
|
|
226
|
+
maxConcurrency: 1,
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
debug: {
|
|
230
|
+
enabled: true,
|
|
231
|
+
overwriteArtifactsOnStart: true,
|
|
232
|
+
perf: {
|
|
233
|
+
enabled: true,
|
|
234
|
+
filePath: '.harness/perf-startup.jsonl',
|
|
235
|
+
},
|
|
236
|
+
mux: {
|
|
237
|
+
debugPath: '.harness/mux-debug.jsonl',
|
|
238
|
+
validateAnsi: false,
|
|
239
|
+
resizeMinIntervalMs: 33,
|
|
240
|
+
ptyResizeSettleMs: 75,
|
|
241
|
+
startupSettleQuietMs: 300,
|
|
242
|
+
serverSnapshotModelEnabled: true,
|
|
243
|
+
},
|
|
244
|
+
inspect: {
|
|
245
|
+
enabled: false,
|
|
246
|
+
gatewayPort: 6499,
|
|
247
|
+
clientPort: 6500,
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
codex: {
|
|
251
|
+
telemetry: {
|
|
252
|
+
enabled: true,
|
|
253
|
+
host: '127.0.0.1',
|
|
254
|
+
port: 0,
|
|
255
|
+
logUserPrompt: true,
|
|
256
|
+
captureLogs: true,
|
|
257
|
+
captureMetrics: true,
|
|
258
|
+
captureTraces: true,
|
|
259
|
+
captureVerboseEvents: false,
|
|
260
|
+
ingestMode: 'lifecycle-fast',
|
|
261
|
+
},
|
|
262
|
+
history: {
|
|
263
|
+
enabled: true,
|
|
264
|
+
filePath: '~/.codex/history.jsonl',
|
|
265
|
+
pollMs: 5000,
|
|
266
|
+
},
|
|
267
|
+
launch: {
|
|
268
|
+
defaultMode: 'yolo',
|
|
269
|
+
directoryModes: {},
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
claude: {
|
|
273
|
+
launch: {
|
|
274
|
+
defaultMode: 'yolo',
|
|
275
|
+
directoryModes: {},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
cursor: {
|
|
279
|
+
launch: {
|
|
280
|
+
defaultMode: 'yolo',
|
|
281
|
+
directoryModes: {},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
critique: {
|
|
285
|
+
launch: {
|
|
286
|
+
defaultArgs: ['--watch'],
|
|
287
|
+
},
|
|
288
|
+
install: {
|
|
289
|
+
autoInstall: true,
|
|
290
|
+
package: 'critique@latest',
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
hooks: {
|
|
294
|
+
lifecycle: {
|
|
295
|
+
enabled: false,
|
|
296
|
+
providers: {
|
|
297
|
+
codex: true,
|
|
298
|
+
claude: true,
|
|
299
|
+
cursor: true,
|
|
300
|
+
controlPlane: true,
|
|
301
|
+
},
|
|
302
|
+
peonPing: {
|
|
303
|
+
enabled: false,
|
|
304
|
+
baseUrl: 'http://127.0.0.1:19998',
|
|
305
|
+
timeoutMs: 1200,
|
|
306
|
+
eventCategoryMap: {
|
|
307
|
+
'session.started': 'session.start',
|
|
308
|
+
'turn.started': 'task.acknowledge',
|
|
309
|
+
'turn.completed': 'task.complete',
|
|
310
|
+
'turn.failed': 'task.error',
|
|
311
|
+
'input.required': 'input.required',
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
webhooks: [],
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
function stripJsoncComments(text: string): string {
|
|
320
|
+
let output = '';
|
|
321
|
+
let inString = false;
|
|
322
|
+
let inLineComment = false;
|
|
323
|
+
let inBlockComment = false;
|
|
324
|
+
let escaped = false;
|
|
325
|
+
|
|
326
|
+
for (let idx = 0; idx < text.length; idx += 1) {
|
|
327
|
+
const char = text[idx]!;
|
|
328
|
+
const next = text[idx + 1] ?? '';
|
|
329
|
+
|
|
330
|
+
if (inLineComment) {
|
|
331
|
+
if (char === '\n') {
|
|
332
|
+
inLineComment = false;
|
|
333
|
+
output += char;
|
|
334
|
+
}
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (inBlockComment) {
|
|
339
|
+
if (char === '*' && next === '/') {
|
|
340
|
+
inBlockComment = false;
|
|
341
|
+
idx += 1;
|
|
342
|
+
}
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (inString) {
|
|
347
|
+
output += char;
|
|
348
|
+
if (escaped) {
|
|
349
|
+
escaped = false;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (char === '\\') {
|
|
353
|
+
escaped = true;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (char === '"') {
|
|
357
|
+
inString = false;
|
|
358
|
+
}
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (char === '"') {
|
|
363
|
+
inString = true;
|
|
364
|
+
output += char;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (char === '/' && next === '/') {
|
|
369
|
+
inLineComment = true;
|
|
370
|
+
idx += 1;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (char === '/' && next === '*') {
|
|
375
|
+
inBlockComment = true;
|
|
376
|
+
idx += 1;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
output += char;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return output;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function stripTrailingCommas(text: string): string {
|
|
387
|
+
let output = '';
|
|
388
|
+
let inString = false;
|
|
389
|
+
let escaped = false;
|
|
390
|
+
|
|
391
|
+
for (let idx = 0; idx < text.length; idx += 1) {
|
|
392
|
+
const char = text[idx]!;
|
|
393
|
+
if (inString) {
|
|
394
|
+
output += char;
|
|
395
|
+
if (escaped) {
|
|
396
|
+
escaped = false;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (char === '\\') {
|
|
400
|
+
escaped = true;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (char === '"') {
|
|
404
|
+
inString = false;
|
|
405
|
+
}
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (char === '"') {
|
|
410
|
+
inString = true;
|
|
411
|
+
output += char;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (char === ',') {
|
|
416
|
+
let lookahead = idx + 1;
|
|
417
|
+
while (lookahead < text.length) {
|
|
418
|
+
const next = text[lookahead]!;
|
|
419
|
+
if (next === ' ' || next === '\n' || next === '\r' || next === '\t') {
|
|
420
|
+
lookahead += 1;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
const closing = text[lookahead];
|
|
426
|
+
if (closing === '}' || closing === ']') {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
output += char;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return output;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function normalizeKeybindings(input: unknown): Readonly<Record<string, readonly string[]>> {
|
|
438
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
439
|
+
return {};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const out: Record<string, readonly string[]> = {};
|
|
443
|
+
for (const [action, raw] of Object.entries(input)) {
|
|
444
|
+
const normalizedAction = action.trim();
|
|
445
|
+
if (normalizedAction.length === 0) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (typeof raw === 'string') {
|
|
449
|
+
const trimmed = raw.trim();
|
|
450
|
+
if (trimmed.length > 0) {
|
|
451
|
+
out[normalizedAction] = [trimmed];
|
|
452
|
+
}
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (!Array.isArray(raw)) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const keys = raw
|
|
459
|
+
.flatMap((entry) => (typeof entry === 'string' ? [entry.trim()] : []))
|
|
460
|
+
.filter((entry) => entry.length > 0);
|
|
461
|
+
if (keys.length > 0) {
|
|
462
|
+
out[normalizedAction] = keys;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return out;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function normalizePaneWidthPercent(value: unknown, fallback: number | null): number | null {
|
|
470
|
+
if (value === null || value === undefined) {
|
|
471
|
+
return fallback;
|
|
472
|
+
}
|
|
473
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
474
|
+
return fallback;
|
|
475
|
+
}
|
|
476
|
+
if (value <= 0 || value >= 100) {
|
|
477
|
+
return fallback;
|
|
478
|
+
}
|
|
479
|
+
return value;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
483
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
return value as Record<string, unknown>;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function normalizeMuxThemeMode(value: unknown, fallback: HarnessMuxThemeMode): HarnessMuxThemeMode {
|
|
490
|
+
if (value === 'dark' || value === 'light') {
|
|
491
|
+
return value;
|
|
492
|
+
}
|
|
493
|
+
return fallback;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function normalizeMuxThemeConfig(input: unknown): HarnessMuxThemeConfig | null {
|
|
497
|
+
if (input === null || input === false) {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
const record = asRecord(input);
|
|
501
|
+
if (record === null) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
const presetRaw = record['preset'];
|
|
505
|
+
const preset = typeof presetRaw === 'string' ? presetRaw.trim() : '';
|
|
506
|
+
if (preset.length === 0) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
const customThemePathRaw = record['customThemePath'];
|
|
510
|
+
const customThemePath =
|
|
511
|
+
typeof customThemePathRaw === 'string' && customThemePathRaw.trim().length > 0
|
|
512
|
+
? customThemePathRaw.trim()
|
|
513
|
+
: null;
|
|
514
|
+
const mode = normalizeMuxThemeMode(record['mode'], 'dark');
|
|
515
|
+
return {
|
|
516
|
+
preset,
|
|
517
|
+
mode,
|
|
518
|
+
customThemePath,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function normalizeMuxUiConfig(input: unknown): HarnessMuxUiConfig {
|
|
523
|
+
const record = asRecord(input);
|
|
524
|
+
if (record === null) {
|
|
525
|
+
return DEFAULT_HARNESS_CONFIG.mux.ui;
|
|
526
|
+
}
|
|
527
|
+
const paneWidthPercent = normalizePaneWidthPercent(
|
|
528
|
+
record['paneWidthPercent'],
|
|
529
|
+
DEFAULT_HARNESS_CONFIG.mux.ui.paneWidthPercent,
|
|
530
|
+
);
|
|
531
|
+
const shortcutsCollapsed =
|
|
532
|
+
typeof record['shortcutsCollapsed'] === 'boolean'
|
|
533
|
+
? record['shortcutsCollapsed']
|
|
534
|
+
: DEFAULT_HARNESS_CONFIG.mux.ui.shortcutsCollapsed;
|
|
535
|
+
const repositoriesCollapsed =
|
|
536
|
+
typeof record['repositoriesCollapsed'] === 'boolean'
|
|
537
|
+
? record['repositoriesCollapsed']
|
|
538
|
+
: DEFAULT_HARNESS_CONFIG.mux.ui.repositoriesCollapsed;
|
|
539
|
+
return {
|
|
540
|
+
paneWidthPercent,
|
|
541
|
+
repositoriesCollapsed,
|
|
542
|
+
shortcutsCollapsed,
|
|
543
|
+
theme: normalizeMuxThemeConfig(record['theme']),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function normalizeMuxGitConfig(input: unknown): HarnessMuxGitConfig {
|
|
548
|
+
const record = asRecord(input);
|
|
549
|
+
if (record === null) {
|
|
550
|
+
return DEFAULT_HARNESS_CONFIG.mux.git;
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
enabled:
|
|
554
|
+
typeof record['enabled'] === 'boolean'
|
|
555
|
+
? record['enabled']
|
|
556
|
+
: DEFAULT_HARNESS_CONFIG.mux.git.enabled,
|
|
557
|
+
activePollMs: normalizeNonNegativeInt(
|
|
558
|
+
record['activePollMs'],
|
|
559
|
+
DEFAULT_HARNESS_CONFIG.mux.git.activePollMs,
|
|
560
|
+
),
|
|
561
|
+
idlePollMs: normalizeNonNegativeInt(
|
|
562
|
+
record['idlePollMs'],
|
|
563
|
+
DEFAULT_HARNESS_CONFIG.mux.git.idlePollMs,
|
|
564
|
+
),
|
|
565
|
+
burstPollMs: normalizeNonNegativeInt(
|
|
566
|
+
record['burstPollMs'],
|
|
567
|
+
DEFAULT_HARNESS_CONFIG.mux.git.burstPollMs,
|
|
568
|
+
),
|
|
569
|
+
burstWindowMs: normalizeNonNegativeInt(
|
|
570
|
+
record['burstWindowMs'],
|
|
571
|
+
DEFAULT_HARNESS_CONFIG.mux.git.burstWindowMs,
|
|
572
|
+
),
|
|
573
|
+
triggerDebounceMs: normalizeNonNegativeInt(
|
|
574
|
+
record['triggerDebounceMs'],
|
|
575
|
+
DEFAULT_HARNESS_CONFIG.mux.git.triggerDebounceMs,
|
|
576
|
+
),
|
|
577
|
+
maxConcurrency: Math.max(
|
|
578
|
+
1,
|
|
579
|
+
normalizeNonNegativeInt(
|
|
580
|
+
record['maxConcurrency'],
|
|
581
|
+
DEFAULT_HARNESS_CONFIG.mux.git.maxConcurrency,
|
|
582
|
+
),
|
|
583
|
+
),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function normalizePerfConfig(input: unknown): HarnessPerfConfig {
|
|
588
|
+
const record = asRecord(input);
|
|
589
|
+
if (record === null) {
|
|
590
|
+
return DEFAULT_HARNESS_CONFIG.debug.perf;
|
|
591
|
+
}
|
|
592
|
+
const enabled =
|
|
593
|
+
typeof record['enabled'] === 'boolean'
|
|
594
|
+
? record['enabled']
|
|
595
|
+
: DEFAULT_HARNESS_CONFIG.debug.perf.enabled;
|
|
596
|
+
const filePath =
|
|
597
|
+
typeof record['filePath'] === 'string' && record['filePath'].trim().length > 0
|
|
598
|
+
? record['filePath'].trim()
|
|
599
|
+
: DEFAULT_HARNESS_CONFIG.debug.perf.filePath;
|
|
600
|
+
return {
|
|
601
|
+
enabled,
|
|
602
|
+
filePath,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function normalizeNonNegativeInt(value: unknown, fallback: number): number {
|
|
607
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
608
|
+
return fallback;
|
|
609
|
+
}
|
|
610
|
+
const normalized = Math.floor(value);
|
|
611
|
+
if (normalized < 0) {
|
|
612
|
+
return fallback;
|
|
613
|
+
}
|
|
614
|
+
return normalized;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function normalizeDebugMuxConfig(input: unknown): HarnessDebugMuxConfig {
|
|
618
|
+
const record = asRecord(input);
|
|
619
|
+
if (record === null) {
|
|
620
|
+
return DEFAULT_HARNESS_CONFIG.debug.mux;
|
|
621
|
+
}
|
|
622
|
+
const debugPathRaw = record['debugPath'];
|
|
623
|
+
const debugPath =
|
|
624
|
+
typeof debugPathRaw === 'string' && debugPathRaw.trim().length > 0
|
|
625
|
+
? debugPathRaw.trim()
|
|
626
|
+
: DEFAULT_HARNESS_CONFIG.debug.mux.debugPath;
|
|
627
|
+
const validateAnsi =
|
|
628
|
+
typeof record['validateAnsi'] === 'boolean'
|
|
629
|
+
? record['validateAnsi']
|
|
630
|
+
: DEFAULT_HARNESS_CONFIG.debug.mux.validateAnsi;
|
|
631
|
+
const resizeMinIntervalMs = normalizeNonNegativeInt(
|
|
632
|
+
record['resizeMinIntervalMs'],
|
|
633
|
+
DEFAULT_HARNESS_CONFIG.debug.mux.resizeMinIntervalMs,
|
|
634
|
+
);
|
|
635
|
+
const ptyResizeSettleMs = normalizeNonNegativeInt(
|
|
636
|
+
record['ptyResizeSettleMs'],
|
|
637
|
+
DEFAULT_HARNESS_CONFIG.debug.mux.ptyResizeSettleMs,
|
|
638
|
+
);
|
|
639
|
+
const startupSettleQuietMs = normalizeNonNegativeInt(
|
|
640
|
+
record['startupSettleQuietMs'],
|
|
641
|
+
DEFAULT_HARNESS_CONFIG.debug.mux.startupSettleQuietMs,
|
|
642
|
+
);
|
|
643
|
+
const serverSnapshotModelEnabled =
|
|
644
|
+
typeof record['serverSnapshotModelEnabled'] === 'boolean'
|
|
645
|
+
? record['serverSnapshotModelEnabled']
|
|
646
|
+
: DEFAULT_HARNESS_CONFIG.debug.mux.serverSnapshotModelEnabled;
|
|
647
|
+
return {
|
|
648
|
+
debugPath,
|
|
649
|
+
validateAnsi,
|
|
650
|
+
resizeMinIntervalMs,
|
|
651
|
+
ptyResizeSettleMs,
|
|
652
|
+
startupSettleQuietMs,
|
|
653
|
+
serverSnapshotModelEnabled,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function normalizeInspectPort(value: unknown, fallback: number): number {
|
|
658
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
659
|
+
return fallback;
|
|
660
|
+
}
|
|
661
|
+
const normalized = Math.floor(value);
|
|
662
|
+
if (normalized < 1 || normalized > 65535) {
|
|
663
|
+
return fallback;
|
|
664
|
+
}
|
|
665
|
+
return normalized;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function normalizeDebugInspectConfig(input: unknown): HarnessDebugInspectConfig {
|
|
669
|
+
const record = asRecord(input);
|
|
670
|
+
if (record === null) {
|
|
671
|
+
return DEFAULT_HARNESS_CONFIG.debug.inspect;
|
|
672
|
+
}
|
|
673
|
+
const enabled =
|
|
674
|
+
typeof record['enabled'] === 'boolean'
|
|
675
|
+
? record['enabled']
|
|
676
|
+
: DEFAULT_HARNESS_CONFIG.debug.inspect.enabled;
|
|
677
|
+
const gatewayPort = normalizeInspectPort(
|
|
678
|
+
record['gatewayPort'],
|
|
679
|
+
DEFAULT_HARNESS_CONFIG.debug.inspect.gatewayPort,
|
|
680
|
+
);
|
|
681
|
+
const clientPort = normalizeInspectPort(
|
|
682
|
+
record['clientPort'],
|
|
683
|
+
DEFAULT_HARNESS_CONFIG.debug.inspect.clientPort,
|
|
684
|
+
);
|
|
685
|
+
return {
|
|
686
|
+
enabled,
|
|
687
|
+
gatewayPort,
|
|
688
|
+
clientPort,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function normalizeDebugConfig(input: unknown, legacyPerf: HarnessPerfConfig): HarnessDebugConfig {
|
|
693
|
+
const record = asRecord(input);
|
|
694
|
+
if (record === null) {
|
|
695
|
+
return {
|
|
696
|
+
...DEFAULT_HARNESS_CONFIG.debug,
|
|
697
|
+
perf: legacyPerf,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
const enabled =
|
|
701
|
+
typeof record['enabled'] === 'boolean'
|
|
702
|
+
? record['enabled']
|
|
703
|
+
: DEFAULT_HARNESS_CONFIG.debug.enabled;
|
|
704
|
+
const overwriteArtifactsOnStart =
|
|
705
|
+
typeof record['overwriteArtifactsOnStart'] === 'boolean'
|
|
706
|
+
? record['overwriteArtifactsOnStart']
|
|
707
|
+
: DEFAULT_HARNESS_CONFIG.debug.overwriteArtifactsOnStart;
|
|
708
|
+
const perf = normalizePerfConfig(record['perf']);
|
|
709
|
+
const mux = normalizeDebugMuxConfig(record['mux']);
|
|
710
|
+
const inspect = normalizeDebugInspectConfig(record['inspect']);
|
|
711
|
+
return {
|
|
712
|
+
enabled,
|
|
713
|
+
overwriteArtifactsOnStart,
|
|
714
|
+
perf,
|
|
715
|
+
mux,
|
|
716
|
+
inspect,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function normalizePort(value: unknown, fallback: number): number {
|
|
721
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
722
|
+
return fallback;
|
|
723
|
+
}
|
|
724
|
+
const rounded = Math.floor(value);
|
|
725
|
+
if (rounded < 0 || rounded > 65535) {
|
|
726
|
+
return fallback;
|
|
727
|
+
}
|
|
728
|
+
return rounded;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function normalizeHost(value: unknown, fallback: string): string {
|
|
732
|
+
if (typeof value !== 'string') {
|
|
733
|
+
return fallback;
|
|
734
|
+
}
|
|
735
|
+
const trimmed = value.trim();
|
|
736
|
+
if (trimmed.length === 0) {
|
|
737
|
+
return fallback;
|
|
738
|
+
}
|
|
739
|
+
return trimmed;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function normalizeCodexTelemetryConfig(input: unknown): HarnessCodexTelemetryConfig {
|
|
743
|
+
const record = asRecord(input);
|
|
744
|
+
if (record === null) {
|
|
745
|
+
return DEFAULT_HARNESS_CONFIG.codex.telemetry;
|
|
746
|
+
}
|
|
747
|
+
const rawIngestMode = record['ingestMode'];
|
|
748
|
+
const ingestMode =
|
|
749
|
+
rawIngestMode === 'full' || rawIngestMode === 'lifecycle-fast'
|
|
750
|
+
? rawIngestMode
|
|
751
|
+
: DEFAULT_HARNESS_CONFIG.codex.telemetry.ingestMode;
|
|
752
|
+
return {
|
|
753
|
+
enabled:
|
|
754
|
+
typeof record['enabled'] === 'boolean'
|
|
755
|
+
? record['enabled']
|
|
756
|
+
: DEFAULT_HARNESS_CONFIG.codex.telemetry.enabled,
|
|
757
|
+
host: normalizeHost(record['host'], DEFAULT_HARNESS_CONFIG.codex.telemetry.host),
|
|
758
|
+
port: normalizePort(record['port'], DEFAULT_HARNESS_CONFIG.codex.telemetry.port),
|
|
759
|
+
logUserPrompt:
|
|
760
|
+
typeof record['logUserPrompt'] === 'boolean'
|
|
761
|
+
? record['logUserPrompt']
|
|
762
|
+
: DEFAULT_HARNESS_CONFIG.codex.telemetry.logUserPrompt,
|
|
763
|
+
captureLogs:
|
|
764
|
+
typeof record['captureLogs'] === 'boolean'
|
|
765
|
+
? record['captureLogs']
|
|
766
|
+
: DEFAULT_HARNESS_CONFIG.codex.telemetry.captureLogs,
|
|
767
|
+
captureMetrics:
|
|
768
|
+
typeof record['captureMetrics'] === 'boolean'
|
|
769
|
+
? record['captureMetrics']
|
|
770
|
+
: DEFAULT_HARNESS_CONFIG.codex.telemetry.captureMetrics,
|
|
771
|
+
captureTraces:
|
|
772
|
+
typeof record['captureTraces'] === 'boolean'
|
|
773
|
+
? record['captureTraces']
|
|
774
|
+
: DEFAULT_HARNESS_CONFIG.codex.telemetry.captureTraces,
|
|
775
|
+
captureVerboseEvents:
|
|
776
|
+
typeof record['captureVerboseEvents'] === 'boolean'
|
|
777
|
+
? record['captureVerboseEvents']
|
|
778
|
+
: DEFAULT_HARNESS_CONFIG.codex.telemetry.captureVerboseEvents,
|
|
779
|
+
ingestMode,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function normalizeCodexHistoryConfig(input: unknown): HarnessCodexHistoryConfig {
|
|
784
|
+
const record = asRecord(input);
|
|
785
|
+
if (record === null) {
|
|
786
|
+
return DEFAULT_HARNESS_CONFIG.codex.history;
|
|
787
|
+
}
|
|
788
|
+
const filePath =
|
|
789
|
+
typeof record['filePath'] === 'string' && record['filePath'].trim().length > 0
|
|
790
|
+
? record['filePath'].trim()
|
|
791
|
+
: DEFAULT_HARNESS_CONFIG.codex.history.filePath;
|
|
792
|
+
return {
|
|
793
|
+
enabled:
|
|
794
|
+
typeof record['enabled'] === 'boolean'
|
|
795
|
+
? record['enabled']
|
|
796
|
+
: DEFAULT_HARNESS_CONFIG.codex.history.enabled,
|
|
797
|
+
filePath,
|
|
798
|
+
pollMs: normalizeNonNegativeInt(record['pollMs'], DEFAULT_HARNESS_CONFIG.codex.history.pollMs),
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function readCodexLaunchMode(value: unknown): HarnessCodexLaunchMode | null {
|
|
803
|
+
if (typeof value !== 'string') {
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
const normalized = value.trim().toLowerCase();
|
|
807
|
+
if (normalized === 'yolo' || normalized === 'standard') {
|
|
808
|
+
return normalized;
|
|
809
|
+
}
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function normalizeCodexDirectoryModesConfig(
|
|
814
|
+
input: unknown,
|
|
815
|
+
): Readonly<Record<string, HarnessCodexLaunchMode>> {
|
|
816
|
+
const record = asRecord(input);
|
|
817
|
+
if (record === null) {
|
|
818
|
+
return DEFAULT_HARNESS_CONFIG.codex.launch.directoryModes;
|
|
819
|
+
}
|
|
820
|
+
const out: Record<string, HarnessCodexLaunchMode> = {};
|
|
821
|
+
for (const [rawPath, rawMode] of Object.entries(record)) {
|
|
822
|
+
const path = rawPath.trim();
|
|
823
|
+
if (path.length === 0) {
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
const mode = readCodexLaunchMode(rawMode);
|
|
827
|
+
if (mode === null) {
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
out[path] = mode;
|
|
831
|
+
}
|
|
832
|
+
return out;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function normalizeCodexLaunchConfig(input: unknown): HarnessCodexLaunchConfig {
|
|
836
|
+
const record = asRecord(input);
|
|
837
|
+
if (record === null) {
|
|
838
|
+
return DEFAULT_HARNESS_CONFIG.codex.launch;
|
|
839
|
+
}
|
|
840
|
+
const defaultMode =
|
|
841
|
+
readCodexLaunchMode(record['defaultMode']) ?? DEFAULT_HARNESS_CONFIG.codex.launch.defaultMode;
|
|
842
|
+
return {
|
|
843
|
+
defaultMode,
|
|
844
|
+
directoryModes: normalizeCodexDirectoryModesConfig(record['directoryModes']),
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function normalizeCodexConfig(input: unknown): HarnessCodexConfig {
|
|
849
|
+
const record = asRecord(input);
|
|
850
|
+
if (record === null) {
|
|
851
|
+
return DEFAULT_HARNESS_CONFIG.codex;
|
|
852
|
+
}
|
|
853
|
+
return {
|
|
854
|
+
telemetry: normalizeCodexTelemetryConfig(record['telemetry']),
|
|
855
|
+
history: normalizeCodexHistoryConfig(record['history']),
|
|
856
|
+
launch: normalizeCodexLaunchConfig(record['launch']),
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function readClaudeLaunchMode(value: unknown): HarnessClaudeLaunchMode | null {
|
|
861
|
+
if (typeof value !== 'string') {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
const normalized = value.trim().toLowerCase();
|
|
865
|
+
if (normalized === 'yolo' || normalized === 'standard') {
|
|
866
|
+
return normalized;
|
|
867
|
+
}
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function normalizeClaudeDirectoryModesConfig(
|
|
872
|
+
input: unknown,
|
|
873
|
+
): Readonly<Record<string, HarnessClaudeLaunchMode>> {
|
|
874
|
+
const record = asRecord(input);
|
|
875
|
+
if (record === null) {
|
|
876
|
+
return DEFAULT_HARNESS_CONFIG.claude.launch.directoryModes;
|
|
877
|
+
}
|
|
878
|
+
const out: Record<string, HarnessClaudeLaunchMode> = {};
|
|
879
|
+
for (const [rawPath, rawMode] of Object.entries(record)) {
|
|
880
|
+
const path = rawPath.trim();
|
|
881
|
+
if (path.length === 0) {
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
const mode = readClaudeLaunchMode(rawMode);
|
|
885
|
+
if (mode === null) {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
out[path] = mode;
|
|
889
|
+
}
|
|
890
|
+
return out;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function normalizeClaudeLaunchConfig(input: unknown): HarnessClaudeLaunchConfig {
|
|
894
|
+
const record = asRecord(input);
|
|
895
|
+
if (record === null) {
|
|
896
|
+
return DEFAULT_HARNESS_CONFIG.claude.launch;
|
|
897
|
+
}
|
|
898
|
+
const defaultMode =
|
|
899
|
+
readClaudeLaunchMode(record['defaultMode']) ?? DEFAULT_HARNESS_CONFIG.claude.launch.defaultMode;
|
|
900
|
+
return {
|
|
901
|
+
defaultMode,
|
|
902
|
+
directoryModes: normalizeClaudeDirectoryModesConfig(record['directoryModes']),
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function normalizeClaudeConfig(input: unknown): HarnessClaudeConfig {
|
|
907
|
+
const record = asRecord(input);
|
|
908
|
+
if (record === null) {
|
|
909
|
+
return DEFAULT_HARNESS_CONFIG.claude;
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
launch: normalizeClaudeLaunchConfig(record['launch']),
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function readCursorLaunchMode(value: unknown): HarnessCursorLaunchMode | null {
|
|
917
|
+
if (typeof value !== 'string') {
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
const normalized = value.trim().toLowerCase();
|
|
921
|
+
if (normalized === 'yolo' || normalized === 'standard') {
|
|
922
|
+
return normalized;
|
|
923
|
+
}
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function normalizeCursorDirectoryModesConfig(
|
|
928
|
+
input: unknown,
|
|
929
|
+
): Readonly<Record<string, HarnessCursorLaunchMode>> {
|
|
930
|
+
const record = asRecord(input);
|
|
931
|
+
if (record === null) {
|
|
932
|
+
return DEFAULT_HARNESS_CONFIG.cursor.launch.directoryModes;
|
|
933
|
+
}
|
|
934
|
+
const out: Record<string, HarnessCursorLaunchMode> = {};
|
|
935
|
+
for (const [rawPath, rawMode] of Object.entries(record)) {
|
|
936
|
+
const path = rawPath.trim();
|
|
937
|
+
if (path.length === 0) {
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
const mode = readCursorLaunchMode(rawMode);
|
|
941
|
+
if (mode === null) {
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
out[path] = mode;
|
|
945
|
+
}
|
|
946
|
+
return out;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function normalizeCursorLaunchConfig(input: unknown): HarnessCursorLaunchConfig {
|
|
950
|
+
const record = asRecord(input);
|
|
951
|
+
if (record === null) {
|
|
952
|
+
return DEFAULT_HARNESS_CONFIG.cursor.launch;
|
|
953
|
+
}
|
|
954
|
+
const defaultMode =
|
|
955
|
+
readCursorLaunchMode(record['defaultMode']) ?? DEFAULT_HARNESS_CONFIG.cursor.launch.defaultMode;
|
|
956
|
+
return {
|
|
957
|
+
defaultMode,
|
|
958
|
+
directoryModes: normalizeCursorDirectoryModesConfig(record['directoryModes']),
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function normalizeCursorConfig(input: unknown): HarnessCursorConfig {
|
|
963
|
+
const record = asRecord(input);
|
|
964
|
+
if (record === null) {
|
|
965
|
+
return DEFAULT_HARNESS_CONFIG.cursor;
|
|
966
|
+
}
|
|
967
|
+
return {
|
|
968
|
+
launch: normalizeCursorLaunchConfig(record['launch']),
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function normalizeStringArray(input: unknown, fallback: readonly string[]): readonly string[] {
|
|
973
|
+
if (!Array.isArray(input)) {
|
|
974
|
+
return fallback;
|
|
975
|
+
}
|
|
976
|
+
const normalized = input
|
|
977
|
+
.flatMap((value) => (typeof value === 'string' ? [value.trim()] : []))
|
|
978
|
+
.filter((value) => value.length > 0);
|
|
979
|
+
return normalized.length === 0 ? fallback : normalized;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function normalizeCritiqueLaunchConfig(input: unknown): HarnessCritiqueLaunchConfig {
|
|
983
|
+
const record = asRecord(input);
|
|
984
|
+
if (record === null) {
|
|
985
|
+
return DEFAULT_HARNESS_CONFIG.critique.launch;
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
defaultArgs: normalizeStringArray(
|
|
989
|
+
record['defaultArgs'],
|
|
990
|
+
DEFAULT_HARNESS_CONFIG.critique.launch.defaultArgs,
|
|
991
|
+
),
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function normalizeCritiqueInstallConfig(input: unknown): HarnessCritiqueInstallConfig {
|
|
996
|
+
const record = asRecord(input);
|
|
997
|
+
if (record === null) {
|
|
998
|
+
return DEFAULT_HARNESS_CONFIG.critique.install;
|
|
999
|
+
}
|
|
1000
|
+
const packageName =
|
|
1001
|
+
typeof record['package'] === 'string' && record['package'].trim().length > 0
|
|
1002
|
+
? record['package'].trim()
|
|
1003
|
+
: DEFAULT_HARNESS_CONFIG.critique.install.package;
|
|
1004
|
+
return {
|
|
1005
|
+
autoInstall:
|
|
1006
|
+
typeof record['autoInstall'] === 'boolean'
|
|
1007
|
+
? record['autoInstall']
|
|
1008
|
+
: DEFAULT_HARNESS_CONFIG.critique.install.autoInstall,
|
|
1009
|
+
package: packageName,
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function normalizeCritiqueConfig(input: unknown): HarnessCritiqueConfig {
|
|
1014
|
+
const record = asRecord(input);
|
|
1015
|
+
if (record === null) {
|
|
1016
|
+
return DEFAULT_HARNESS_CONFIG.critique;
|
|
1017
|
+
}
|
|
1018
|
+
return {
|
|
1019
|
+
launch: normalizeCritiqueLaunchConfig(record['launch']),
|
|
1020
|
+
install: normalizeCritiqueInstallConfig(record['install']),
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function isHarnessLifecycleEventType(value: string): value is HarnessLifecycleEventType {
|
|
1025
|
+
return (HARNESS_LIFECYCLE_EVENT_TYPES as readonly string[]).includes(value);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function normalizeLifecycleEventCategoryMap(
|
|
1029
|
+
input: unknown,
|
|
1030
|
+
): Readonly<Partial<Record<HarnessLifecycleEventType, string>>> {
|
|
1031
|
+
const record = asRecord(input);
|
|
1032
|
+
if (record === null) {
|
|
1033
|
+
return DEFAULT_HARNESS_CONFIG.hooks.lifecycle.peonPing.eventCategoryMap;
|
|
1034
|
+
}
|
|
1035
|
+
const out: Partial<Record<HarnessLifecycleEventType, string>> = {};
|
|
1036
|
+
for (const [rawEventType, rawCategory] of Object.entries(record)) {
|
|
1037
|
+
if (!isHarnessLifecycleEventType(rawEventType)) {
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
if (typeof rawCategory !== 'string') {
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
const category = rawCategory.trim();
|
|
1044
|
+
if (category.length === 0) {
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
out[rawEventType] = category;
|
|
1048
|
+
}
|
|
1049
|
+
return out;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function normalizeLifecycleProviders(input: unknown): HarnessLifecycleProviderConfig {
|
|
1053
|
+
const record = asRecord(input);
|
|
1054
|
+
if (record === null) {
|
|
1055
|
+
return DEFAULT_HARNESS_CONFIG.hooks.lifecycle.providers;
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
codex:
|
|
1059
|
+
typeof record['codex'] === 'boolean'
|
|
1060
|
+
? record['codex']
|
|
1061
|
+
: DEFAULT_HARNESS_CONFIG.hooks.lifecycle.providers.codex,
|
|
1062
|
+
claude:
|
|
1063
|
+
typeof record['claude'] === 'boolean'
|
|
1064
|
+
? record['claude']
|
|
1065
|
+
: DEFAULT_HARNESS_CONFIG.hooks.lifecycle.providers.claude,
|
|
1066
|
+
cursor:
|
|
1067
|
+
typeof record['cursor'] === 'boolean'
|
|
1068
|
+
? record['cursor']
|
|
1069
|
+
: DEFAULT_HARNESS_CONFIG.hooks.lifecycle.providers.cursor,
|
|
1070
|
+
controlPlane:
|
|
1071
|
+
typeof record['controlPlane'] === 'boolean'
|
|
1072
|
+
? record['controlPlane']
|
|
1073
|
+
: DEFAULT_HARNESS_CONFIG.hooks.lifecycle.providers.controlPlane,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function normalizeLifecyclePeonPingConfig(input: unknown): HarnessLifecyclePeonPingConfig {
|
|
1078
|
+
const record = asRecord(input);
|
|
1079
|
+
if (record === null) {
|
|
1080
|
+
return DEFAULT_HARNESS_CONFIG.hooks.lifecycle.peonPing;
|
|
1081
|
+
}
|
|
1082
|
+
return {
|
|
1083
|
+
enabled:
|
|
1084
|
+
typeof record['enabled'] === 'boolean'
|
|
1085
|
+
? record['enabled']
|
|
1086
|
+
: DEFAULT_HARNESS_CONFIG.hooks.lifecycle.peonPing.enabled,
|
|
1087
|
+
baseUrl:
|
|
1088
|
+
typeof record['baseUrl'] === 'string' && record['baseUrl'].trim().length > 0
|
|
1089
|
+
? record['baseUrl'].trim()
|
|
1090
|
+
: DEFAULT_HARNESS_CONFIG.hooks.lifecycle.peonPing.baseUrl,
|
|
1091
|
+
timeoutMs: normalizeNonNegativeInt(
|
|
1092
|
+
record['timeoutMs'],
|
|
1093
|
+
DEFAULT_HARNESS_CONFIG.hooks.lifecycle.peonPing.timeoutMs,
|
|
1094
|
+
),
|
|
1095
|
+
eventCategoryMap: normalizeLifecycleEventCategoryMap(record['eventCategoryMap']),
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function normalizeStringMap(input: unknown): Readonly<Record<string, string>> {
|
|
1100
|
+
const record = asRecord(input);
|
|
1101
|
+
if (record === null) {
|
|
1102
|
+
return {};
|
|
1103
|
+
}
|
|
1104
|
+
const out: Record<string, string> = {};
|
|
1105
|
+
for (const [key, value] of Object.entries(record)) {
|
|
1106
|
+
const normalizedKey = key.trim();
|
|
1107
|
+
if (normalizedKey.length === 0 || typeof value !== 'string') {
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
const normalizedValue = value.trim();
|
|
1111
|
+
if (normalizedValue.length === 0) {
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
out[normalizedKey] = normalizedValue;
|
|
1115
|
+
}
|
|
1116
|
+
return out;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function normalizeLifecycleEventTypes(input: unknown): readonly HarnessLifecycleEventType[] {
|
|
1120
|
+
if (!Array.isArray(input)) {
|
|
1121
|
+
return [];
|
|
1122
|
+
}
|
|
1123
|
+
const out: HarnessLifecycleEventType[] = [];
|
|
1124
|
+
for (const value of input) {
|
|
1125
|
+
if (typeof value !== 'string') {
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
const normalized = value.trim();
|
|
1129
|
+
if (isHarnessLifecycleEventType(normalized) && !out.includes(normalized)) {
|
|
1130
|
+
out.push(normalized);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return out;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function normalizeLifecycleWebhookConfig(
|
|
1137
|
+
input: unknown,
|
|
1138
|
+
index: number,
|
|
1139
|
+
): HarnessLifecycleWebhookConfig | null {
|
|
1140
|
+
const record = asRecord(input);
|
|
1141
|
+
if (record === null) {
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
const defaultName = `webhook-${String(index + 1)}`;
|
|
1145
|
+
const name =
|
|
1146
|
+
typeof record['name'] === 'string' && record['name'].trim().length > 0
|
|
1147
|
+
? record['name'].trim()
|
|
1148
|
+
: defaultName;
|
|
1149
|
+
const url = typeof record['url'] === 'string' ? record['url'].trim() : '';
|
|
1150
|
+
if (url.length === 0) {
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
const methodRaw =
|
|
1154
|
+
typeof record['method'] === 'string' && record['method'].trim().length > 0
|
|
1155
|
+
? record['method'].trim().toUpperCase()
|
|
1156
|
+
: 'POST';
|
|
1157
|
+
const method = methodRaw;
|
|
1158
|
+
return {
|
|
1159
|
+
name,
|
|
1160
|
+
enabled: typeof record['enabled'] === 'boolean' ? record['enabled'] : true,
|
|
1161
|
+
url,
|
|
1162
|
+
method,
|
|
1163
|
+
timeoutMs: normalizeNonNegativeInt(record['timeoutMs'], 1200),
|
|
1164
|
+
headers: normalizeStringMap(record['headers']),
|
|
1165
|
+
eventTypes: normalizeLifecycleEventTypes(record['eventTypes']),
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function normalizeLifecycleHooksConfig(input: unknown): HarnessLifecycleHooksConfig {
|
|
1170
|
+
const record = asRecord(input);
|
|
1171
|
+
if (record === null) {
|
|
1172
|
+
return DEFAULT_HARNESS_CONFIG.hooks.lifecycle;
|
|
1173
|
+
}
|
|
1174
|
+
const webhooksRaw = Array.isArray(record['webhooks']) ? record['webhooks'] : [];
|
|
1175
|
+
const webhooks: HarnessLifecycleWebhookConfig[] = [];
|
|
1176
|
+
for (let index = 0; index < webhooksRaw.length; index += 1) {
|
|
1177
|
+
const normalized = normalizeLifecycleWebhookConfig(webhooksRaw[index], index);
|
|
1178
|
+
if (normalized !== null) {
|
|
1179
|
+
webhooks.push(normalized);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return {
|
|
1183
|
+
enabled:
|
|
1184
|
+
typeof record['enabled'] === 'boolean'
|
|
1185
|
+
? record['enabled']
|
|
1186
|
+
: DEFAULT_HARNESS_CONFIG.hooks.lifecycle.enabled,
|
|
1187
|
+
providers: normalizeLifecycleProviders(record['providers']),
|
|
1188
|
+
peonPing: normalizeLifecyclePeonPingConfig(record['peonPing']),
|
|
1189
|
+
webhooks,
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
export function parseHarnessConfigText(text: string): HarnessConfig {
|
|
1194
|
+
const stripped = stripTrailingCommas(stripJsoncComments(text));
|
|
1195
|
+
const parsed = JSON.parse(stripped) as unknown;
|
|
1196
|
+
const root = asRecord(parsed);
|
|
1197
|
+
if (root === null) {
|
|
1198
|
+
return DEFAULT_HARNESS_CONFIG;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const mux = asRecord(root['mux']);
|
|
1202
|
+
const legacyPerf = normalizePerfConfig(root['perf']);
|
|
1203
|
+
const debug = normalizeDebugConfig(root['debug'], legacyPerf);
|
|
1204
|
+
const codex = normalizeCodexConfig(root['codex']);
|
|
1205
|
+
const claude = normalizeClaudeConfig(root['claude']);
|
|
1206
|
+
const cursor = normalizeCursorConfig(root['cursor']);
|
|
1207
|
+
const critique = normalizeCritiqueConfig(root['critique']);
|
|
1208
|
+
const hooks = normalizeLifecycleHooksConfig(asRecord(root['hooks'])?.['lifecycle']);
|
|
1209
|
+
|
|
1210
|
+
return {
|
|
1211
|
+
mux: {
|
|
1212
|
+
keybindings: mux === null ? {} : normalizeKeybindings(mux['keybindings']),
|
|
1213
|
+
ui: mux === null ? DEFAULT_HARNESS_CONFIG.mux.ui : normalizeMuxUiConfig(mux['ui']),
|
|
1214
|
+
git: mux === null ? DEFAULT_HARNESS_CONFIG.mux.git : normalizeMuxGitConfig(mux['git']),
|
|
1215
|
+
},
|
|
1216
|
+
debug,
|
|
1217
|
+
codex,
|
|
1218
|
+
claude,
|
|
1219
|
+
cursor,
|
|
1220
|
+
critique,
|
|
1221
|
+
hooks: {
|
|
1222
|
+
lifecycle: hooks,
|
|
1223
|
+
},
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
export function resolveHarnessConfigPath(cwd: string): string {
|
|
1228
|
+
return resolve(cwd, HARNESS_CONFIG_FILE_NAME);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
export function loadHarnessConfig(options?: {
|
|
1232
|
+
cwd?: string;
|
|
1233
|
+
filePath?: string;
|
|
1234
|
+
lastKnownGood?: HarnessConfig;
|
|
1235
|
+
}): LoadedHarnessConfig {
|
|
1236
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
1237
|
+
const filePath = options?.filePath ?? resolveHarnessConfigPath(cwd);
|
|
1238
|
+
const lastKnownGood = options?.lastKnownGood ?? DEFAULT_HARNESS_CONFIG;
|
|
1239
|
+
|
|
1240
|
+
if (!existsSync(filePath)) {
|
|
1241
|
+
return {
|
|
1242
|
+
filePath,
|
|
1243
|
+
config: lastKnownGood,
|
|
1244
|
+
fromLastKnownGood: false,
|
|
1245
|
+
error: null,
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
try {
|
|
1250
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
1251
|
+
return {
|
|
1252
|
+
filePath,
|
|
1253
|
+
config: parseHarnessConfigText(raw),
|
|
1254
|
+
fromLastKnownGood: false,
|
|
1255
|
+
error: null,
|
|
1256
|
+
};
|
|
1257
|
+
} catch (error: unknown) {
|
|
1258
|
+
return {
|
|
1259
|
+
filePath,
|
|
1260
|
+
config: lastKnownGood,
|
|
1261
|
+
fromLastKnownGood: true,
|
|
1262
|
+
error: String(error),
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function serializeHarnessConfig(config: HarnessConfig): string {
|
|
1268
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function readCurrentHarnessConfig(filePath: string): HarnessConfig {
|
|
1272
|
+
if (!existsSync(filePath)) {
|
|
1273
|
+
return DEFAULT_HARNESS_CONFIG;
|
|
1274
|
+
}
|
|
1275
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
1276
|
+
return parseHarnessConfigText(raw);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function roundUiPercent(value: number): number {
|
|
1280
|
+
return Math.round(value * 100) / 100;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
export function updateHarnessConfig(options: {
|
|
1284
|
+
cwd?: string;
|
|
1285
|
+
filePath?: string;
|
|
1286
|
+
update: (current: HarnessConfig) => HarnessConfig;
|
|
1287
|
+
}): HarnessConfig {
|
|
1288
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1289
|
+
const filePath = options.filePath ?? resolveHarnessConfigPath(cwd);
|
|
1290
|
+
const current = readCurrentHarnessConfig(filePath);
|
|
1291
|
+
const next = options.update(current);
|
|
1292
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
1293
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${randomUUID()}`;
|
|
1294
|
+
try {
|
|
1295
|
+
writeFileSync(tempPath, serializeHarnessConfig(next), 'utf8');
|
|
1296
|
+
renameSync(tempPath, filePath);
|
|
1297
|
+
} catch (error: unknown) {
|
|
1298
|
+
try {
|
|
1299
|
+
unlinkSync(tempPath);
|
|
1300
|
+
} catch {
|
|
1301
|
+
// Best-effort cleanup only.
|
|
1302
|
+
}
|
|
1303
|
+
throw error;
|
|
1304
|
+
}
|
|
1305
|
+
return next;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
export function updateHarnessMuxUiConfig(
|
|
1309
|
+
update: Partial<{
|
|
1310
|
+
paneWidthPercent: number | null;
|
|
1311
|
+
repositoriesCollapsed: boolean;
|
|
1312
|
+
shortcutsCollapsed: boolean;
|
|
1313
|
+
}>,
|
|
1314
|
+
options?: {
|
|
1315
|
+
cwd?: string;
|
|
1316
|
+
filePath?: string;
|
|
1317
|
+
},
|
|
1318
|
+
): HarnessConfig {
|
|
1319
|
+
const updateOptions: {
|
|
1320
|
+
cwd?: string;
|
|
1321
|
+
filePath?: string;
|
|
1322
|
+
update: (current: HarnessConfig) => HarnessConfig;
|
|
1323
|
+
} = {
|
|
1324
|
+
update: (current) => {
|
|
1325
|
+
const nextPaneWidthPercent =
|
|
1326
|
+
update.paneWidthPercent === undefined
|
|
1327
|
+
? current.mux.ui.paneWidthPercent
|
|
1328
|
+
: normalizePaneWidthPercent(update.paneWidthPercent, null);
|
|
1329
|
+
const nextShortcutsCollapsed =
|
|
1330
|
+
update.shortcutsCollapsed === undefined
|
|
1331
|
+
? current.mux.ui.shortcutsCollapsed
|
|
1332
|
+
: update.shortcutsCollapsed;
|
|
1333
|
+
const nextRepositoriesCollapsed =
|
|
1334
|
+
update.repositoriesCollapsed === undefined
|
|
1335
|
+
? current.mux.ui.repositoriesCollapsed
|
|
1336
|
+
: update.repositoriesCollapsed;
|
|
1337
|
+
return {
|
|
1338
|
+
...current,
|
|
1339
|
+
mux: {
|
|
1340
|
+
...current.mux,
|
|
1341
|
+
ui: {
|
|
1342
|
+
paneWidthPercent:
|
|
1343
|
+
nextPaneWidthPercent === null ? null : roundUiPercent(nextPaneWidthPercent),
|
|
1344
|
+
repositoriesCollapsed: nextRepositoriesCollapsed,
|
|
1345
|
+
shortcutsCollapsed: nextShortcutsCollapsed,
|
|
1346
|
+
theme: current.mux.ui.theme,
|
|
1347
|
+
},
|
|
1348
|
+
},
|
|
1349
|
+
};
|
|
1350
|
+
},
|
|
1351
|
+
};
|
|
1352
|
+
if (options?.cwd !== undefined) {
|
|
1353
|
+
updateOptions.cwd = options.cwd;
|
|
1354
|
+
}
|
|
1355
|
+
if (options?.filePath !== undefined) {
|
|
1356
|
+
updateOptions.filePath = options.filePath;
|
|
1357
|
+
}
|
|
1358
|
+
return updateHarnessConfig(updateOptions);
|
|
1359
|
+
}
|