@dungle-scrubs/harness-cli-normalizer 0.3.1 → 0.4.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/README.md +64 -86
- package/dist/cli/args.d.ts +11 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +23 -5
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config.d.ts +25 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +139 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/inspect.d.ts.map +1 -1
- package/dist/cli/inspect.js +35 -1
- package/dist/cli/inspect.js.map +1 -1
- package/dist/cli/render.d.ts.map +1 -1
- package/dist/cli/render.js +10 -1
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +134 -4
- package/dist/cli/run.js.map +1 -1
- package/dist/execution/failure.d.ts +21 -4
- package/dist/execution/failure.d.ts.map +1 -1
- package/dist/execution/failure.js +26 -3
- package/dist/execution/failure.js.map +1 -1
- package/dist/execution/stream-turn.d.ts +4 -0
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +28 -4
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/interpretation/argv.d.ts +1 -0
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +13 -19
- package/dist/interpretation/argv.js.map +1 -1
- package/dist/interpretation/hints.d.ts +22 -0
- package/dist/interpretation/hints.d.ts.map +1 -0
- package/dist/interpretation/hints.js +59 -0
- package/dist/interpretation/hints.js.map +1 -0
- package/dist/interpretation/refusal.d.ts +25 -4
- package/dist/interpretation/refusal.d.ts.map +1 -1
- package/dist/interpretation/refusal.js +16 -0
- package/dist/interpretation/refusal.js.map +1 -1
- package/dist/interpretation/resolve-options.d.ts +42 -0
- package/dist/interpretation/resolve-options.d.ts.map +1 -0
- package/dist/interpretation/resolve-options.js +140 -0
- package/dist/interpretation/resolve-options.js.map +1 -0
- package/dist/interpretation/support.d.ts +32 -0
- package/dist/interpretation/support.d.ts.map +1 -0
- package/dist/interpretation/support.js +107 -0
- package/dist/interpretation/support.js.map +1 -0
- package/dist/interpretation/tool-selection.d.ts +29 -0
- package/dist/interpretation/tool-selection.d.ts.map +1 -0
- package/dist/interpretation/tool-selection.js +122 -0
- package/dist/interpretation/tool-selection.js.map +1 -0
- package/dist/interpretation/turn-options.d.ts.map +1 -1
- package/dist/interpretation/turn-options.js +4 -0
- package/dist/interpretation/turn-options.js.map +1 -1
- package/dist/knowledge/claude-code.d.ts.map +1 -1
- package/dist/knowledge/claude-code.js +34 -1
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +19 -0
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +32 -0
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/muse.d.ts.map +1 -1
- package/dist/knowledge/muse.js +19 -0
- package/dist/knowledge/muse.js.map +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +21 -0
- package/dist/knowledge/pi.js.map +1 -1
- package/dist/knowledge/profile.d.ts +13 -0
- package/dist/knowledge/profile.d.ts.map +1 -0
- package/dist/knowledge/profile.js +31 -0
- package/dist/knowledge/profile.js.map +1 -0
- package/package.json +2 -23
- package/src/cli/args.ts +31 -1
- package/src/cli/config.ts +166 -0
- package/src/cli/inspect.ts +34 -1
- package/src/cli/render.ts +10 -1
- package/src/cli/run.ts +161 -5
- package/src/execution/failure.ts +44 -5
- package/src/execution/stream-turn.ts +33 -3
- package/src/interpretation/argv.ts +14 -19
- package/src/interpretation/hints.ts +99 -0
- package/src/interpretation/refusal.ts +32 -3
- package/src/interpretation/resolve-options.ts +190 -0
- package/src/interpretation/support.ts +119 -0
- package/src/interpretation/tool-selection.ts +161 -0
- package/src/interpretation/turn-options.ts +4 -0
- package/src/knowledge/claude-code.ts +34 -1
- package/src/knowledge/codex.ts +19 -0
- package/src/knowledge/descriptor.ts +32 -0
- package/src/knowledge/muse.ts +19 -0
- package/src/knowledge/pi.ts +21 -0
- package/src/knowledge/profile.ts +33 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -8
- package/dist/index.js.map +0 -1
- package/src/index.ts +0 -7
package/src/cli/run.ts
CHANGED
|
@@ -3,6 +3,13 @@ import { nodeRunnerDeps } from "../execution/node-deps.js";
|
|
|
3
3
|
import { KILL_GRACE_MS, redactArgv, streamTurn } from "../execution/stream-turn.js";
|
|
4
4
|
import { buildLaunchArgv, buildResumeArgv } from "../interpretation/argv.js";
|
|
5
5
|
import { ArgvRefusalError } from "../interpretation/refusal.js";
|
|
6
|
+
import {
|
|
7
|
+
FloorExceededError,
|
|
8
|
+
type ProvenanceEntry,
|
|
9
|
+
resolveEffectiveOptions,
|
|
10
|
+
} from "../interpretation/resolve-options.js";
|
|
11
|
+
import { recognizeNativeSpelling, supportedBy } from "../interpretation/support.js";
|
|
12
|
+
import { defaultDescriptors } from "../knowledge/overrides.js";
|
|
6
13
|
import { parseRunExtra, parseTurnOptions, resolvePromptAsync } from "./args.js";
|
|
7
14
|
import { createRenderState, renderEvent, writeEventNdjson } from "./render.js";
|
|
8
15
|
import { resolveHarness } from "./resolve-harness.js";
|
|
@@ -16,7 +23,10 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
16
23
|
return;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
|
-
const { parseCommonFlags, detectPositionalPromptInjection } = await import(
|
|
26
|
+
const { parseCommonFlags, detectPositionalPromptInjection, splitPassthrough } = await import(
|
|
27
|
+
"./args.js"
|
|
28
|
+
);
|
|
29
|
+
const { passthrough } = splitPassthrough(rawArgs);
|
|
20
30
|
const injection = detectPositionalPromptInjection(rawArgs);
|
|
21
31
|
if (injection) {
|
|
22
32
|
const err = new ArgvRefusalError({
|
|
@@ -35,7 +45,52 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
35
45
|
parsed = parseCommonFlags(rawArgs);
|
|
36
46
|
} catch (err) {
|
|
37
47
|
const message = err instanceof Error ? err.message : String(err);
|
|
38
|
-
|
|
48
|
+
// D7 part B: a native spelling passed before the separator gets
|
|
49
|
+
// recognized and redirected to the normalized flag instead of a
|
|
50
|
+
// generic unknown-flag error.
|
|
51
|
+
// parseArgs reports unknown long flags as "Unknown option '--x'" but
|
|
52
|
+
// splits bundled short flags ("-nt" -> "Unknown option 'n'"). Match the
|
|
53
|
+
// reported token back against the ORIGINAL argv: a short-flag bundle
|
|
54
|
+
// that some descriptor spells exactly (pi's -nt) is recognizable; a
|
|
55
|
+
// lone unknown token keeps the plain error.
|
|
56
|
+
const flagMatch = message.match(/Unknown option '([A-Za-z0-9_-]+)'/);
|
|
57
|
+
let rawFlag: string | undefined;
|
|
58
|
+
if (flagMatch?.[1] !== undefined) {
|
|
59
|
+
const reported = flagMatch[1].startsWith("-") ? flagMatch[1] : `-${flagMatch[1]}`;
|
|
60
|
+
// Exact long flag: use it. Reported short flag (e.g. -n): the caller
|
|
61
|
+
// may have typed a BUNDLE (-nt) that parseArgs split - find the argv
|
|
62
|
+
// token that starts with the reported short and is longer; recognition
|
|
63
|
+
// then decides whether the whole bundle is a descriptor spelling.
|
|
64
|
+
const fromArgv =
|
|
65
|
+
rawArgs.find((a) => a === reported) ??
|
|
66
|
+
(reported.length === 2
|
|
67
|
+
? rawArgs.find((a) => a.length > 2 && a.startsWith(reported))
|
|
68
|
+
: undefined);
|
|
69
|
+
rawFlag = fromArgv ?? reported;
|
|
70
|
+
}
|
|
71
|
+
const native =
|
|
72
|
+
rawFlag !== undefined ? recognizeNativeSpelling(defaultDescriptors(), rawFlag) : null;
|
|
73
|
+
if (native !== null) {
|
|
74
|
+
const by = native.option.startsWith("discovery.")
|
|
75
|
+
? native.entries
|
|
76
|
+
: supportedBy(defaultDescriptors(), native.option);
|
|
77
|
+
const normalizedSpelling =
|
|
78
|
+
native.option === "excludeTools"
|
|
79
|
+
? "--exclude-tools"
|
|
80
|
+
: native.option.startsWith("discovery.")
|
|
81
|
+
? `--no-${native.option.split(".")[1] === "instructionFiles" ? "instruction-files" : native.option.split(".")[1]}`
|
|
82
|
+
: `--${native.option}`;
|
|
83
|
+
process.stderr.write(
|
|
84
|
+
`unknown flag: ${rawFlag} is a native spelling (used by ${native.entries.map((e) => e.harness).join(", ")}) - use the normalized ${normalizedSpelling} flag instead\n`,
|
|
85
|
+
);
|
|
86
|
+
if (by.length > 0) {
|
|
87
|
+
process.stderr.write(
|
|
88
|
+
`supported on: ${by.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
process.stderr.write(`unknown flag: ${message}\n`);
|
|
93
|
+
}
|
|
39
94
|
process.stderr.write(`Run 'hcn run --help' for usage.\n`);
|
|
40
95
|
process.exitCode = 2;
|
|
41
96
|
return;
|
|
@@ -50,6 +105,11 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
50
105
|
process.exitCode = 2;
|
|
51
106
|
return;
|
|
52
107
|
}
|
|
108
|
+
if (passthrough.length === 0 && rawArgs.includes("--")) {
|
|
109
|
+
process.stderr.write(`-- separator given but no passthrough tokens followed it\n`);
|
|
110
|
+
process.exitCode = 2;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
53
113
|
|
|
54
114
|
const wantJson = values.json === true;
|
|
55
115
|
|
|
@@ -67,6 +127,12 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
67
127
|
} catch (err) {
|
|
68
128
|
if (err instanceof ArgvRefusalError) {
|
|
69
129
|
process.stderr.write(`${err.message}\n`);
|
|
130
|
+
if (err.hint) process.stderr.write(`hint: ${err.hint}\n`);
|
|
131
|
+
if (err.supportedBy?.length) {
|
|
132
|
+
process.stderr.write(
|
|
133
|
+
`supported on: ${err.supportedBy.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
70
136
|
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
71
137
|
process.exitCode = 2;
|
|
72
138
|
return;
|
|
@@ -86,6 +152,12 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
86
152
|
} catch (err) {
|
|
87
153
|
if (err instanceof ArgvRefusalError) {
|
|
88
154
|
process.stderr.write(`${err.message}\n`);
|
|
155
|
+
if (err.hint) process.stderr.write(`hint: ${err.hint}\n`);
|
|
156
|
+
if (err.supportedBy?.length) {
|
|
157
|
+
process.stderr.write(
|
|
158
|
+
`supported on: ${err.supportedBy.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
89
161
|
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
90
162
|
process.exitCode = 2;
|
|
91
163
|
return;
|
|
@@ -99,6 +171,12 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
99
171
|
} catch (err) {
|
|
100
172
|
if (err instanceof ArgvRefusalError) {
|
|
101
173
|
process.stderr.write(`${err.message}\n`);
|
|
174
|
+
if (err.hint) process.stderr.write(`hint: ${err.hint}\n`);
|
|
175
|
+
if (err.supportedBy?.length) {
|
|
176
|
+
process.stderr.write(
|
|
177
|
+
`supported on: ${err.supportedBy.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
102
180
|
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
103
181
|
process.exitCode = 2;
|
|
104
182
|
return;
|
|
@@ -107,20 +185,84 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
107
185
|
}
|
|
108
186
|
|
|
109
187
|
const isExplicit = promptSource !== "positional";
|
|
188
|
+
|
|
189
|
+
// Defaults profile + user config: LAUNCH-ONLY. A resumed session keeps
|
|
190
|
+
// its own settings; the resolver never runs on resume paths.
|
|
191
|
+
let resolvedProvenance: readonly ProvenanceEntry[] = [];
|
|
192
|
+
let resolvedUnrenderable: readonly string[] = [];
|
|
193
|
+
let effectiveTurnOpts: ReturnType<typeof parseTurnOptions> = turnOpts;
|
|
194
|
+
if (extra.resume === undefined) {
|
|
195
|
+
const tiers: {
|
|
196
|
+
user?: Partial<ReturnType<typeof parseTurnOptions>>;
|
|
197
|
+
project?: Partial<ReturnType<typeof parseTurnOptions>>;
|
|
198
|
+
} = {};
|
|
199
|
+
const { loadUserConfig, loadProjectConfig, ConfigError } = await import("./config.js");
|
|
200
|
+
try {
|
|
201
|
+
const loaded = loadUserConfig();
|
|
202
|
+
if (loaded !== null) tiers.user = loaded.config;
|
|
203
|
+
const proj = loadProjectConfig();
|
|
204
|
+
if (proj !== null) tiers.project = proj.config;
|
|
205
|
+
} catch (configErr) {
|
|
206
|
+
if (configErr instanceof ConfigError) {
|
|
207
|
+
process.stderr.write(`config error: ${(configErr as Error).message}\n`);
|
|
208
|
+
process.exitCode = 2;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
throw configErr;
|
|
212
|
+
}
|
|
213
|
+
let resolved: ReturnType<typeof resolveEffectiveOptions>;
|
|
214
|
+
try {
|
|
215
|
+
resolved = resolveEffectiveOptions(h, { ...turnOpts, prompt } as never, tiers);
|
|
216
|
+
} catch (resErr) {
|
|
217
|
+
if (resErr instanceof FloorExceededError) {
|
|
218
|
+
process.stderr.write(`${(resErr as Error).message}\n`);
|
|
219
|
+
process.exitCode = 2;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
throw resErr;
|
|
223
|
+
}
|
|
224
|
+
const { provenance, unrenderable } = resolved;
|
|
225
|
+
resolvedProvenance = provenance;
|
|
226
|
+
resolvedUnrenderable = unrenderable;
|
|
227
|
+
const { prompt: _p, ...rest } = resolved.options as { prompt: string };
|
|
228
|
+
effectiveTurnOpts = rest as ReturnType<typeof parseTurnOptions>;
|
|
229
|
+
// Provenance is diagnostic data like the spawn line - stderr in BOTH
|
|
230
|
+
// render modes, never stdout (stdout carries the NDJSON contract).
|
|
231
|
+
if (provenance.length > 0 || unrenderable.length > 0) {
|
|
232
|
+
for (const entry of provenance) {
|
|
233
|
+
process.stderr.write(
|
|
234
|
+
`provenance: ${entry.key} = ${JSON.stringify(entry.value)} (${entry.tier})\n`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
for (const key of unrenderable) {
|
|
238
|
+
process.stderr.write(
|
|
239
|
+
`divergence: profile ${JSON.stringify(key)} not expressible on ${h.name}; harness default applies\n`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
110
245
|
const fullOpts = {
|
|
111
|
-
...
|
|
246
|
+
...effectiveTurnOpts,
|
|
112
247
|
prompt,
|
|
113
248
|
cwd: extra.cwd,
|
|
114
249
|
env: extra.env,
|
|
115
250
|
resume: extra.resume,
|
|
251
|
+
...(passthrough.length > 0 ? { passthrough } : {}),
|
|
116
252
|
...(isExplicit ? { __explicitPrompt: true as const } : {}),
|
|
117
|
-
} as Parameters<typeof streamTurn>[1] & {
|
|
253
|
+
} as Parameters<typeof streamTurn>[1] & {
|
|
254
|
+
resume?: string;
|
|
255
|
+
__explicitPrompt?: boolean;
|
|
256
|
+
passthrough?: readonly string[];
|
|
257
|
+
};
|
|
118
258
|
|
|
119
259
|
// Pre-validate via building argv to catch refusals before spawn (so we don't spawn on bad args)
|
|
120
260
|
let _validated = false;
|
|
121
261
|
let preArgv: string[] | null = null;
|
|
122
262
|
try {
|
|
123
263
|
if (fullOpts.resume) {
|
|
264
|
+
// Resume never carries profile/config resolution (launch-only rule),
|
|
265
|
+
// so it builds from the raw turn options.
|
|
124
266
|
preArgv = buildResumeArgv(h, {
|
|
125
267
|
...(turnOpts as object),
|
|
126
268
|
prompt,
|
|
@@ -128,8 +270,10 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
128
270
|
__explicitPrompt: isExplicit,
|
|
129
271
|
} as never);
|
|
130
272
|
} else {
|
|
273
|
+
// Launch builds from the RESOLVED options so the spawn line and the
|
|
274
|
+
// real argv agree.
|
|
131
275
|
preArgv = buildLaunchArgv(h, {
|
|
132
|
-
...(
|
|
276
|
+
...(effectiveTurnOpts as object),
|
|
133
277
|
prompt,
|
|
134
278
|
__explicitPrompt: isExplicit,
|
|
135
279
|
} as never);
|
|
@@ -138,6 +282,12 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
138
282
|
} catch (err) {
|
|
139
283
|
if (err instanceof ArgvRefusalError) {
|
|
140
284
|
process.stderr.write(`${err.message}\n`);
|
|
285
|
+
if (err.hint) process.stderr.write(`hint: ${err.hint}\n`);
|
|
286
|
+
if (err.supportedBy?.length) {
|
|
287
|
+
process.stderr.write(
|
|
288
|
+
`supported on: ${err.supportedBy.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
141
291
|
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
142
292
|
process.exitCode = 2;
|
|
143
293
|
return;
|
|
@@ -234,6 +384,12 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
234
384
|
} catch (err) {
|
|
235
385
|
if (err instanceof ArgvRefusalError) {
|
|
236
386
|
process.stderr.write(`${err.message}\n`);
|
|
387
|
+
if (err.hint) process.stderr.write(`hint: ${err.hint}\n`);
|
|
388
|
+
if (err.supportedBy?.length) {
|
|
389
|
+
process.stderr.write(
|
|
390
|
+
`supported on: ${err.supportedBy.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
237
393
|
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
238
394
|
process.exitCode = 2;
|
|
239
395
|
process.off("SIGINT", onSig);
|
package/src/execution/failure.ts
CHANGED
|
@@ -30,6 +30,7 @@ export const FAILURE_CLASSES = Object.freeze([
|
|
|
30
30
|
"task",
|
|
31
31
|
"transport",
|
|
32
32
|
"rejected",
|
|
33
|
+
"native",
|
|
33
34
|
] as const);
|
|
34
35
|
export type FailureClass = (typeof FAILURE_CLASSES)[number];
|
|
35
36
|
|
|
@@ -41,13 +42,21 @@ export interface FailureSummary {
|
|
|
41
42
|
readonly authKind?: AuthFailureKind;
|
|
42
43
|
readonly resetsAt?: number;
|
|
43
44
|
readonly issue?: RefusalIssue;
|
|
44
|
-
readonly option?:
|
|
45
|
+
readonly option?: import("../interpretation/refusal.js").RefusalOption;
|
|
45
46
|
readonly facet?: DiscoveryFacet;
|
|
46
47
|
readonly supported?: readonly string[];
|
|
48
|
+
/** D7: cross-harness support entries derived from descriptors. */
|
|
49
|
+
readonly supportedBy?: ReadonlyArray<{ harness: string; spelling: string }>;
|
|
50
|
+
/** D8: nearest-alternative hint for the current harness. */
|
|
51
|
+
readonly hint?: string;
|
|
52
|
+
/** D6: the harness process's own exit code for a native failure - data,
|
|
53
|
+
* because native conventions differ from hcn's (codex usage errors exit
|
|
54
|
+
* 2, which hcn reserves for refusals). */
|
|
55
|
+
readonly nativeExitCode?: number;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
export const retryableOf = (cls: FailureClass): boolean =>
|
|
50
|
-
cls !== "task" && cls !== "budget" && cls !== "rejected";
|
|
59
|
+
cls !== "task" && cls !== "budget" && cls !== "rejected" && cls !== "native";
|
|
51
60
|
|
|
52
61
|
const messageFor = (cls: FailureClass, detail?: string): string => {
|
|
53
62
|
switch (cls) {
|
|
@@ -67,10 +76,30 @@ const messageFor = (cls: FailureClass, detail?: string): string => {
|
|
|
67
76
|
return `Transport failure${detail ? ` (${detail})` : ""} - retry or route to another provider`;
|
|
68
77
|
case "rejected":
|
|
69
78
|
return `Request rejected${detail ? ` (${detail})` : ""} - change options or harness`;
|
|
79
|
+
case "native":
|
|
80
|
+
// D6: labeled NATIVE so it can never be confused with an hcn error.
|
|
81
|
+
// The harness's own message follows verbatim; the process exit code
|
|
82
|
+
// rides as data (nativeExitCode), because harness conventions differ
|
|
83
|
+
// (codex exits 2 on usage errors - the same code hcn uses for
|
|
84
|
+
// refusals, so hcn owns its own exit code and reports the native one).
|
|
85
|
+
return `NATIVE ERROR from harness${detail ? `: ${detail}` : ""} - the harness rejected or failed on its own arguments; this is not an hcn error`;
|
|
70
86
|
default:
|
|
71
87
|
return `Failure${detail ? ` (${detail})` : ""}`;
|
|
72
88
|
}
|
|
73
|
-
};
|
|
89
|
+
}; /** D6: a failure that belongs to the harness, not hcn. Carries the
|
|
90
|
+
* native stderr verbatim and the native exit code as data. */
|
|
91
|
+
export const failureFromNative = (
|
|
92
|
+
nativeExitCode: number | null,
|
|
93
|
+
stderrTail: readonly string[],
|
|
94
|
+
): FailureSummary => ({
|
|
95
|
+
class: "native",
|
|
96
|
+
retryable: false,
|
|
97
|
+
message: messageFor(
|
|
98
|
+
"native",
|
|
99
|
+
stderrTail.slice(-3).join(" | ").slice(0, 512) || `exit ${nativeExitCode}`,
|
|
100
|
+
),
|
|
101
|
+
nativeExitCode: nativeExitCode ?? undefined,
|
|
102
|
+
});
|
|
74
103
|
|
|
75
104
|
export const failureFromLimit = (code: LimitCode): FailureSummary => {
|
|
76
105
|
const cls: FailureClass =
|
|
@@ -114,18 +143,25 @@ export const failureFromTransport = (detail?: string): FailureSummary => ({
|
|
|
114
143
|
|
|
115
144
|
export const failureFromRejected = (opts: {
|
|
116
145
|
issue: RefusalIssue;
|
|
117
|
-
option?:
|
|
146
|
+
option?: import("../interpretation/refusal.js").RefusalOption;
|
|
118
147
|
facet?: DiscoveryFacet;
|
|
119
148
|
supported?: readonly string[];
|
|
149
|
+
supportedBy?: ReadonlyArray<{ harness: string; spelling: string }>;
|
|
150
|
+
hint?: string;
|
|
120
151
|
detail?: string;
|
|
121
152
|
}): FailureSummary => ({
|
|
122
153
|
class: "rejected",
|
|
123
154
|
retryable: false,
|
|
155
|
+
// D8: hint first, support list second - prose order matches the
|
|
156
|
+
// structured fields so an agent scanning the message hits the
|
|
157
|
+
// stay-on-harness suggestion before the switch temptation.
|
|
124
158
|
message: messageFor("rejected", opts.detail ?? opts.issue),
|
|
125
159
|
issue: opts.issue,
|
|
126
160
|
option: opts.option,
|
|
127
161
|
facet: opts.facet,
|
|
128
162
|
supported: opts.supported,
|
|
163
|
+
supportedBy: opts.supportedBy,
|
|
164
|
+
hint: opts.hint,
|
|
129
165
|
});
|
|
130
166
|
|
|
131
167
|
/** Precedence for reduction: lower number = higher priority (wins). */
|
|
@@ -137,7 +173,10 @@ const PRECEDENCE: Record<FailureClass, number> = {
|
|
|
137
173
|
budget: 3,
|
|
138
174
|
task: 3,
|
|
139
175
|
transport: 4,
|
|
140
|
-
|
|
176
|
+
// rejected stands alone (checked before precedence applies); native is
|
|
177
|
+
// terminal-by-classification, never reduced into anything else.
|
|
178
|
+
rejected: 0,
|
|
179
|
+
native: 0,
|
|
141
180
|
};
|
|
142
181
|
|
|
143
182
|
export const reduceFailures = (failures: readonly FailureSummary[]): FailureSummary | undefined => {
|
|
@@ -28,6 +28,7 @@ import type { FailureSummary } from "./failure.js";
|
|
|
28
28
|
import {
|
|
29
29
|
failureFromAuth,
|
|
30
30
|
failureFromLimit,
|
|
31
|
+
failureFromNative,
|
|
31
32
|
failureFromRejected,
|
|
32
33
|
failureFromTransport,
|
|
33
34
|
reduceFailures,
|
|
@@ -96,6 +97,10 @@ export interface TurnRunOptions extends LaunchOptions {
|
|
|
96
97
|
readonly cwd?: string;
|
|
97
98
|
/** Per-call environment, merged over parent; "" deletes. */
|
|
98
99
|
readonly env?: Readonly<Record<string, string>>;
|
|
100
|
+
/** D6 passthrough: raw harness tokens appended verbatim after the
|
|
101
|
+
* normalized argv. Wrong-harness flags here fail in the harness itself
|
|
102
|
+
* and surface as native errors - hcn never validates them. */
|
|
103
|
+
readonly passthrough?: readonly string[];
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
export async function* streamTurn(
|
|
@@ -144,6 +149,9 @@ export async function* streamTurn(
|
|
|
144
149
|
opts.resume === undefined
|
|
145
150
|
? buildLaunchArgv(h, opts)
|
|
146
151
|
: buildResumeArgv(h, { ...opts, sessionId: opts.resume });
|
|
152
|
+
if (opts.passthrough !== undefined && opts.passthrough.length > 0) {
|
|
153
|
+
argv = [...argv, "--", ...opts.passthrough];
|
|
154
|
+
}
|
|
147
155
|
granularity = streamingGranularityOf(h, argv);
|
|
148
156
|
} catch (e) {
|
|
149
157
|
if (e instanceof ArgvRefusalError) {
|
|
@@ -152,6 +160,8 @@ export async function* streamTurn(
|
|
|
152
160
|
option: e.option,
|
|
153
161
|
facet: e.facet,
|
|
154
162
|
supported: e.supported,
|
|
163
|
+
supportedBy: e.supportedBy,
|
|
164
|
+
hint: e.hint,
|
|
155
165
|
detail: e.message,
|
|
156
166
|
});
|
|
157
167
|
// No process spawned on a refusal - log rejected instead of spawn
|
|
@@ -400,7 +410,12 @@ export async function* streamTurn(
|
|
|
400
410
|
try {
|
|
401
411
|
for await (const event of queue) yield event;
|
|
402
412
|
|
|
403
|
-
// Post-queue failure sources
|
|
413
|
+
// Post-queue failure sources. Nonzero exit with no other failure and a
|
|
414
|
+
// non-empty stderr tail is a NATIVE failure (D6): the harness rejected
|
|
415
|
+
// its own arguments or crashed on them - verbatim stderr, native exit
|
|
416
|
+
// code as data, hcn exit 1. Without a stderr tail it stays transport
|
|
417
|
+
// (a silent nonzero exit reads as an environment problem, not a
|
|
418
|
+
// harness judgment).
|
|
404
419
|
if (
|
|
405
420
|
failures.length === 0 &&
|
|
406
421
|
exitCode !== 0 &&
|
|
@@ -408,7 +423,11 @@ export async function* streamTurn(
|
|
|
408
423
|
!killedByWatchdog &&
|
|
409
424
|
!state.limitSeen
|
|
410
425
|
) {
|
|
411
|
-
const
|
|
426
|
+
const tailForNative = stderrTail.snapshot();
|
|
427
|
+
const f =
|
|
428
|
+
tailForNative.length > 0
|
|
429
|
+
? failureFromNative(exitCode, tailForNative)
|
|
430
|
+
: failureFromTransport(`nonzero exit ${exitCode}`);
|
|
412
431
|
failures.push(f);
|
|
413
432
|
// Need to emit this failure before done, even though queue is closed
|
|
414
433
|
yield { kind: "failure", ...f };
|
|
@@ -451,7 +470,18 @@ export async function* streamTurn(
|
|
|
451
470
|
yield { kind: "error", message: tail.join("\n").slice(0, 4096) };
|
|
452
471
|
}
|
|
453
472
|
terminalEventReached = true;
|
|
454
|
-
|
|
473
|
+
// D6: when the failure is native, the harness's own exit convention is
|
|
474
|
+
// DATA (nativeExitCode on the failure), not the done event's contract -
|
|
475
|
+
// hcn owns the process exit code (1 for any native failure) because
|
|
476
|
+
// harness conventions collide with hcn's (codex usage errors exit 2,
|
|
477
|
+
// which hcn reserves for refusals).
|
|
478
|
+
const nativeReduced = reduced?.class === "native";
|
|
479
|
+
yield {
|
|
480
|
+
kind: "done",
|
|
481
|
+
exitCode: nativeReduced ? null : exitCode,
|
|
482
|
+
cause,
|
|
483
|
+
...(reduced ? { failure: reduced } : {}),
|
|
484
|
+
};
|
|
455
485
|
} finally {
|
|
456
486
|
const abandoned = !terminalEventReached;
|
|
457
487
|
cancelled = true;
|
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* no caller re-derives them.
|
|
6
6
|
*/
|
|
7
7
|
import type { HarnessDescriptor, StreamingGranularity } from "../knowledge/descriptor.js";
|
|
8
|
+
import { defaultDescriptors } from "../knowledge/overrides.js";
|
|
8
9
|
import { ArgvRefusalError } from "./refusal.js";
|
|
9
10
|
import { assertUsableSessionId } from "./session-id.js";
|
|
11
|
+
import { supportedBy } from "./support.js";
|
|
12
|
+
import { renderToolSelection } from "./tool-selection.js";
|
|
10
13
|
import { renderTurnOptions } from "./turn-options.js";
|
|
11
14
|
import { validateModel } from "./vocabulary.js";
|
|
12
15
|
|
|
@@ -50,6 +53,7 @@ export interface DiscoveryOptions {
|
|
|
50
53
|
export interface TurnOptions {
|
|
51
54
|
readonly prompt: string;
|
|
52
55
|
readonly tools?: readonly string[];
|
|
56
|
+
readonly excludeTools?: readonly string[];
|
|
53
57
|
readonly model?: string;
|
|
54
58
|
readonly autonomy?: boolean;
|
|
55
59
|
readonly effort?: string;
|
|
@@ -90,32 +94,23 @@ const turnTail = (h: HarnessDescriptor, opts: TurnOptions): string[] => {
|
|
|
90
94
|
}
|
|
91
95
|
if (opts.autonomy === true) {
|
|
92
96
|
if (h.autonomy === null) {
|
|
97
|
+
const by = supportedBy(defaultDescriptors(), "autonomy");
|
|
93
98
|
throw new ArgvRefusalError({
|
|
94
99
|
issue: "no-autonomy-mode",
|
|
95
100
|
harness: h.name,
|
|
96
|
-
supported:
|
|
101
|
+
supported: by.map((e) => `${e.harness} ${e.spelling}`),
|
|
102
|
+
supportedBy: by,
|
|
103
|
+
hint: "pi has no unattended-run flag; approximate with a per-tool allowlist (--tools read,bash) if you need unattended behavior on pi",
|
|
97
104
|
});
|
|
98
105
|
}
|
|
99
106
|
tail.push(h.autonomy.flag);
|
|
100
107
|
}
|
|
101
|
-
if (opts.tools !== undefined) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
detail: "tools",
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
if (opts.tools.length === 0 || opts.tools.some((t) => t.trim() === "" || t.includes(","))) {
|
|
111
|
-
throw new ArgvRefusalError({
|
|
112
|
-
issue: "invalid-tool-grant",
|
|
113
|
-
harness: h.name,
|
|
114
|
-
supported: ["non-empty, comma-free tool names"],
|
|
115
|
-
detail: `tools=${JSON.stringify(opts.tools)}`,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
tail.push(h.launch.toolsFlag, opts.tools.join(","));
|
|
108
|
+
if (opts.tools !== undefined || opts.excludeTools !== undefined) {
|
|
109
|
+
const rendered = renderToolSelection(h, {
|
|
110
|
+
include: opts.tools,
|
|
111
|
+
exclude: opts.excludeTools,
|
|
112
|
+
});
|
|
113
|
+
tail.push(...rendered.tokens);
|
|
119
114
|
}
|
|
120
115
|
return tail;
|
|
121
116
|
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated refusal hints (D8), confirmed in review 2026-08-18
|
|
3
|
+
* (test/fixtures/phase0/hints-confirmed.md is the durable record). A hint
|
|
4
|
+
* is the nearest-alternative control for the CURRENT harness, shown before
|
|
5
|
+
* the cross-harness support list so a scanning caller meets the
|
|
6
|
+
* stay-on-harness suggestion first. Wording is locked verbatim; unit tests
|
|
7
|
+
* pin every string.
|
|
8
|
+
*/
|
|
9
|
+
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
10
|
+
import type { RefusalOption } from "./refusal.js";
|
|
11
|
+
|
|
12
|
+
const HINTS: Readonly<Record<string, Readonly<Record<string, string>>>> = deepFreezeSafe({
|
|
13
|
+
claude: {
|
|
14
|
+
sandbox:
|
|
15
|
+
"claude has no sandbox modes; approximate with a per-tool allowlist (--tools Read,Bash) or --disallowedTools to keep tools out, and run untrusted work in a disposable directory or container",
|
|
16
|
+
write:
|
|
17
|
+
"claude has no write toggle; keep the Write tool out with --tools that omits it, or --disallowedTools Write for the deny-complement spelling",
|
|
18
|
+
shell:
|
|
19
|
+
"claude has no shell toggle; disallow the Bash tool (--tools without Bash, or --disallowedTools Bash) and note Monitor can still run commands in headless runs",
|
|
20
|
+
maxSteps:
|
|
21
|
+
"claude has no step cap flag; bound the work in the prompt (task size, 'stop after N operations') or impose a wall-clock timeout at the caller",
|
|
22
|
+
provider:
|
|
23
|
+
"claude routes models through Anthropic only (Bedrock/Vertex via settings); use --model to pick within it - there is no separate provider selector",
|
|
24
|
+
"discovery.tools":
|
|
25
|
+
"claude has no tools-discovery toggle (tools are always compiled in); shape the tool set with --tools/--disallowedTools instead",
|
|
26
|
+
"discovery.instructionFiles":
|
|
27
|
+
"claude has no isolated instruction-file toggle; --setting-sources project isolates from user-level settings but also skips hooks, LSP and keychain reads - weigh that before using it as an approximation",
|
|
28
|
+
},
|
|
29
|
+
codex: {
|
|
30
|
+
write:
|
|
31
|
+
"codex has no write toggle; use --sandbox read-only (config: sandbox_mode) so shell commands cannot write either",
|
|
32
|
+
shell:
|
|
33
|
+
"codex has no shell toggle; disable the shell tool via config (-c features.shell_tool=false) or use --sandbox read-only",
|
|
34
|
+
maxSteps:
|
|
35
|
+
"codex has no step cap flag; bound via sandbox policy and a caller-side timeout, or prompt-level limits",
|
|
36
|
+
provider:
|
|
37
|
+
"codex routes models through OpenAI (or --oss for local); use --model to pick within it - there is no separate provider selector",
|
|
38
|
+
"discovery.tools":
|
|
39
|
+
"codex has no tools-discovery toggle; disable tool classes via config keys (features.shell_tool, web_search) instead",
|
|
40
|
+
"discovery.instructionFiles":
|
|
41
|
+
"codex always loads AGENTS.md hierarchy; no per-call toggle exists - keep the files out of the tree or work from a directory without them",
|
|
42
|
+
"discovery.extensions":
|
|
43
|
+
"codex loads MCP servers and plugins from config; disable per-server with -c or codex mcp remove rather than a call-time toggle",
|
|
44
|
+
"discovery.skills":
|
|
45
|
+
"codex discovers skills from its skills directory; no call-time toggle - remove or move the skill files instead",
|
|
46
|
+
},
|
|
47
|
+
pi: {
|
|
48
|
+
sandbox:
|
|
49
|
+
"pi has no sandbox dimension; approximate with a minimal tool grant (--tools read,grep,find,ls) so the run cannot write or execute, or sandbox the process yourself (container, VM)",
|
|
50
|
+
write:
|
|
51
|
+
"pi has no write toggle; grant without the write tool (--tools read,bash,edit) or use --exclude-tools write",
|
|
52
|
+
shell:
|
|
53
|
+
"pi has no shell toggle; grant without the bash tool (--tools read,edit,write) or use --exclude-tools bash",
|
|
54
|
+
maxSteps:
|
|
55
|
+
"pi has no step cap flag; bound the work in the prompt or impose a wall-clock timeout at the caller",
|
|
56
|
+
},
|
|
57
|
+
muse: {
|
|
58
|
+
sandbox:
|
|
59
|
+
"muse's sandbox is on by default and not selectable per-call; --disable-sandbox exists to turn it OFF, and exposure can be tuned with --disable-write, --disable-shell, --disable-web-tools - there is no mode selector",
|
|
60
|
+
provider:
|
|
61
|
+
"muse routes models through its own API; use --model to pick within it - there is no separate provider selector",
|
|
62
|
+
"discovery.tools":
|
|
63
|
+
"muse has no tools-discovery toggle; gate execution with --disable-write/--disable-shell/--disable-web-tools",
|
|
64
|
+
"discovery.instructionFiles":
|
|
65
|
+
"muse loads rules per workspace trust; --no-foreign-personal-context excludes foreign personal rules, and withholding --trust-workspace keeps workspace rules unloaded",
|
|
66
|
+
"discovery.skills":
|
|
67
|
+
"muse scopes skills by trust like rules; --no-foreign-personal-context drops foreign skills and untrusted workspaces stay unloaded - there is no unconditional skills-off switch",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/** deepFreeze without importing the descriptor's (which carries extra
|
|
72
|
+
* machinery); structure is plain JSON so Object.freeze all the way down. */
|
|
73
|
+
function deepFreezeSafe<T>(value: T): T {
|
|
74
|
+
if (typeof value === "object" && value !== null) {
|
|
75
|
+
for (const v of Object.values(value as Record<string, unknown>)) deepFreezeSafe(v);
|
|
76
|
+
Object.freeze(value);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The confirmed hint for a refused option on a harness, or undefined. */
|
|
82
|
+
export const hintFor = (harness: string, option: RefusalOption): string | undefined =>
|
|
83
|
+
HINTS[harness]?.[option];
|
|
84
|
+
|
|
85
|
+
/** All hints - for the unit test that pins every string against the
|
|
86
|
+
* evidence file's count. */
|
|
87
|
+
export const allHints = (): ReadonlyArray<{ harness: string; option: string; text: string }> => {
|
|
88
|
+
const out: { harness: string; option: string; text: string }[] = [];
|
|
89
|
+
for (const [harness, table] of Object.entries(HINTS)) {
|
|
90
|
+
for (const [option, text] of Object.entries(table)) out.push({ harness, option, text });
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Convenience for raise sites that already hold the descriptor. */
|
|
96
|
+
export const hintForDescriptor = (
|
|
97
|
+
h: HarnessDescriptor,
|
|
98
|
+
option: RefusalOption,
|
|
99
|
+
): string | undefined => hintFor(h.name, option);
|