@dungle-scrubs/harness-cli-normalizer 0.3.1 → 0.4.1
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 +12 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +44 -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 +147 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +17 -1
- package/dist/cli/help.js.map +1 -1
- 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 +175 -5
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/skills-root.d.ts +4 -0
- package/dist/cli/skills-root.d.ts.map +1 -0
- package/dist/cli/skills-root.js +45 -0
- package/dist/cli/skills-root.js.map +1 -0
- package/dist/execution/failure.d.ts +24 -4
- package/dist/execution/failure.d.ts.map +1 -1
- package/dist/execution/failure.js +39 -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 +43 -6
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/interpretation/argv.d.ts +6 -0
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +17 -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 +162 -0
- package/dist/interpretation/resolve-options.js.map +1 -0
- package/dist/interpretation/skills-selection.d.ts +22 -0
- package/dist/interpretation/skills-selection.d.ts.map +1 -0
- package/dist/interpretation/skills-selection.js +49 -0
- package/dist/interpretation/skills-selection.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 +35 -1
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +20 -0
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +39 -0
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/muse.d.ts.map +1 -1
- package/dist/knowledge/muse.js +20 -0
- package/dist/knowledge/muse.js.map +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +22 -0
- package/dist/knowledge/pi.js.map +1 -1
- package/dist/knowledge/profile.d.ts +16 -0
- package/dist/knowledge/profile.d.ts.map +1 -0
- package/dist/knowledge/profile.js +51 -0
- package/dist/knowledge/profile.js.map +1 -0
- package/package.json +2 -23
- package/src/cli/args.ts +64 -3
- package/src/cli/config.ts +176 -0
- package/src/cli/help.ts +17 -1
- package/src/cli/inspect.ts +34 -1
- package/src/cli/render.ts +10 -1
- package/src/cli/run.ts +203 -6
- package/src/cli/skills-root.ts +47 -0
- package/src/execution/failure.ts +58 -5
- package/src/execution/stream-turn.ts +50 -5
- package/src/interpretation/argv.ts +23 -19
- package/src/interpretation/hints.ts +99 -0
- package/src/interpretation/refusal.ts +33 -3
- package/src/interpretation/resolve-options.ts +214 -0
- package/src/interpretation/skills-selection.ts +73 -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 +35 -1
- package/src/knowledge/codex.ts +20 -0
- package/src/knowledge/descriptor.ts +39 -0
- package/src/knowledge/muse.ts +20 -0
- package/src/knowledge/pi.ts +22 -0
- package/src/knowledge/profile.ts +53 -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,110 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
107
185
|
}
|
|
108
186
|
|
|
109
187
|
const isExplicit = promptSource !== "positional";
|
|
188
|
+
|
|
189
|
+
// issue #38: resolve --skills names against the caller's registry root,
|
|
190
|
+
// then hand the harness its native rendering (pi loads; claude narrows).
|
|
191
|
+
const rawSkills = (turnOpts as unknown as { skills?: string[] }).skills;
|
|
192
|
+
if (rawSkills !== undefined && rawSkills.length > 0) {
|
|
193
|
+
try {
|
|
194
|
+
const { resolveSkillNames, listKnownSkills } = await import("./skills-root.js");
|
|
195
|
+
const resolvedSkills = resolveSkillNames(rawSkills);
|
|
196
|
+
const claudeTokens: string[] = [];
|
|
197
|
+
if (h.name === "claude") {
|
|
198
|
+
const { claudeSkillOverridesArg } = await import("../interpretation/skills-selection.js");
|
|
199
|
+
claudeTokens.push(...claudeSkillOverridesArg(listKnownSkills(), resolvedSkills));
|
|
200
|
+
}
|
|
201
|
+
(turnOpts as unknown as Record<string, unknown>).skills = resolvedSkills;
|
|
202
|
+
(turnOpts as unknown as Record<string, unknown>).__claudeSkillTokens = claudeTokens;
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (err instanceof ArgvRefusalError) {
|
|
205
|
+
process.stderr.write(`${err.message}\n`);
|
|
206
|
+
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
207
|
+
process.exitCode = 2;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Defaults profile + user config: LAUNCH-ONLY. A resumed session keeps
|
|
215
|
+
// its own settings; the resolver never runs on resume paths.
|
|
216
|
+
let resolvedProvenance: readonly ProvenanceEntry[] = [];
|
|
217
|
+
let resolvedUnrenderable: readonly string[] = [];
|
|
218
|
+
let effectiveTurnOpts: ReturnType<typeof parseTurnOptions> = turnOpts;
|
|
219
|
+
const resolvedTiers: {
|
|
220
|
+
user?: Partial<ReturnType<typeof parseTurnOptions>>;
|
|
221
|
+
project?: Partial<ReturnType<typeof parseTurnOptions>>;
|
|
222
|
+
} = {};
|
|
223
|
+
if (extra.resume === undefined) {
|
|
224
|
+
const tiers = resolvedTiers;
|
|
225
|
+
const { loadUserConfig, loadProjectConfig, ConfigError } = await import("./config.js");
|
|
226
|
+
try {
|
|
227
|
+
const loaded = loadUserConfig();
|
|
228
|
+
if (loaded !== null) tiers.user = loaded.config;
|
|
229
|
+
const proj = loadProjectConfig();
|
|
230
|
+
if (proj !== null) tiers.project = proj.config;
|
|
231
|
+
} catch (configErr) {
|
|
232
|
+
if (configErr instanceof ConfigError) {
|
|
233
|
+
process.stderr.write(`config error: ${(configErr as Error).message}\n`);
|
|
234
|
+
process.exitCode = 2;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
throw configErr;
|
|
238
|
+
}
|
|
239
|
+
let resolved: ReturnType<typeof resolveEffectiveOptions>;
|
|
240
|
+
try {
|
|
241
|
+
resolved = resolveEffectiveOptions(h, { ...turnOpts, prompt } as never, tiers);
|
|
242
|
+
} catch (resErr) {
|
|
243
|
+
if (resErr instanceof FloorExceededError) {
|
|
244
|
+
process.stderr.write(`${(resErr as Error).message}\n`);
|
|
245
|
+
process.exitCode = 2;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
throw resErr;
|
|
249
|
+
}
|
|
250
|
+
const { provenance, unrenderable } = resolved;
|
|
251
|
+
resolvedProvenance = provenance;
|
|
252
|
+
resolvedUnrenderable = unrenderable;
|
|
253
|
+
const { prompt: _p, ...rest } = resolved.options as { prompt: string };
|
|
254
|
+
effectiveTurnOpts = rest as ReturnType<typeof parseTurnOptions>;
|
|
255
|
+
// Provenance is diagnostic data like the spawn line - stderr in BOTH
|
|
256
|
+
// render modes, never stdout (stdout carries the NDJSON contract).
|
|
257
|
+
if (provenance.length > 0 || unrenderable.length > 0) {
|
|
258
|
+
for (const entry of provenance) {
|
|
259
|
+
process.stderr.write(
|
|
260
|
+
`provenance: ${entry.key} = ${JSON.stringify(entry.value)} (${entry.tier})\n`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
for (const key of unrenderable) {
|
|
264
|
+
process.stderr.write(
|
|
265
|
+
`divergence: profile ${JSON.stringify(key)} not expressible on ${h.name}; harness default applies\n`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
110
271
|
const fullOpts = {
|
|
111
|
-
...
|
|
272
|
+
...effectiveTurnOpts,
|
|
112
273
|
prompt,
|
|
113
274
|
cwd: extra.cwd,
|
|
114
275
|
env: extra.env,
|
|
115
276
|
resume: extra.resume,
|
|
277
|
+
...(passthrough.length > 0 ? { passthrough } : {}),
|
|
116
278
|
...(isExplicit ? { __explicitPrompt: true as const } : {}),
|
|
117
|
-
} as Parameters<typeof streamTurn>[1] & {
|
|
279
|
+
} as Parameters<typeof streamTurn>[1] & {
|
|
280
|
+
resume?: string;
|
|
281
|
+
__explicitPrompt?: boolean;
|
|
282
|
+
passthrough?: readonly string[];
|
|
283
|
+
};
|
|
118
284
|
|
|
119
285
|
// Pre-validate via building argv to catch refusals before spawn (so we don't spawn on bad args)
|
|
120
286
|
let _validated = false;
|
|
121
287
|
let preArgv: string[] | null = null;
|
|
122
288
|
try {
|
|
123
289
|
if (fullOpts.resume) {
|
|
290
|
+
// Resume never carries profile/config resolution (launch-only rule),
|
|
291
|
+
// so it builds from the raw turn options.
|
|
124
292
|
preArgv = buildResumeArgv(h, {
|
|
125
293
|
...(turnOpts as object),
|
|
126
294
|
prompt,
|
|
@@ -128,16 +296,29 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
128
296
|
__explicitPrompt: isExplicit,
|
|
129
297
|
} as never);
|
|
130
298
|
} else {
|
|
299
|
+
// Launch builds from the RESOLVED options so the spawn line and the
|
|
300
|
+
// real argv agree.
|
|
131
301
|
preArgv = buildLaunchArgv(h, {
|
|
132
|
-
...(
|
|
302
|
+
...(effectiveTurnOpts as object),
|
|
133
303
|
prompt,
|
|
134
304
|
__explicitPrompt: isExplicit,
|
|
135
305
|
} as never);
|
|
306
|
+
const claudeSkillTokens = (effectiveTurnOpts as unknown as { __claudeSkillTokens?: string[] })
|
|
307
|
+
.__claudeSkillTokens;
|
|
308
|
+
if (claudeSkillTokens !== undefined && claudeSkillTokens.length > 0) {
|
|
309
|
+
preArgv.push(...claudeSkillTokens);
|
|
310
|
+
}
|
|
136
311
|
}
|
|
137
312
|
_validated = true;
|
|
138
313
|
} catch (err) {
|
|
139
314
|
if (err instanceof ArgvRefusalError) {
|
|
140
315
|
process.stderr.write(`${err.message}\n`);
|
|
316
|
+
if (err.hint) process.stderr.write(`hint: ${err.hint}\n`);
|
|
317
|
+
if (err.supportedBy?.length) {
|
|
318
|
+
process.stderr.write(
|
|
319
|
+
`supported on: ${err.supportedBy.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
141
322
|
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
142
323
|
process.exitCode = 2;
|
|
143
324
|
return;
|
|
@@ -161,7 +342,17 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
161
342
|
// Delete HERDR_ENV before spawn
|
|
162
343
|
delete (process.env as Record<string, string | undefined>).HERDR_ENV;
|
|
163
344
|
|
|
164
|
-
|
|
345
|
+
// D11: opt-in wall-clock budget. Precedence arg > project > user (no
|
|
346
|
+
// profile entry by ratification). 0 = explicit disable.
|
|
347
|
+
const timeoutSeconds =
|
|
348
|
+
extra.timeoutSeconds !== undefined
|
|
349
|
+
? extra.timeoutSeconds
|
|
350
|
+
: ((resolvedTiers?.project as { timeout?: number } | undefined)?.timeout ??
|
|
351
|
+
(resolvedTiers?.user as { timeout?: number } | undefined)?.timeout);
|
|
352
|
+
const deps =
|
|
353
|
+
timeoutSeconds !== undefined && timeoutSeconds > 0
|
|
354
|
+
? nodeRunnerDeps({ turnTimeoutMs: timeoutSeconds * 1000 })
|
|
355
|
+
: nodeRunnerDeps();
|
|
165
356
|
|
|
166
357
|
// Signal handling
|
|
167
358
|
const _currentProc: { signal: (sig: "SIGTERM" | "SIGKILL") => void } | null = null;
|
|
@@ -234,6 +425,12 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
234
425
|
} catch (err) {
|
|
235
426
|
if (err instanceof ArgvRefusalError) {
|
|
236
427
|
process.stderr.write(`${err.message}\n`);
|
|
428
|
+
if (err.hint) process.stderr.write(`hint: ${err.hint}\n`);
|
|
429
|
+
if (err.supportedBy?.length) {
|
|
430
|
+
process.stderr.write(
|
|
431
|
+
`supported on: ${err.supportedBy.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
237
434
|
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
238
435
|
process.exitCode = 2;
|
|
239
436
|
process.off("SIGINT", onSig);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills registry resolution (issue #38): --skills names resolve against
|
|
3
|
+
* the caller's deployed root. The delegating agent's judgment picked the
|
|
4
|
+
* names; this only maps them to paths and refuses unknowns loudly -
|
|
5
|
+
* a typo silently loading nothing is the failure this prevents.
|
|
6
|
+
*
|
|
7
|
+
* Root: $HCN_SKILLS_ROOT, else ~/.agents/skills (the deployed shared
|
|
8
|
+
* registry; ~/.claude/skills symlinks to it, so claude's complement
|
|
9
|
+
* narrowing and pi's loads see the same set).
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { ArgvRefusalError } from "../interpretation/refusal.js";
|
|
15
|
+
|
|
16
|
+
export const skillsRoot = (): string =>
|
|
17
|
+
process.env.HCN_SKILLS_ROOT ?? join(homedir(), ".agents", "skills");
|
|
18
|
+
|
|
19
|
+
const skillDir = (root: string, name: string): string => join(root, name);
|
|
20
|
+
|
|
21
|
+
export const listKnownSkills = (): string[] => {
|
|
22
|
+
const root = skillsRoot();
|
|
23
|
+
if (!existsSync(root)) return [];
|
|
24
|
+
return readdirSync(root).filter((n) => {
|
|
25
|
+
try {
|
|
26
|
+
return statSync(skillDir(root, n)).isDirectory();
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const resolveSkillNames = (names: readonly string[]): string[] => {
|
|
34
|
+
const root = skillsRoot();
|
|
35
|
+
const known = new Set(listKnownSkills());
|
|
36
|
+
const unknown = names.filter((n) => !known.has(n));
|
|
37
|
+
if (unknown.length > 0) {
|
|
38
|
+
throw new ArgvRefusalError({
|
|
39
|
+
issue: "unknown-tool-name",
|
|
40
|
+
harness: "claude",
|
|
41
|
+
option: "skills",
|
|
42
|
+
supported: known.size > 0 ? [[...known].join(", ")] : ["(registry is empty)"],
|
|
43
|
+
detail: `unknown skill name(s): ${unknown.join(", ")}`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return names.map((n) => skillDir(root, n));
|
|
47
|
+
};
|
package/src/execution/failure.ts
CHANGED
|
@@ -30,6 +30,8 @@ export const FAILURE_CLASSES = Object.freeze([
|
|
|
30
30
|
"task",
|
|
31
31
|
"transport",
|
|
32
32
|
"rejected",
|
|
33
|
+
"native",
|
|
34
|
+
"timeout",
|
|
33
35
|
] as const);
|
|
34
36
|
export type FailureClass = (typeof FAILURE_CLASSES)[number];
|
|
35
37
|
|
|
@@ -41,13 +43,21 @@ export interface FailureSummary {
|
|
|
41
43
|
readonly authKind?: AuthFailureKind;
|
|
42
44
|
readonly resetsAt?: number;
|
|
43
45
|
readonly issue?: RefusalIssue;
|
|
44
|
-
readonly option?:
|
|
46
|
+
readonly option?: import("../interpretation/refusal.js").RefusalOption;
|
|
45
47
|
readonly facet?: DiscoveryFacet;
|
|
46
48
|
readonly supported?: readonly string[];
|
|
49
|
+
/** D7: cross-harness support entries derived from descriptors. */
|
|
50
|
+
readonly supportedBy?: ReadonlyArray<{ harness: string; spelling: string }>;
|
|
51
|
+
/** D8: nearest-alternative hint for the current harness. */
|
|
52
|
+
readonly hint?: string;
|
|
53
|
+
/** D6: the harness process's own exit code for a native failure - data,
|
|
54
|
+
* because native conventions differ from hcn's (codex usage errors exit
|
|
55
|
+
* 2, which hcn reserves for refusals). */
|
|
56
|
+
readonly nativeExitCode?: number;
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
export const retryableOf = (cls: FailureClass): boolean =>
|
|
50
|
-
cls !== "task" && cls !== "budget" && cls !== "rejected";
|
|
60
|
+
cls !== "task" && cls !== "budget" && cls !== "rejected" && cls !== "native" && cls !== "timeout";
|
|
51
61
|
|
|
52
62
|
const messageFor = (cls: FailureClass, detail?: string): string => {
|
|
53
63
|
switch (cls) {
|
|
@@ -67,10 +77,42 @@ const messageFor = (cls: FailureClass, detail?: string): string => {
|
|
|
67
77
|
return `Transport failure${detail ? ` (${detail})` : ""} - retry or route to another provider`;
|
|
68
78
|
case "rejected":
|
|
69
79
|
return `Request rejected${detail ? ` (${detail})` : ""} - change options or harness`;
|
|
80
|
+
case "timeout":
|
|
81
|
+
// D11: hcn's wall-clock budget expired and the run was killed. The
|
|
82
|
+
// caller chose this number (arg or config); retrying unchanged will
|
|
83
|
+
// hit the same wall - retry only with a raised budget.
|
|
84
|
+
return `Timeout: run exceeded its wall-clock budget and was killed (SIGTERM, then SIGKILL after grace) - raise --timeout for this workload or split the task`;
|
|
85
|
+
case "native":
|
|
86
|
+
// D6: labeled NATIVE so it can never be confused with an hcn error.
|
|
87
|
+
// The harness's own message follows verbatim; the process exit code
|
|
88
|
+
// rides as data (nativeExitCode), because harness conventions differ
|
|
89
|
+
// (codex exits 2 on usage errors - the same code hcn uses for
|
|
90
|
+
// refusals, so hcn owns its own exit code and reports the native one).
|
|
91
|
+
return `NATIVE ERROR from harness${detail ? `: ${detail}` : ""} - the harness rejected or failed on its own arguments; this is not an hcn error`;
|
|
70
92
|
default:
|
|
71
93
|
return `Failure${detail ? ` (${detail})` : ""}`;
|
|
72
94
|
}
|
|
73
|
-
};
|
|
95
|
+
}; /** D11: the run outlived its caller-set wall-clock budget. */
|
|
96
|
+
export const failureFromTimeout = (): FailureSummary => ({
|
|
97
|
+
class: "timeout",
|
|
98
|
+
retryable: false,
|
|
99
|
+
message: messageFor("timeout"),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/** D6: a failure that belongs to the harness, not hcn. Carries the
|
|
103
|
+
* native stderr verbatim and the native exit code as data. */
|
|
104
|
+
export const failureFromNative = (
|
|
105
|
+
nativeExitCode: number | null,
|
|
106
|
+
stderrTail: readonly string[],
|
|
107
|
+
): FailureSummary => ({
|
|
108
|
+
class: "native",
|
|
109
|
+
retryable: false,
|
|
110
|
+
message: messageFor(
|
|
111
|
+
"native",
|
|
112
|
+
stderrTail.slice(-3).join(" | ").slice(0, 512) || `exit ${nativeExitCode}`,
|
|
113
|
+
),
|
|
114
|
+
nativeExitCode: nativeExitCode ?? undefined,
|
|
115
|
+
});
|
|
74
116
|
|
|
75
117
|
export const failureFromLimit = (code: LimitCode): FailureSummary => {
|
|
76
118
|
const cls: FailureClass =
|
|
@@ -114,18 +156,25 @@ export const failureFromTransport = (detail?: string): FailureSummary => ({
|
|
|
114
156
|
|
|
115
157
|
export const failureFromRejected = (opts: {
|
|
116
158
|
issue: RefusalIssue;
|
|
117
|
-
option?:
|
|
159
|
+
option?: import("../interpretation/refusal.js").RefusalOption;
|
|
118
160
|
facet?: DiscoveryFacet;
|
|
119
161
|
supported?: readonly string[];
|
|
162
|
+
supportedBy?: ReadonlyArray<{ harness: string; spelling: string }>;
|
|
163
|
+
hint?: string;
|
|
120
164
|
detail?: string;
|
|
121
165
|
}): FailureSummary => ({
|
|
122
166
|
class: "rejected",
|
|
123
167
|
retryable: false,
|
|
168
|
+
// D8: hint first, support list second - prose order matches the
|
|
169
|
+
// structured fields so an agent scanning the message hits the
|
|
170
|
+
// stay-on-harness suggestion before the switch temptation.
|
|
124
171
|
message: messageFor("rejected", opts.detail ?? opts.issue),
|
|
125
172
|
issue: opts.issue,
|
|
126
173
|
option: opts.option,
|
|
127
174
|
facet: opts.facet,
|
|
128
175
|
supported: opts.supported,
|
|
176
|
+
supportedBy: opts.supportedBy,
|
|
177
|
+
hint: opts.hint,
|
|
129
178
|
});
|
|
130
179
|
|
|
131
180
|
/** Precedence for reduction: lower number = higher priority (wins). */
|
|
@@ -137,7 +186,11 @@ const PRECEDENCE: Record<FailureClass, number> = {
|
|
|
137
186
|
budget: 3,
|
|
138
187
|
task: 3,
|
|
139
188
|
transport: 4,
|
|
140
|
-
|
|
189
|
+
timeout: 3,
|
|
190
|
+
// rejected stands alone (checked before precedence applies); native is
|
|
191
|
+
// terminal-by-classification, never reduced into anything else.
|
|
192
|
+
rejected: 0,
|
|
193
|
+
native: 0,
|
|
141
194
|
};
|
|
142
195
|
|
|
143
196
|
export const reduceFailures = (failures: readonly FailureSummary[]): FailureSummary | undefined => {
|
|
@@ -28,7 +28,9 @@ import type { FailureSummary } from "./failure.js";
|
|
|
28
28
|
import {
|
|
29
29
|
failureFromAuth,
|
|
30
30
|
failureFromLimit,
|
|
31
|
+
failureFromNative,
|
|
31
32
|
failureFromRejected,
|
|
33
|
+
failureFromTimeout,
|
|
32
34
|
failureFromTransport,
|
|
33
35
|
reduceFailures,
|
|
34
36
|
} from "./failure.js";
|
|
@@ -96,6 +98,10 @@ export interface TurnRunOptions extends LaunchOptions {
|
|
|
96
98
|
readonly cwd?: string;
|
|
97
99
|
/** Per-call environment, merged over parent; "" deletes. */
|
|
98
100
|
readonly env?: Readonly<Record<string, string>>;
|
|
101
|
+
/** D6 passthrough: raw harness tokens appended verbatim after the
|
|
102
|
+
* normalized argv. Wrong-harness flags here fail in the harness itself
|
|
103
|
+
* and surface as native errors - hcn never validates them. */
|
|
104
|
+
readonly passthrough?: readonly string[];
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
export async function* streamTurn(
|
|
@@ -144,6 +150,16 @@ export async function* streamTurn(
|
|
|
144
150
|
opts.resume === undefined
|
|
145
151
|
? buildLaunchArgv(h, opts)
|
|
146
152
|
: buildResumeArgv(h, { ...opts, sessionId: opts.resume });
|
|
153
|
+
if (opts.passthrough !== undefined && opts.passthrough.length > 0) {
|
|
154
|
+
argv = [...argv, "--", ...opts.passthrough];
|
|
155
|
+
}
|
|
156
|
+
// issue #38: claude renders the skills allowlist as settings JSON at
|
|
157
|
+
// the argv tail (the complement-off form).
|
|
158
|
+
const claudeSkillTokens = (opts as unknown as { __claudeSkillTokens?: string[] })
|
|
159
|
+
.__claudeSkillTokens;
|
|
160
|
+
if (claudeSkillTokens !== undefined && claudeSkillTokens.length > 0) {
|
|
161
|
+
argv = [...argv, ...claudeSkillTokens];
|
|
162
|
+
}
|
|
147
163
|
granularity = streamingGranularityOf(h, argv);
|
|
148
164
|
} catch (e) {
|
|
149
165
|
if (e instanceof ArgvRefusalError) {
|
|
@@ -152,6 +168,8 @@ export async function* streamTurn(
|
|
|
152
168
|
option: e.option,
|
|
153
169
|
facet: e.facet,
|
|
154
170
|
supported: e.supported,
|
|
171
|
+
supportedBy: e.supportedBy,
|
|
172
|
+
hint: e.hint,
|
|
155
173
|
detail: e.message,
|
|
156
174
|
});
|
|
157
175
|
// No process spawned on a refusal - log rejected instead of spawn
|
|
@@ -400,7 +418,12 @@ export async function* streamTurn(
|
|
|
400
418
|
try {
|
|
401
419
|
for await (const event of queue) yield event;
|
|
402
420
|
|
|
403
|
-
// Post-queue failure sources
|
|
421
|
+
// Post-queue failure sources. Nonzero exit with no other failure and a
|
|
422
|
+
// non-empty stderr tail is a NATIVE failure (D6): the harness rejected
|
|
423
|
+
// its own arguments or crashed on them - verbatim stderr, native exit
|
|
424
|
+
// code as data, hcn exit 1. Without a stderr tail it stays transport
|
|
425
|
+
// (a silent nonzero exit reads as an environment problem, not a
|
|
426
|
+
// harness judgment).
|
|
404
427
|
if (
|
|
405
428
|
failures.length === 0 &&
|
|
406
429
|
exitCode !== 0 &&
|
|
@@ -408,14 +431,23 @@ export async function* streamTurn(
|
|
|
408
431
|
!killedByWatchdog &&
|
|
409
432
|
!state.limitSeen
|
|
410
433
|
) {
|
|
411
|
-
const
|
|
434
|
+
const tailForNative = stderrTail.snapshot();
|
|
435
|
+
const f =
|
|
436
|
+
tailForNative.length > 0
|
|
437
|
+
? failureFromNative(exitCode, tailForNative)
|
|
438
|
+
: failureFromTransport(`nonzero exit ${exitCode}`);
|
|
412
439
|
failures.push(f);
|
|
413
440
|
// Need to emit this failure before done, even though queue is closed
|
|
414
441
|
yield { kind: "failure", ...f };
|
|
415
442
|
}
|
|
416
443
|
// Stall watchdog also implies a transport failure if not already present
|
|
417
444
|
if (killedByWatchdog && failures.length === 0) {
|
|
418
|
-
|
|
445
|
+
// D11: a wall-clock deadline kill is a timeout, not a stall - the
|
|
446
|
+
// run was not necessarily silent, it simply outlived its budget.
|
|
447
|
+
const f =
|
|
448
|
+
watchdogReason === "turn-deadline"
|
|
449
|
+
? failureFromTimeout()
|
|
450
|
+
: failureFromTransport(`stalled: ${watchdogReason ?? "inactivity"}`);
|
|
419
451
|
failures.push(f);
|
|
420
452
|
yield { kind: "failure", ...f };
|
|
421
453
|
}
|
|
@@ -423,7 +455,9 @@ export async function* streamTurn(
|
|
|
423
455
|
let cause: ExitCause = state.limitSeen
|
|
424
456
|
? "limit"
|
|
425
457
|
: killedByWatchdog && exitCode !== 0
|
|
426
|
-
? "
|
|
458
|
+
? watchdogReason === "turn-deadline"
|
|
459
|
+
? "killed" // D11: the run was killed on budget, not stalled
|
|
460
|
+
: "stall"
|
|
427
461
|
: exitCode === 0
|
|
428
462
|
? "clean"
|
|
429
463
|
: exitCode === null
|
|
@@ -451,7 +485,18 @@ export async function* streamTurn(
|
|
|
451
485
|
yield { kind: "error", message: tail.join("\n").slice(0, 4096) };
|
|
452
486
|
}
|
|
453
487
|
terminalEventReached = true;
|
|
454
|
-
|
|
488
|
+
// D6: when the failure is native, the harness's own exit convention is
|
|
489
|
+
// DATA (nativeExitCode on the failure), not the done event's contract -
|
|
490
|
+
// hcn owns the process exit code (1 for any native failure) because
|
|
491
|
+
// harness conventions collide with hcn's (codex usage errors exit 2,
|
|
492
|
+
// which hcn reserves for refusals).
|
|
493
|
+
const nativeReduced = reduced?.class === "native";
|
|
494
|
+
yield {
|
|
495
|
+
kind: "done",
|
|
496
|
+
exitCode: nativeReduced ? null : exitCode,
|
|
497
|
+
cause,
|
|
498
|
+
...(reduced ? { failure: reduced } : {}),
|
|
499
|
+
};
|
|
455
500
|
} finally {
|
|
456
501
|
const abandoned = !terminalEventReached;
|
|
457
502
|
cancelled = true;
|
|
@@ -5,8 +5,12 @@
|
|
|
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 { renderSkillsSelection } from "./skills-selection.js";
|
|
12
|
+
import { supportedBy } from "./support.js";
|
|
13
|
+
import { renderToolSelection } from "./tool-selection.js";
|
|
10
14
|
import { renderTurnOptions } from "./turn-options.js";
|
|
11
15
|
import { validateModel } from "./vocabulary.js";
|
|
12
16
|
|
|
@@ -50,6 +54,12 @@ export interface DiscoveryOptions {
|
|
|
50
54
|
export interface TurnOptions {
|
|
51
55
|
readonly prompt: string;
|
|
52
56
|
readonly tools?: readonly string[];
|
|
57
|
+
readonly excludeTools?: readonly string[];
|
|
58
|
+
/** Caller-directed skills allowlist: resolved absolute paths, one per
|
|
59
|
+
* skill. Rendering: pi loads each via --skill with discovery off;
|
|
60
|
+
* claude turns off the complement via skillOverrides settings; codex
|
|
61
|
+
* and muse refuse (structural - no per-skill surface). */
|
|
62
|
+
readonly skills?: readonly string[];
|
|
53
63
|
readonly model?: string;
|
|
54
64
|
readonly autonomy?: boolean;
|
|
55
65
|
readonly effort?: string;
|
|
@@ -90,32 +100,26 @@ const turnTail = (h: HarnessDescriptor, opts: TurnOptions): string[] => {
|
|
|
90
100
|
}
|
|
91
101
|
if (opts.autonomy === true) {
|
|
92
102
|
if (h.autonomy === null) {
|
|
103
|
+
const by = supportedBy(defaultDescriptors(), "autonomy");
|
|
93
104
|
throw new ArgvRefusalError({
|
|
94
105
|
issue: "no-autonomy-mode",
|
|
95
106
|
harness: h.name,
|
|
96
|
-
supported:
|
|
107
|
+
supported: by.map((e) => `${e.harness} ${e.spelling}`),
|
|
108
|
+
supportedBy: by,
|
|
109
|
+
hint: "pi has no unattended-run flag; approximate with a per-tool allowlist (--tools read,bash) if you need unattended behavior on pi",
|
|
97
110
|
});
|
|
98
111
|
}
|
|
99
112
|
tail.push(h.autonomy.flag);
|
|
100
113
|
}
|
|
101
|
-
if (opts.tools !== undefined) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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(","));
|
|
114
|
+
if (opts.tools !== undefined || opts.excludeTools !== undefined) {
|
|
115
|
+
const rendered = renderToolSelection(h, {
|
|
116
|
+
include: opts.tools,
|
|
117
|
+
exclude: opts.excludeTools,
|
|
118
|
+
});
|
|
119
|
+
tail.push(...rendered.tokens);
|
|
120
|
+
}
|
|
121
|
+
if (opts.skills !== undefined && opts.skills.length > 0) {
|
|
122
|
+
tail.push(...renderSkillsSelection(h, opts.skills));
|
|
119
123
|
}
|
|
120
124
|
return tail;
|
|
121
125
|
};
|