@jmoyers/harness 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +33 -155
  2. package/package.json +5 -1
  3. package/packages/harness-ai/src/anthropic-client.ts +99 -0
  4. package/packages/harness-ai/src/anthropic-protocol.ts +581 -0
  5. package/packages/harness-ai/src/anthropic-provider.ts +82 -0
  6. package/packages/harness-ai/src/async-iterable-stream.ts +65 -0
  7. package/packages/harness-ai/src/index.ts +36 -0
  8. package/packages/harness-ai/src/json-parse.ts +66 -0
  9. package/packages/harness-ai/src/sse.ts +80 -0
  10. package/packages/harness-ai/src/stream-object.ts +96 -0
  11. package/packages/harness-ai/src/stream-text.ts +1340 -0
  12. package/packages/harness-ai/src/types.ts +330 -0
  13. package/packages/harness-ai/src/ui-stream.ts +217 -0
  14. package/scripts/codex-live-mux-runtime.ts +123 -7
  15. package/scripts/control-plane-daemon.ts +20 -3
  16. package/scripts/harness.ts +566 -133
  17. package/src/cli/gateway-record.ts +16 -1
  18. package/src/control-plane/agent-realtime-api.ts +4 -0
  19. package/src/control-plane/prompt/agent-prompt-extractor.ts +191 -0
  20. package/src/control-plane/prompt/extractors/claude-prompt-extractor.ts +53 -0
  21. package/src/control-plane/prompt/extractors/codex-prompt-extractor.ts +50 -0
  22. package/src/control-plane/prompt/extractors/cursor-prompt-extractor.ts +56 -0
  23. package/src/control-plane/prompt/session-prompt-engine.ts +69 -0
  24. package/src/control-plane/prompt/thread-title-namer.ts +290 -0
  25. package/src/control-plane/stream-command-parser.ts +12 -0
  26. package/src/control-plane/stream-protocol.ts +109 -0
  27. package/src/control-plane/stream-server-command.ts +14 -0
  28. package/src/control-plane/stream-server-session-runtime.ts +12 -0
  29. package/src/control-plane/stream-server.ts +485 -19
  30. package/src/mux/input-shortcuts.ts +9 -0
  31. package/src/mux/live-mux/critique-review.ts +5 -1
  32. package/src/mux/live-mux/git-parsing.ts +24 -0
  33. package/src/mux/live-mux/global-shortcut-handlers.ts +8 -0
  34. package/src/mux/render-frame.ts +1 -1
  35. package/src/pty/pty_host.ts +46 -1
  36. package/src/services/control-plane.ts +22 -0
  37. package/src/services/runtime-control-actions.ts +69 -0
  38. package/src/services/runtime-navigation-input.ts +4 -0
  39. package/src/services/runtime-rail-input.ts +4 -0
  40. package/src/services/runtime-workspace-actions.ts +5 -0
  41. package/src/ui/global-shortcut-input.ts +2 -0
@@ -6,6 +6,7 @@ export const DEFAULT_GATEWAY_PORT = 7777;
6
6
  export const DEFAULT_GATEWAY_DB_PATH = '.harness/control-plane.sqlite';
7
7
  export const DEFAULT_GATEWAY_RECORD_PATH = '.harness/gateway.json';
8
8
  export const DEFAULT_GATEWAY_LOG_PATH = '.harness/gateway.log';
9
+ export const DEFAULT_GATEWAY_LOCK_PATH = '.harness/gateway.lock';
9
10
 
