pi-harness-delegate 0.1.0 → 0.2.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.
@@ -1,138 +1,155 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
- import type { Harness, BuildArgsOpts, NormalizedPermission, ParseOutcome, ParseState, StreamedResult } from './types.ts';
3
+ import type {
4
+ BuildArgsOpts,
5
+ Harness,
6
+ NormalizedPermission,
7
+ ParseOutcome,
8
+ ParseState,
9
+ StreamedResult,
10
+ } from './types.ts';
4
11
 
5
12
  const execFileAsync = promisify(execFile);
6
13
 
7
14
  function isRecord(v: unknown): v is Record<string, unknown> {
8
- return typeof v === 'object' && v !== null && !Array.isArray(v);
15
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
9
16
  }
10
17
 
11
18
  const PERMISSION_MAP: Record<NormalizedPermission, string> = {
12
- readonly: 'read-only',
13
- edit: 'allow-edit',
14
- danger: 'danger',
19
+ readonly: 'read-only',
20
+ edit: 'allow-edit',
21
+ danger: 'danger',
15
22
  };
16
23
 
17
24
  function extractOpencodeText(o: Record<string, unknown>): string | undefined {
18
- if (typeof o.text === 'string') return o.text;
19
- if (typeof o.output === 'string') return o.output;
20
- if (typeof o.delta === 'string') return o.delta;
21
- if (typeof o.content === 'string') return o.content;
22
- if (isRecord(o.event) && typeof o.event.text === 'string') return o.event.text;
23
- if (isRecord(o.message) && typeof o.message.text === 'string') return o.message.text;
24
- return undefined;
25
+ if (typeof o.text === 'string') return o.text;
26
+ if (typeof o.output === 'string') return o.output;
27
+ if (typeof o.delta === 'string') return o.delta;
28
+ if (typeof o.content === 'string') return o.content;
29
+ if (isRecord(o.event) && typeof o.event.text === 'string') return o.event.text;
30
+ if (isRecord(o.message) && typeof o.message.text === 'string') return o.message.text;
31
+ return undefined;
25
32
  }
26
33
 