10
11
  export interface GatewayRecord {
11
12
  readonly version: number;
@@ -16,6 +17,7 @@ export interface GatewayRecord {
16
17
  readonly stateDbPath: string;
17
18
  readonly startedAt: string;
18
19
  readonly workspaceRoot: string;
20
+ readonly gatewayRunId?: string;
19
21
  }
20
22
 
21
23
  function asRecord(value: unknown): Record<string, unknown> | null {
@@ -74,6 +76,13 @@ export function resolveGatewayLogPath(
74
76
  return resolveHarnessRuntimePath(workspaceRoot, DEFAULT_GATEWAY_LOG_PATH, env);
75
77
  }
76
78
 
79
+ export function resolveGatewayLockPath(
80
+ workspaceRoot: string,
81
+ env: NodeJS.ProcessEnv = process.env,
82
+ ): string {
83
+ return resolveHarnessRuntimePath(workspaceRoot, DEFAULT_GATEWAY_LOCK_PATH, env);
84
+ }
85
+
77
86
  export function normalizeGatewayHost(
78
87
  input: string | null | undefined,
79
88
  fallback = DEFAULT_GATEWAY_HOST,
@@ -149,6 +158,9 @@ export function parseGatewayRecordText(text: string): GatewayRecord | null {
149
158
  const workspaceRoot = readNonEmptyString(record['workspaceRoot']);
150
159
  const authTokenRaw = record['authToken'];
151
160
  const authToken = authTokenRaw === null ? null : readNonEmptyString(authTokenRaw);
161
+ const gatewayRunIdRaw = record['gatewayRunId'];
162
+ const gatewayRunId =
163
+ gatewayRunIdRaw === undefined ? undefined : readNonEmptyString(gatewayRunIdRaw);
152
164
 
153
165
  if (
154
166
  pid === null ||
@@ -157,10 +169,12 @@ export function parseGatewayRecordText(text: string): GatewayRecord | null {
157
169
  stateDbPath === null ||
158
170
  startedAt === null ||
159
171
  workspaceRoot === null ||
160
- (authToken === null && authTokenRaw !== null)
172
+ (authToken === null && authTokenRaw !== null) ||
173
+ (gatewayRunIdRaw !== undefined && gatewayRunId === null)
161
174
  ) {
162
175
  return null;
163
176
  }
177
+ const parsedGatewayRunId = gatewayRunId === null ? undefined : gatewayRunId;
164
178
 
165
179
  return {
166
180
  version,
@@ -171,6 +185,7 @@ export function parseGatewayRecordText(text: string): GatewayRecord | null {
171
185
  stateDbPath,
172
186
  startedAt,
173
187
  workspaceRoot,
188
+ ...(parsedGatewayRunId === undefined ? {} : { gatewayRunId: parsedGatewayRunId }),
174
189
  };
175
190
  }
176
191
 
@@ -54,6 +54,7 @@ interface AgentEventTypeMap {
54
54
  'session.status': Extract<StreamObservedEvent, { type: 'session-status' }>;
55
55
  'session.event': Extract<StreamObservedEvent, { type: 'session-event' }>;
56
56
  'session.telemetry': Extract<StreamObservedEvent, { type: 'session-key-event' }>;
57
+ 'session.prompt': Extract<StreamObservedEvent, { type: 'session-prompt-event' }>;
57
58
  'session.control': Extract<StreamObservedEvent, { type: 'session-control' }>;
58
59
  'session.output': Extract<StreamObservedEvent, { type: 'session-output' }>;
59
60
  }
@@ -1155,6 +1156,9 @@ function mapObservedEventType(observed: StreamObservedEvent): AgentRealtimeEvent
1155
1156
  if (observed.type === 'session-key-event') {
1156
1157
  return 'session.telemetry';
1157
1158
  }
1159
+ if (observed.type === 'session-prompt-event') {
1160
+ return 'session.prompt';
1161
+ }
1158
1162
  if (observed.type === 'session-control') {
1159
1163
  return 'session.control';
1160
1164
  }
@@ -0,0 +1,191 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type {
3
+ StreamPromptCaptureSource,
4
+ StreamPromptConfidence,
5
+ StreamSessionPromptRecord,
6
+ StreamTelemetrySource,
7
+ } from '../stream-protocol.ts';
8
+
9
+ export interface PromptFromNotifyInput {
10
+ readonly payload: Record<string, unknown>;
11
+ readonly observedAt: string;
12
+ }
13
+
14
+ export interface PromptFromTelemetryInput {
15
+ readonly source: StreamTelemetrySource;
16
+ readonly eventName: string | null;
17
+ readonly summary: string | null;
18
+ readonly payload: Record<string, unknown>;
19
+ readonly observedAt: string;
20
+ }
21
+
22
+ export interface AgentPromptExtractor {
23
+ readonly agentType: string;
24
+ fromNotify(input: PromptFromNotifyInput): StreamSessionPromptRecord | null;
25
+ fromTelemetry(input: PromptFromTelemetryInput): StreamSessionPromptRecord | null;
26
+ }
27
+
28
+ export function readTrimmedString(value: unknown): string | null {
29
+ if (typeof value !== 'string') {
30
+ return null;
31
+ }
32
+ const trimmed = value.trim();
33
+ return trimmed.length > 0 ? trimmed : null;
34
+ }
35
+
36
+ export function normalizeEventToken(value: string): string {
37
+ return value
38
+ .trim()
39
+ .toLowerCase()
40
+ .replace(/[^a-z0-9]+/g, '');
41
+ }
42
+
43
+ function asRecord(value: unknown): Record<string, unknown> | null {
44
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
45
+ return null;
46
+ }
47
+ return value as Record<string, unknown>;
48
+ }
49
+
50
+ function stableSerialize(value: unknown, depth = 0): string {
51
+ if (depth > 4) {
52
+ return '[depth-limit]';
53
+ }
54
+ if (value === null) {
55
+ return 'null';
56
+ }
57
+ if (typeof value === 'string') {
58
+ return JSON.stringify(value);
59
+ }
60
+ if (typeof value === 'number' || typeof value === 'boolean') {
61
+ return String(value);
62
+ }
63
+ if (Array.isArray(value)) {
64
+ return `[${value
65
+ .slice(0, 24)
66
+ .map((entry) => stableSerialize(entry, depth + 1))
67
+ .join(',')}]`;
68
+ }
69
+ const record = asRecord(value);
70
+ if (record === null) {
71
+ return typeof value;
72
+ }
73
+ const keys = Object.keys(record).sort().slice(0, 24);
74
+ return `{${keys
75
+ .map((key) => `${JSON.stringify(key)}:${stableSerialize(record[key], depth + 1)}`)
76
+ .join(',')}}`;
77
+ }
78
+
79
+ function hashPromptPayload(input: {
80
+ readonly text: string | null;
81
+ readonly providerEventName: string | null;
82
+ readonly payload: Record<string, unknown>;
83
+ }): string {
84
+ const hash = createHash('sha256');
85
+ hash.update(input.providerEventName ?? '');
86
+ hash.update('\n');
87
+ hash.update(input.text ?? '');
88
+ hash.update('\n');
89
+ hash.update(stableSerialize(input.payload));
90
+ return hash.digest('hex');
91
+ }
92
+
93
+ function lookupRecordValue(
94
+ record: Record<string, unknown>,
95
+ candidateKeys: readonly string[],
96
+ ): string | null {
97
+ const normalizedKeys = new Set(candidateKeys.map((key) => key.toLowerCase()));
98
+ for (const [key, value] of Object.entries(record)) {
99
+ if (!normalizedKeys.has(key.toLowerCase())) {
100
+ continue;
101
+ }
102
+ const text = readTrimmedString(value);
103
+ if (text !== null) {
104
+ return text;
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+
110
+ export function findPromptText(
111
+ value: unknown,
112
+ options: {
113
+ readonly keys: readonly string[];
114
+ readonly maxDepth?: number;
115
+ },
116
+ ): string | null {
117
+ const maxDepth = options.maxDepth ?? 3;
118
+ const visited = new Set<unknown>();
119
+ const queue: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }];
120
+ while (queue.length > 0) {
121
+ const next = queue.shift();
122
+ if (next === undefined) {
123
+ continue;
124
+ }
125
+ if (next.depth > maxDepth) {
126
+ continue;
127
+ }
128
+ if (next.value !== null && typeof next.value === 'object') {
129
+ if (visited.has(next.value)) {
130
+ continue;
131
+ }
132
+ visited.add(next.value);
133
+ }
134
+ if (typeof next.value === 'string') {
135
+ if (next.depth === 0) {
136
+ const text = readTrimmedString(next.value);
137
+ if (text !== null) {
138
+ return text;
139
+ }
140
+ }
141
+ continue;
142
+ }
143
+ const record = asRecord(next.value);
144
+ if (record !== null) {
145
+ const match = lookupRecordValue(record, options.keys);
146
+ if (match !== null) {
147
+ return match;
148
+ }
149
+ if (next.depth >= maxDepth) {
150
+ continue;
151
+ }
152
+ for (const nested of Object.values(record)) {
153
+ queue.push({ value: nested, depth: next.depth + 1 });
154
+ }
155
+ continue;
156
+ }
157
+ if (Array.isArray(next.value) && next.depth < maxDepth) {
158
+ for (const nested of next.value.slice(0, 24)) {
159
+ queue.push({ value: nested, depth: next.depth + 1 });
160
+ }
161
+ }
162
+ }
163
+ return null;
164
+ }
165
+
166
+ function providerPayloadKeys(payload: Record<string, unknown>): string[] {
167
+ return Object.keys(payload).sort().slice(0, 12);
168
+ }
169
+
170
+ export function createPromptRecord(input: {
171
+ readonly text: string | null;
172
+ readonly confidence: StreamPromptConfidence;
173
+ readonly captureSource: StreamPromptCaptureSource;
174
+ readonly providerEventName: string | null;
175
+ readonly payload: Record<string, unknown>;
176
+ readonly observedAt: string;
177
+ }): StreamSessionPromptRecord {
178
+ return {
179
+ text: input.text,
180
+ hash: hashPromptPayload({
181
+ text: input.text,
182
+ providerEventName: input.providerEventName,
183
+ payload: input.payload,
184
+ }),
185
+ confidence: input.confidence,
186
+ captureSource: input.captureSource,
187
+ providerEventName: input.providerEventName,
188
+ providerPayloadKeys: providerPayloadKeys(input.payload),
189
+ observedAt: input.observedAt,
190
+ };
191
+ }
@@ -0,0 +1,53 @@
1
+ import type { StreamSessionPromptRecord } from '../../stream-protocol.ts';
2
+ import type {
3
+ AgentPromptExtractor,
4
+ PromptFromNotifyInput,
5
+ PromptFromTelemetryInput,
6
+ } from '../agent-prompt-extractor.ts';
7
+ import {
8
+ createPromptRecord,
9
+ findPromptText,
10
+ normalizeEventToken,
11
+ readTrimmedString,
12
+ } from '../agent-prompt-extractor.ts';
13
+
14
+ function fromNotify(input: PromptFromNotifyInput): StreamSessionPromptRecord | null {
15
+ const hookEventName =
16
+ readTrimmedString(input.payload['hook_event_name']) ??
17
+ readTrimmedString(input.payload['hookEventName']);
18
+ if (hookEventName === null) {
19
+ return null;
20
+ }
21
+ const hookToken = normalizeEventToken(hookEventName);
22
+ if (hookToken !== 'userpromptsubmit') {
23
+ return null;
24
+ }
25
+ const directPrompt =
26
+ readTrimmedString(input.payload['prompt']) ??
27
+ readTrimmedString(input.payload['user_prompt']) ??
28
+ readTrimmedString(input.payload['userPrompt']);
29
+ const fallbackPrompt =
30
+ directPrompt ??
31
+ findPromptText(input.payload, {
32
+ keys: ['prompt', 'user_prompt', 'userPrompt', 'text', 'input', 'query', 'message'],
33
+ maxDepth: 3,
34
+ });
35
+ return createPromptRecord({
36
+ text: fallbackPrompt,
37
+ confidence: directPrompt !== null ? 'high' : fallbackPrompt !== null ? 'medium' : 'low',
38
+ captureSource: 'hook-notify',
39
+ providerEventName: `claude.${hookToken}`,
40
+ payload: input.payload,
41
+ observedAt: input.observedAt,
42
+ });
43
+ }
44
+
45
+ function fromTelemetry(_input: PromptFromTelemetryInput): StreamSessionPromptRecord | null {
46
+ return null;
47
+ }
48
+
49
+ export const claudePromptExtractor: AgentPromptExtractor = {
50
+ agentType: 'claude',
51
+ fromNotify,
52
+ fromTelemetry,
53
+ };
@@ -0,0 +1,50 @@
1
+ import type { StreamSessionPromptRecord } from '../../stream-protocol.ts';
2
+ import type {
3
+ AgentPromptExtractor,
4
+ PromptFromNotifyInput,
5
+ PromptFromTelemetryInput,
6
+ } from '../agent-prompt-extractor.ts';
7
+ import {
8
+ createPromptRecord,
9
+ findPromptText,
10
+ readTrimmedString,
11
+ } from '../agent-prompt-extractor.ts';
12
+
13
+ function fromNotify(_input: PromptFromNotifyInput): StreamSessionPromptRecord | null {
14
+ return null;
15
+ }
16
+
17
+ function fromTelemetry(input: PromptFromTelemetryInput): StreamSessionPromptRecord | null {
18
+ const normalizedEventName = (input.eventName ?? '').trim().toLowerCase();
19
+ if (normalizedEventName !== 'codex.user_prompt' && normalizedEventName !== 'user_prompt') {
20
+ return null;
21
+ }
22
+ if (input.source !== 'otlp-log' && input.source !== 'history') {
23
+ return null;
24
+ }
25
+ const textFromPayload = findPromptText(input.payload, {
26
+ keys: ['prompt', 'user_prompt', 'userPrompt', 'message', 'text', 'content', 'input', 'body'],
27
+ maxDepth: 4,
28
+ });
29
+ const normalizedSummary = readTrimmedString(input.summary);
30
+ const textFromSummary =
31
+ normalizedSummary !== null && normalizedSummary.toLowerCase().startsWith('prompt:')
32
+ ? readTrimmedString(normalizedSummary.slice('prompt:'.length))
33
+ : null;
34
+ const promptText = textFromPayload ?? textFromSummary;
35
+ const confidence = promptText === null ? 'low' : textFromPayload !== null ? 'high' : 'medium';
36
+ return createPromptRecord({
37
+ text: promptText,
38
+ confidence,
39
+ captureSource: input.source === 'history' ? 'history' : 'otlp-log',
40
+ providerEventName: input.eventName,
41
+ payload: input.payload,
42
+ observedAt: input.observedAt,
43
+ });
44
+ }
45
+
46
+ export const codexPromptExtractor: AgentPromptExtractor = {
47
+ agentType: 'codex',
48
+ fromNotify,
49
+ fromTelemetry,
50
+ };
@@ -0,0 +1,56 @@
1
+ import type { StreamSessionPromptRecord } from '../../stream-protocol.ts';
2
+ import type {
3
+ AgentPromptExtractor,
4
+ PromptFromNotifyInput,
5
+ PromptFromTelemetryInput,
6
+ } from '../agent-prompt-extractor.ts';
7
+ import {
8
+ createPromptRecord,
9
+ findPromptText,
10
+ normalizeEventToken,
11
+ readTrimmedString,
12
+ } from '../agent-prompt-extractor.ts';
13
+
14
+ function fromNotify(input: PromptFromNotifyInput): StreamSessionPromptRecord | null {
15
+ const hookEventName =
16
+ readTrimmedString(input.payload['hook_event_name']) ??
17
+ readTrimmedString(input.payload['hookEventName']) ??
18
+ readTrimmedString(input.payload['event_name']) ??
19
+ readTrimmedString(input.payload['eventName']) ??
20
+ readTrimmedString(input.payload['event']);
21
+ if (hookEventName === null) {
22
+ return null;
23
+ }
24
+ const hookToken = normalizeEventToken(hookEventName);
25
+ if (hookToken !== 'beforesubmitprompt') {
26
+ return null;
27
+ }
28
+ const directPrompt =
29
+ readTrimmedString(input.payload['prompt']) ??
30
+ readTrimmedString(input.payload['user_prompt']) ??
31
+ readTrimmedString(input.payload['userPrompt']);
32
+ const promptText =
33
+ directPrompt ??
34
+ findPromptText(input.payload, {
35
+ keys: ['prompt', 'user_prompt', 'userPrompt', 'text', 'input', 'query', 'message'],
36
+ maxDepth: 3,
37
+ });
38
+ return createPromptRecord({
39
+ text: promptText,
40
+ confidence: directPrompt !== null ? 'high' : promptText !== null ? 'medium' : 'low',
41
+ captureSource: 'hook-notify',
42
+ providerEventName: `cursor.${hookToken}`,
43
+ payload: input.payload,
44
+ observedAt: input.observedAt,
45
+ });
46
+ }
47
+
48
+ function fromTelemetry(_input: PromptFromTelemetryInput): StreamSessionPromptRecord | null {
49
+ return null;
50
+ }
51
+
52
+ export const cursorPromptExtractor: AgentPromptExtractor = {
53
+ agentType: 'cursor',
54
+ fromNotify,
55
+ fromTelemetry,
56
+ };
@@ -0,0 +1,69 @@
1
+ import type { StreamSessionPromptRecord, StreamTelemetrySource } from '../stream-protocol.ts';
2
+ import type { AgentPromptExtractor } from './agent-prompt-extractor.ts';
3
+ import { claudePromptExtractor } from './extractors/claude-prompt-extractor.ts';
4
+ import { codexPromptExtractor } from './extractors/codex-prompt-extractor.ts';
5
+ import { cursorPromptExtractor } from './extractors/cursor-prompt-extractor.ts';
6
+
7
+ type SupportedAgentType = 'codex' | 'claude' | 'cursor';
8
+
9
+ const DEFAULT_EXTRACTORS: Record<SupportedAgentType, AgentPromptExtractor> = {
10
+ codex: codexPromptExtractor,
11
+ claude: claudePromptExtractor,
12
+ cursor: cursorPromptExtractor,
13
+ };
14
+
15
+ function normalizeAgentType(value: string): SupportedAgentType | null {
16
+ const normalized = value.trim().toLowerCase();
17
+ if (normalized === 'codex' || normalized === 'claude' || normalized === 'cursor') {
18
+ return normalized;
19
+ }
20
+ return null;
21
+ }
22
+
23
+ interface PromptFromNotifyInput {
24
+ readonly agentType: string;
25
+ readonly payload: Record<string, unknown>;
26
+ readonly observedAt: string;
27
+ }
28
+
29
+ interface PromptFromTelemetryInput {
30
+ readonly agentType: string;
31
+ readonly source: StreamTelemetrySource;
32
+ readonly eventName: string | null;
33
+ readonly summary: string | null;
34
+ readonly payload: Record<string, unknown>;
35
+ readonly observedAt: string;
36
+ }
37
+
38
+ export class SessionPromptEngine {
39
+ private readonly extractors: Record<SupportedAgentType, AgentPromptExtractor>;
40
+
41
+ constructor(extractors: Record<SupportedAgentType, AgentPromptExtractor> = DEFAULT_EXTRACTORS) {
42
+ this.extractors = extractors;
43
+ }
44
+
45
+ extractFromNotify(input: PromptFromNotifyInput): StreamSessionPromptRecord | null {
46
+ const agentType = normalizeAgentType(input.agentType);
47
+ if (agentType === null) {
48
+ return null;
49
+ }
50
+ return this.extractors[agentType].fromNotify({
51
+ payload: input.payload,
52
+ observedAt: input.observedAt,
53
+ });
54
+ }
55
+
56
+ extractFromTelemetry(input: PromptFromTelemetryInput): StreamSessionPromptRecord | null {
57
+ const agentType = normalizeAgentType(input.agentType);
58
+ if (agentType === null) {
59
+ return null;
60
+ }
61
+ return this.extractors[agentType].fromTelemetry({
62
+ source: input.source,
63
+ eventName: input.eventName,
64
+ summary: input.summary,
65
+ payload: input.payload,
66
+ observedAt: input.observedAt,
67
+ });
68
+ }
69
+ }