27
34
  export function parseOpencodeLine(line: string, state: ParseState): ParseOutcome {
28
- let o: unknown;
29
- try {
30
- o = JSON.parse(line);
31
- } catch {
32
- if (line.trim().length > 0) return { streamedText: line + '\n', activities: [] };
33
- return { activities: [] };
34
- }
35
- if (!isRecord(o)) return { activities: [] };
36
- const activities: ParseOutcome['activities'] = [];
37
- let streamedText: string | undefined;
38
- const typeStr = typeof o.type === 'string' ? o.type : '';
35
+ let o: unknown;
36
+ try {
37
+ o = JSON.parse(line);
38
+ } catch {
39
+ if (line.trim().length > 0) return { streamedText: line + '\n', activities: [] };
40
+ return { activities: [] };
41
+ }
42
+ if (!isRecord(o)) return { activities: [] };
43
+ const activities: ParseOutcome['activities'] = [];
44
+ let streamedText: string | undefined;
45
+ const typeStr = typeof o.type === 'string' ? o.type : '';
39
46
 
40
- if (typeStr.includes('tool') || o.type === 'tool_use' || o.type === 'tool_result') {
41
- const name = typeof o.name === 'string' ? o.name : typeof (o as Record<string, unknown>).tool === 'string' ? (o as Record<string, unknown>).tool as string : 'tool';
42
- if (typeStr.includes('start') || o.type === 'tool_use') {
43
- activities.push({ kind: 'tool_start', name });
44
- if (isRecord(o.input)) activities.push({ kind: 'tool_input', name, input: o.input as Record<string, unknown> });
45
- } else if (typeStr.includes('result') || typeStr.includes('completed') || o.type === 'tool_result') {
46
- activities.push({ kind: 'tool_result', isError: o.is_error === true || o.error === true });
47
- }
48
- }
49
- if (typeStr.includes('thinking') || o.type === 'reasoning') {
50
- activities.push({ kind: 'thinking', chars: 10 });
51
- }
47
+ if (typeStr.includes('tool') || o.type === 'tool_use' || o.type === 'tool_result') {
48
+ const name =
49
+ typeof o.name === 'string'
50
+ ? o.name
51
+ : typeof (o as Record<string, unknown>).tool === 'string'
52
+ ? ((o as Record<string, unknown>).tool as string)
53
+ : 'tool';
54
+ if (typeStr.includes('start') || o.type === 'tool_use') {
55
+ activities.push({ kind: 'tool_start', name });
56
+ if (isRecord(o.input)) activities.push({ kind: 'tool_input', name, input: o.input as Record<string, unknown> });
57
+ } else if (typeStr.includes('result') || typeStr.includes('completed') || o.type === 'tool_result') {
58
+ activities.push({ kind: 'tool_result', isError: o.is_error === true || o.error === true });
59
+ }
60
+ }
61
+ if (typeStr.includes('thinking') || o.type === 'reasoning') {
62
+ activities.push({ kind: 'thinking', chars: 10 });
63
+ }
52
64
 
53
- const text = extractOpencodeText(o);
54
- if (text && !typeStr.includes('tool') && !typeStr.includes('thinking')) streamedText = text;
65
+ const text = extractOpencodeText(o);
66
+ if (text && !typeStr.includes('tool') && !typeStr.includes('thinking')) streamedText = text;
55
67
 
56
- if (o.type === 'result' || o.type === 'completed' || o.type === 'done') {
57
- const usage = isRecord(o.usage) ? o.usage : null;
58
- const result: StreamedResult = {
59
- result: typeof o.result === 'string' ? o.result : typeof o.output === 'string' ? o.output : state.streamedText + (streamedText ?? ''),
60
- isError: o.is_error === true,
61
- numTurns: typeof o.num_turns === 'number' ? o.num_turns : 0,
62
- totalCostUsd: typeof o.total_cost_usd === 'number' ? o.total_cost_usd : 0,
63
- sessionId: typeof o.session_id === 'string' ? o.session_id : typeof o.id === 'string' ? o.id : null,
64
- stopReason: typeof o.stop_reason === 'string' ? o.stop_reason : null,
65
- permissionDenials: [],
66
- durationMs: typeof o.duration_ms === 'number' ? o.duration_ms : null,
67
- durationApiMs: null,
68
- ttftMs: null,
69
- model: typeof o.model === 'string' ? o.model : null,
70
- contextWindow: null,
71
- maxOutputTokens: null,
72
- usage: usage
73
- ? {
74
- inputTokens: typeof usage.input_tokens === 'number' ? usage.input_tokens : 0,
75
- outputTokens: typeof usage.output_tokens === 'number' ? usage.output_tokens : 0,
76
- cacheCreationInputTokens: 0,
77
- cacheReadInputTokens: 0,
78
- }
79
- : null,
80
- };
81
- return { activities, streamedText, result };
82
- }
83
- return { activities, streamedText };
68
+ if (o.type === 'result' || o.type === 'completed' || o.type === 'done') {
69
+ const usage = isRecord(o.usage) ? o.usage : null;
70
+ const result: StreamedResult = {
71
+ result:
72
+ typeof o.result === 'string'
73
+ ? o.result
74
+ : typeof o.output === 'string'
75
+ ? o.output
76
+ : state.streamedText + (streamedText ?? ''),
77
+ isError: o.is_error === true,
78
+ numTurns: typeof o.num_turns === 'number' ? o.num_turns : 0,
79
+ totalCostUsd: typeof o.total_cost_usd === 'number' ? o.total_cost_usd : 0,
80
+ sessionId: typeof o.session_id === 'string' ? o.session_id : typeof o.id === 'string' ? o.id : null,
81
+ stopReason: typeof o.stop_reason === 'string' ? o.stop_reason : null,
82
+ permissionDenials: [],
83
+ durationMs: typeof o.duration_ms === 'number' ? o.duration_ms : null,
84
+ durationApiMs: null,
85
+ ttftMs: null,
86
+ model: typeof o.model === 'string' ? o.model : null,
87
+ contextWindow: null,
88
+ maxOutputTokens: null,
89
+ usage: usage
90
+ ? {
91
+ inputTokens: typeof usage.input_tokens === 'number' ? usage.input_tokens : 0,
92
+ outputTokens: typeof usage.output_tokens === 'number' ? usage.output_tokens : 0,
93
+ cacheCreationInputTokens: 0,
94
+ cacheReadInputTokens: 0,
95
+ }
96
+ : null,
97
+ };
98
+ return { activities, streamedText, result };
99
+ }
100
+ return { activities, streamedText };
84
101
  }
85
102
 
86
103
  export const opencodeHarness: Harness = {
87
- name: 'opencode',
88
- displayName: 'OpenCode',
89
- binary: 'opencode',
90
- async detect() {
91
- try {
92
- const { stdout } = await execFileAsync('opencode', ['--version'], { timeout: 5000 });
93
- return { ok: true, version: stdout.trim() };
94
- } catch {
95
- return { ok: false, hint: 'Install OpenCode: https://opencode.ai' };
96
- }
97
- },
98
- buildArgs(opts: BuildArgsOpts): string[] {
99
- const args = ['run', '--format', 'json', opts.prompt];
100
- // permission not fully standardized; pass as --permission if supported
101
- const perm = opts.nativePermission ?? PERMISSION_MAP[opts.permission] ?? 'allow-edit';
102
- args.push('--permission', perm);
103
- if (opts.model) args.push('--model', opts.model);
104
- if (opts.resumeSessionId) args.push('--session', opts.resumeSessionId);
105
- for (const dir of opts.addDirs ?? []) args.push('--add-dir', dir);
106
- return args;
107
- },
108
- parseLine(line: string, state: ParseState): ParseOutcome {
109
- return parseOpencodeLine(line, state);
110
- },
111
- extractResult(state: ParseState): StreamedResult | null {
112
- if (state.result) return state.result;
113
- if (state.streamedText.trim().length > 0) {
114
- return {
115
- result: state.streamedText,
116
- isError: false,
117
- numTurns: 1,
118
- totalCostUsd: 0,
119
- sessionId: null,
120
- stopReason: null,
121
- permissionDenials: [],
122
- durationMs: null,
123
- durationApiMs: null,
124
- ttftMs: null,
125
- model: null,
126
- contextWindow: null,
127
- maxOutputTokens: null,
128
- usage: null,
129
- };
130
- }
131
- return null;
132
- },
133
- permissionMap: {
134
- readonly: ['read-only'],
135
- edit: ['allow-edit'],
136
- danger: ['danger'],
137
- },
104
+ name: 'opencode',
105
+ displayName: 'OpenCode',
106
+ binary: 'opencode',
107
+ async detect() {
108
+ try {
109
+ const { stdout } = await execFileAsync('opencode', ['--version'], { timeout: 5000 });
110
+ return { ok: true, version: stdout.trim() };
111
+ } catch {
112
+ return { ok: false, hint: 'Install OpenCode: https://opencode.ai' };
113
+ }
114
+ },
115
+ buildArgs(opts: BuildArgsOpts): string[] {
116
+ const args = ['run', '--format', 'json', opts.prompt];
117
+ // permission not fully standardized; pass as --permission if supported
118
+ const perm = opts.nativePermission ?? PERMISSION_MAP[opts.permission] ?? 'allow-edit';
119
+ args.push('--permission', perm);
120
+ if (opts.model) args.push('--model', opts.model);
121
+ if (opts.resumeSessionId) args.push('--session', opts.resumeSessionId);
122
+ for (const dir of opts.addDirs ?? []) args.push('--add-dir', dir);
123
+ return args;
124
+ },
125
+ parseLine(line: string, state: ParseState): ParseOutcome {
126
+ return parseOpencodeLine(line, state);
127
+ },
128
+ extractResult(state: ParseState): StreamedResult | null {
129
+ if (state.result) return state.result;
130
+ if (state.streamedText.trim().length > 0) {
131
+ return {
132
+ result: state.streamedText,
133
+ isError: false,
134
+ numTurns: 1,
135
+ totalCostUsd: 0,
136
+ sessionId: null,
137
+ stopReason: null,
138
+ permissionDenials: [],
139
+ durationMs: null,
140
+ durationApiMs: null,
141
+ ttftMs: null,
142
+ model: null,
143
+ contextWindow: null,
144
+ maxOutputTokens: null,
145
+ usage: null,
146
+ };
147
+ }
148
+ return null;
149
+ },
150
+ permissionMap: {
151
+ readonly: ['read-only'],
152
+ edit: ['allow-edit'],
153
+ danger: ['danger'],
154
+ },
138
155
  };
@@ -1,51 +1,51 @@
1
+ import { ampHarness } from './amp.ts';
1
2
  import { claudeHarness } from './claude.ts';
2
3
  import { codexHarness } from './codex.ts';
3
4
  import { opencodeHarness } from './opencode.ts';
4
- import { ampHarness } from './amp.ts';
5
5
  import type { Harness } from './types.ts';
6
6
 
7
7
  export const HARNESSES: Record<string, Harness> = {
8
- claude: claudeHarness,
9
- codex: codexHarness,
10
- opencode: opencodeHarness,
11
- amp: ampHarness,
8
+ claude: claudeHarness,
9
+ codex: codexHarness,
10
+ opencode: opencodeHarness,
11
+ amp: ampHarness,
12
12
  };
13
13
 
14
14
  export const ALIASES: Record<string, string> = {
15
- omp: 'amp',
15
+ omp: 'amp',
16
16
  };
17
17
 
18
18
  export const HARNESS_NAMES = Object.keys(HARNESSES);
19
19
 
20
20
  export function resolveHarnessName(name: string): string {
21
- const lower = name.toLowerCase();
22
- if (HARNESSES[lower]) return lower;
23
- if (ALIASES[lower] && HARNESSES[ALIASES[lower]]) return ALIASES[lower];
24
- return lower;
21
+ const lower = name.toLowerCase();
22
+ if (HARNESSES[lower]) return lower;
23
+ if (ALIASES[lower] && HARNESSES[ALIASES[lower]]) return ALIASES[lower];
24
+ return lower;
25
25
  }
26
26
 
27
27
  export function getHarness(name: string): Harness | undefined {
28
- const resolved = resolveHarnessName(name);
29
- return HARNESSES[resolved];
28
+ const resolved = resolveHarnessName(name);
29
+ return HARNESSES[resolved];
30
30
  }
31
31
 
32
32
  export function getAllHarnesses(): Harness[] {
33
- return Object.values(HARNESSES);
33
+ return Object.values(HARNESSES);
34
34
  }
35
35
 
36
36
  export async function detectAll(): Promise<Record<string, { ok: boolean; version?: string; hint?: string }>> {
37
- const out: Record<string, { ok: boolean; version?: string; hint?: string }> = {};
38
- await Promise.all(
39
- Object.entries(HARNESSES).map(async ([name, h]) => {
40
- out[name] = await h.detect();
41
- }),
42
- );
43
- return out;
37
+ const out: Record<string, { ok: boolean; version?: string; hint?: string }> = {};
38
+ await Promise.all(
39
+ Object.entries(HARNESSES).map(async ([name, h]) => {
40
+ out[name] = await h.detect();
41
+ }),
42
+ );
43
+ return out;
44
44
  }
45
45
 
46
46
  export function isKnownHarness(name: string): boolean {
47
- const r = resolveHarnessName(name);
48
- return r in HARNESSES;
47
+ const r = resolveHarnessName(name);
48
+ return r in HARNESSES;
49
49
  }
50
50
 
51
51
  export const normalizeHarnessName = resolveHarnessName;
@@ -3,91 +3,91 @@
3
3
  export type NormalizedPermission = 'readonly' | 'edit' | 'danger';
4
4
 
5
5
  export interface StreamedUsage {
6
- inputTokens: number;
7
- outputTokens: number;
8
- cacheCreationInputTokens: number;
9
- cacheReadInputTokens: number;
6
+ inputTokens: number;
7
+ outputTokens: number;
8
+ cacheCreationInputTokens: number;
9
+ cacheReadInputTokens: number;
10
10
  }
11
11
 
12
12
  export interface StreamedResult {
13
- result: string;
14
- isError: boolean;
15
- numTurns: number;
16
- totalCostUsd: number;
17
- sessionId: string | null;
18
- stopReason: string | null;
19
- permissionDenials: unknown[];
20
- usage: StreamedUsage | null;
21
- durationMs: number | null;
22
- durationApiMs: number | null;
23
- ttftMs: number | null;
24
- model: string | null;
25
- contextWindow: number | null;
26
- maxOutputTokens: number | null;
13
+ result: string;
14
+ isError: boolean;
15
+ numTurns: number;
16
+ totalCostUsd: number;
17
+ sessionId: string | null;
18
+ stopReason: string | null;
19
+ permissionDenials: unknown[];
20
+ usage: StreamedUsage | null;
21
+ durationMs: number | null;
22
+ durationApiMs: number | null;
23
+ ttftMs: number | null;
24
+ model: string | null;
25
+ contextWindow: number | null;
26
+ maxOutputTokens: number | null;
27
27
  }
28
28
 
29
29
  export type ActivityEvent =
30
- | { kind: 'tool_start'; name: string }
31
- | { kind: 'tool_input'; name: string; input: Record<string, unknown> }
32
- | { kind: 'tool_result'; isError: boolean }
33
- | { kind: 'thinking'; chars: number };
30
+ | { kind: 'tool_start'; name: string }
31
+ | { kind: 'tool_input'; name: string; input: Record<string, unknown> }
32
+ | { kind: 'tool_result'; isError: boolean }
33
+ | { kind: 'thinking'; chars: number };
34
34
 
35
35
  export interface ParseState {
36
- streamedText: string;
37
- activities: ActivityEvent[];
38
- result: StreamedResult | null;
39
- /** Harness-internal scratch space. */
40
- _harness?: Record<string, unknown>;
36
+ streamedText: string;
37
+ activities: ActivityEvent[];
38
+ result: StreamedResult | null;
39
+ /** Harness-internal scratch space. */
40
+ _harness?: Record<string, unknown>;
41
41
  }
42
42
 
43
43
  export interface ParseOutcome {
44
- streamedText?: string;
45
- activities?: ActivityEvent[];
46
- result?: StreamedResult | null;
44
+ streamedText?: string;
45
+ activities?: ActivityEvent[];
46
+ result?: StreamedResult | null;
47
47
  }
48
48
 
49
49
  export interface StreamParseOutcome {
50
- streamedText: string;
51
- result: StreamedResult | null;
52
- activities: ActivityEvent[];
50
+ streamedText: string;
51
+ result: StreamedResult | null;
52
+ activities: ActivityEvent[];
53
53
  }
54
54
 
55
55
  export interface BuildArgsOpts {
56
- prompt: string;
57
- cwd: string;
58
- permission: NormalizedPermission;
59
- nativePermission?: string;
60
- model?: string;
61
- maxBudgetUsd?: number;
62
- addDirs?: string[];
63
- resumeSessionId?: string;
64
- resumeId?: string;
56
+ prompt: string;
57
+ cwd: string;
58
+ permission: NormalizedPermission;
59
+ nativePermission?: string;
60
+ model?: string;
61
+ maxBudgetUsd?: number;
62
+ addDirs?: string[];
63
+ resumeSessionId?: string;
64
+ resumeId?: string;
65
65
  }
66
66
 
67
67
  export type HarnessBuildOpts = BuildArgsOpts;
68
68
 
69
69
  export interface DetectResult {
70
- ok: boolean;
71
- version?: string;
72
- hint?: string;
70
+ ok: boolean;
71
+ version?: string;
72
+ hint?: string;
73
73
  }
74
74
 
75
75
  export interface Harness {
76
- name: string;
77
- displayName: string;
78
- binary: string;
79
- aliases?: string[];
80
- /** Check if binary is available. */
81
- detect(): Promise<DetectResult>;
82
- /** Build CLI args (excluding binary). */
83
- buildArgs(opts: BuildArgsOpts): string[];
84
- /** Parse a single stdout line. State is mutated by runner; return deltas. */
85
- parseLine(line: string, state: ParseState): ParseOutcome;
86
- /** Extract final result after process exit (state.result may already be set). */
87
- extractResult(state: ParseState): StreamedResult | null;
88
- /** Normalized -> native arg fragments. */
89
- permissionMap?: Record<NormalizedPermission, string[]>;
90
- permissionHint?: (permission: NormalizedPermission) => string[];
76
+ name: string;
77
+ displayName: string;
78
+ binary: string;
79
+ aliases?: string[];
80
+ /** Check if binary is available. */
81
+ detect(): Promise<DetectResult>;
82
+ /** Build CLI args (excluding binary). */
83
+ buildArgs(opts: BuildArgsOpts): string[];
84
+ /** Parse a single stdout line. State is mutated by runner; return deltas. */
85
+ parseLine(line: string, state: ParseState): ParseOutcome;
86
+ /** Extract final result after process exit (state.result may already be set). */
87
+ extractResult(state: ParseState): StreamedResult | null;
88
+ /** Normalized -> native arg fragments. */
89
+ permissionMap?: Record<NormalizedPermission, string[]>;
90
+ permissionHint?: (permission: NormalizedPermission) => string[];
91
91
  }
92
92
 
93
93
  export const DEFAULT_TIMEOUT_MS = 600_000;
@@ -13,45 +13,48 @@
13
13
  */
14
14
 
15
15
  export interface HintConfig {
16
- /** Master switch — false = no hinting at all. */
17
- autoDelegateHints: boolean;
16
+ /** Master switch — false = no hinting at all. */
17
+ autoDelegateHints: boolean;
18
18
  }
19
19
 
20
20
  const EXPLICIT_MARKER_RE =
21
- /(?:^|\s)@(?:claude|codex|opencode|amp)\b|(?:\b(with|via|using)\s+(?:claude|codex|opencode|amp|delegate)\b)|(?:\bdelegate\b[\s\S]*\b(?:claude|codex|opencode|amp|delegate)\b)/i;
21
+ /(?:^|\s)@(?:claude|codex|opencode|amp)\b|(?:\b(with|via|using)\s+(?:claude|codex|opencode|amp|delegate)\b)|(?:\bdelegate\b[\s\S]*\b(?:claude|codex|opencode|amp|delegate)\b)/i;
22
22
 
23
23
  const KEYWORD_RE = /^\s*(review|plan|audit|security\s*audit|document|implement|write\s+tests?)\b/i;
24
24
 
25
25
  const HINT_TEXT =
26
- '[delegate] The user wants this delegated to a harness. ' +
27
- 'Call the delegate tool with a fitting harness (claude, codex, opencode, amp) and mode (review, plan, security-audit, docs, implement, …) ' +
28
- 'rather than doing the work yourself. Only skip it if delegation is clearly inappropriate.';
26
+ '[delegate] The user wants this delegated to a harness. ' +
27
+ 'Call the delegate tool with a fitting harness (claude, codex, opencode, amp) and mode (review, plan, security-audit, docs, implement, …) ' +
28
+ 'rather than doing the work yourself. Only skip it if delegation is clearly inappropriate.';
29
29
 
30
30
  const LEGACY_HINT_TEXT =
31
- '[claude-delegate] The user wants this delegated to Claude Code. ' +
32
- 'Call the claude_delegate tool with a fitting mode (review, plan, security-audit, docs, implement, …) ' +
33
- 'rather than doing the work yourself. Only skip it if delegation is clearly inappropriate.';
31
+ '[claude-delegate] The user wants this delegated to Claude Code. ' +
32
+ 'Call the claude_delegate tool with a fitting mode (review, plan, security-audit, docs, implement, …) ' +
33
+ 'rather than doing the work yourself. Only skip it if delegation is clearly inappropriate.';
34
34
 
35
35
  export function delegationHint(text: string, cfg: HintConfig): string | null {
36
- if (!cfg.autoDelegateHints) return null;
36
+ if (!cfg.autoDelegateHints) return null;
37
37
 
38
- // already explicit about the tool or command — nothing to add
39
- if (/\bclaude_delegate\b|\/(?:claude|delegate|codex|opencode|amp)\b/.test(text)) return null;
38
+ // already explicit about the tool or command — nothing to add
39
+ if (/\bclaude_delegate\b|\/(?:claude|delegate|codex|opencode|amp)\b/.test(text)) return null;
40
40
 
41
- if (EXPLICIT_MARKER_RE.test(text)) return HINT_TEXT;
41
+ if (EXPLICIT_MARKER_RE.test(text)) return HINT_TEXT;
42
42
 
43
- if (KEYWORD_RE.test(text)) return HINT_TEXT;
43
+ if (KEYWORD_RE.test(text)) return HINT_TEXT;
44
44
 
45
- return null;
45
+ return null;
46
46
  }
47
47
 
48
48
  export function claudeDelegationHint(text: string, cfg: HintConfig): string | null {
49
- const h = delegationHint(text, cfg);
50
- if (h === HINT_TEXT) return LEGACY_HINT_TEXT;
51
- return h;
49
+ const h = delegationHint(text, cfg);
50
+ if (h === HINT_TEXT) return LEGACY_HINT_TEXT;
51
+ return h;
52
52
  }
53
53
 
54
54
  /** Remove the @harness prefix marker from the text before sending. */
55
55
  export function stripMarker(text: string): string {
56
- return text.replace(/(?:^|\s)@(?:claude|codex|opencode|amp|delegate)\b/g, ' ').replace(/\s{2,}/g, ' ').trim();
56
+ return text
57
+ .replace(/(?:^|\s)@(?:claude|codex|opencode|amp|delegate)\b/g, ' ')
58
+ .replace(/\s{2,}/g, ' ')
59
+ .trim();
57
60
  }