@dungle-scrubs/harness-cli-normalizer 0.4.0 → 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/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +21 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +8 -0
- package/dist/cli/config.js.map +1 -1
- 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/run.d.ts.map +1 -1
- package/dist/cli/run.js +42 -2
- 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 +4 -1
- package/dist/execution/failure.d.ts.map +1 -1
- package/dist/execution/failure.js +15 -2
- package/dist/execution/failure.js.map +1 -1
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +16 -3
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/interpretation/argv.d.ts +5 -0
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +4 -0
- package/dist/interpretation/argv.js.map +1 -1
- package/dist/interpretation/refusal.d.ts +1 -1
- package/dist/interpretation/refusal.d.ts.map +1 -1
- package/dist/interpretation/refusal.js.map +1 -1
- package/dist/interpretation/resolve-options.d.ts.map +1 -1
- package/dist/interpretation/resolve-options.js +27 -5
- package/dist/interpretation/resolve-options.js.map +1 -1
- 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/knowledge/claude-code.d.ts.map +1 -1
- package/dist/knowledge/claude-code.js +1 -0
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +1 -0
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +7 -0
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/muse.d.ts.map +1 -1
- package/dist/knowledge/muse.js +1 -0
- package/dist/knowledge/muse.js.map +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +1 -0
- package/dist/knowledge/pi.js.map +1 -1
- package/dist/knowledge/profile.d.ts +3 -0
- package/dist/knowledge/profile.d.ts.map +1 -1
- package/dist/knowledge/profile.js +20 -0
- package/dist/knowledge/profile.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/args.ts +33 -2
- package/src/cli/config.ts +10 -0
- package/src/cli/help.ts +17 -1
- package/src/cli/run.ts +46 -5
- package/src/cli/skills-root.ts +47 -0
- package/src/execution/failure.ts +16 -2
- package/src/execution/stream-turn.ts +17 -2
- package/src/interpretation/argv.ts +9 -0
- package/src/interpretation/refusal.ts +1 -0
- package/src/interpretation/resolve-options.ts +29 -5
- package/src/interpretation/skills-selection.ts +73 -0
- package/src/knowledge/claude-code.ts +1 -0
- package/src/knowledge/codex.ts +1 -0
- package/src/knowledge/descriptor.ts +7 -0
- package/src/knowledge/muse.ts +1 -0
- package/src/knowledge/pi.ts +1 -0
- package/src/knowledge/profile.ts +20 -0
|
@@ -8,6 +8,9 @@ export declare const DEFAULT_TURN_PROFILE: {
|
|
|
8
8
|
readonly skills: true;
|
|
9
9
|
};
|
|
10
10
|
readonly autonomy: false;
|
|
11
|
+
readonly write: true;
|
|
12
|
+
readonly shell: true;
|
|
13
|
+
readonly tools: "all-known";
|
|
11
14
|
};
|
|
12
15
|
export type ProfileKey = keyof typeof DEFAULT_TURN_PROFILE;
|
|
13
16
|
//# sourceMappingURL=profile.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/knowledge/profile.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,oBAAoB;qBACvB,QAAQ;sBAKP,iBAAiB;;iBAMb,KAAK;iBAAQ,gBAAgB;iBAAQ,UAAU;iBAAQ,MAAM
|
|
1
|
+
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/knowledge/profile.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,oBAAoB;qBACvB,QAAQ;sBAKP,iBAAiB;;iBAMb,KAAK;iBAAQ,gBAAgB;iBAAQ,UAAU;iBAAQ,MAAM;;;;;oBAenE,WAAW;CAST,CAAC;AAEZ,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,oBAAoB,CAAC"}
|
|
@@ -27,5 +27,25 @@ export const DEFAULT_TURN_PROFILE = deepFreeze({
|
|
|
27
27
|
// Ratified: autonomy OFF. No bare run is unattended; --autonomy or a
|
|
28
28
|
// config must say so deliberately.
|
|
29
29
|
autonomy: false,
|
|
30
|
+
// D9/D10, ratified round 2: write and shell ON - names current
|
|
31
|
+
// behavior (emit-nothing on claude/codex/pi; muse omits its disable
|
|
32
|
+
// flags). Completes the "what can a bare run do to the machine" row of
|
|
33
|
+
// provenance.
|
|
34
|
+
write: true,
|
|
35
|
+
shell: true,
|
|
36
|
+
// D13, ratified: equivalent-as-possible tool defaults. The marker
|
|
37
|
+
// expands per descriptor at resolve time - pi gains its dormant
|
|
38
|
+
// grep/find/ls; claude emits nothing (already everything); codex/muse
|
|
39
|
+
// report divergence (no list surface). Project floors narrow it by
|
|
40
|
+
// the normal precedence chain.
|
|
41
|
+
tools: "all-known",
|
|
42
|
+
// Model is PERMANENTLY out of profile scope (ratified round 2): no
|
|
43
|
+
// cross-harness model namespace exists, pi's registry is
|
|
44
|
+
// runtime-extensible and environment-dependent, and per-harness config
|
|
45
|
+
// already covers "claude on X, pi on Y". Do not add a model entry.
|
|
46
|
+
// timeout and maxSteps are likewise opt-in-only dimensions (D11/D12):
|
|
47
|
+
// no harness ships a wall-clock cap, one prompt expands into an
|
|
48
|
+
// unbounded turn loop, and a fixed default kills legitimate work. They
|
|
49
|
+
// live in args/config only, never here.
|
|
30
50
|
});
|
|
31
51
|
//# sourceMappingURL=profile.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile.js","sourceRoot":"","sources":["../../src/knowledge/profile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,MAAM,CAAC,MAAM,oBAAoB,GAAG,UAAU,CAAC;IAC7C,MAAM,EAAE,QAAQ;IAChB,uEAAuE;IACvE,iEAAiE;IACjE,qEAAqE;IACrE,yBAAyB;IACzB,OAAO,EAAE,iBAAiB;IAC1B,iEAAiE;IACjE,wEAAwE;IACxE,8DAA8D;IAC9D,wEAAwE;IACxE,QAAQ;IACR,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;IAClF,qEAAqE;IACrE,mCAAmC;IACnC,QAAQ,EAAE,KAAK;
|
|
1
|
+
{"version":3,"file":"profile.js","sourceRoot":"","sources":["../../src/knowledge/profile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,MAAM,CAAC,MAAM,oBAAoB,GAAG,UAAU,CAAC;IAC7C,MAAM,EAAE,QAAQ;IAChB,uEAAuE;IACvE,iEAAiE;IACjE,qEAAqE;IACrE,yBAAyB;IACzB,OAAO,EAAE,iBAAiB;IAC1B,iEAAiE;IACjE,wEAAwE;IACxE,8DAA8D;IAC9D,wEAAwE;IACxE,QAAQ;IACR,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;IAClF,qEAAqE;IACrE,mCAAmC;IACnC,QAAQ,EAAE,KAAK;IACf,+DAA+D;IAC/D,oEAAoE;IACpE,uEAAuE;IACvE,cAAc;IACd,KAAK,EAAE,IAAI;IACX,KAAK,EAAE,IAAI;IACX,kEAAkE;IAClE,gEAAgE;IAChE,sEAAsE;IACtE,mEAAmE;IACnE,+BAA+B;IAC/B,KAAK,EAAE,WAAW;IAClB,mEAAmE;IACnE,yDAAyD;IACzD,uEAAuE;IACvE,mEAAmE;IACnE,sEAAsE;IACtE,gEAAgE;IAChE,uEAAuE;IACvE,wCAAwC;CAChC,CAAC,CAAC"}
|
package/package.json
CHANGED
package/src/cli/args.ts
CHANGED
|
@@ -166,6 +166,11 @@ export const parseTurnOptions = (values: Record<string, unknown>): TurnOptions =
|
|
|
166
166
|
const raw = String(values["exclude-tools"]);
|
|
167
167
|
opts.excludeTools = raw.length === 0 ? [] : raw.split(",").map((s) => s.trim());
|
|
168
168
|
}
|
|
169
|
+
if (values.skills !== undefined) {
|
|
170
|
+
const raw = String(values.skills);
|
|
171
|
+
(opts as Record<string, unknown>).skills =
|
|
172
|
+
raw.length === 0 ? [] : raw.split(",").map((s) => s.trim());
|
|
173
|
+
}
|
|
169
174
|
if (values.autonomy === true) opts.autonomy = true;
|
|
170
175
|
else if (values["no-autonomy"] === true) opts.autonomy = false;
|
|
171
176
|
else if (values.autonomy === false) opts.autonomy = false; // for completeness
|
|
@@ -216,8 +221,31 @@ export const parseTurnOptions = (values: Record<string, unknown>): TurnOptions =
|
|
|
216
221
|
|
|
217
222
|
export const parseRunExtra = (
|
|
218
223
|
values: Record<string, unknown>,
|
|
219
|
-
): {
|
|
220
|
-
|
|
224
|
+
): {
|
|
225
|
+
cwd?: string;
|
|
226
|
+
env?: Record<string, string>;
|
|
227
|
+
resume?: string;
|
|
228
|
+
timeoutSeconds?: number;
|
|
229
|
+
} => {
|
|
230
|
+
const extra: {
|
|
231
|
+
cwd?: string;
|
|
232
|
+
env?: Record<string, string>;
|
|
233
|
+
resume?: string;
|
|
234
|
+
timeoutSeconds?: number;
|
|
235
|
+
} = {};
|
|
236
|
+
if (values.timeout !== undefined) {
|
|
237
|
+
const n = Number(values.timeout);
|
|
238
|
+
if (!Number.isFinite(n) || n < 0 || !Number.isInteger(n)) {
|
|
239
|
+
throw new ArgvRefusalError({
|
|
240
|
+
issue: "invalid-option-value",
|
|
241
|
+
harness: "claude",
|
|
242
|
+
option: "maxSteps",
|
|
243
|
+
supported: ["whole seconds, >= 0 (0 disables)"],
|
|
244
|
+
detail: String(values.timeout),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
extra.timeoutSeconds = n;
|
|
248
|
+
}
|
|
221
249
|
if (values.cwd !== undefined) extra.cwd = String(values.cwd);
|
|
222
250
|
if (values.resume !== undefined) extra.resume = String(values.resume);
|
|
223
251
|
if (values["session-id"] !== undefined) extra.resume = String(values["session-id"]);
|
|
@@ -280,6 +308,7 @@ const FLAGS_WITH_VALUE = new Set([
|
|
|
280
308
|
"--env",
|
|
281
309
|
"--resume",
|
|
282
310
|
"--session-id",
|
|
311
|
+
"--timeout",
|
|
283
312
|
]);
|
|
284
313
|
|
|
285
314
|
export const detectPositionalPromptInjection = (argv: string[]): string | null => {
|
|
@@ -385,6 +414,7 @@ export const parseCommonFlags = (
|
|
|
385
414
|
provider: { type: "string" as const },
|
|
386
415
|
tools: { type: "string" as const },
|
|
387
416
|
"exclude-tools": { type: "string" as const },
|
|
417
|
+
skills: { type: "string" as const },
|
|
388
418
|
autonomy: { type: "boolean" as const },
|
|
389
419
|
"no-autonomy": { type: "boolean" as const },
|
|
390
420
|
write: { type: "boolean" as const },
|
|
@@ -400,6 +430,7 @@ export const parseCommonFlags = (
|
|
|
400
430
|
env: { type: "string" as const, multiple: true },
|
|
401
431
|
resume: { type: "string" as const },
|
|
402
432
|
"session-id": { type: "string" as const },
|
|
433
|
+
timeout: { type: "string" as const },
|
|
403
434
|
json: { type: "boolean" as const },
|
|
404
435
|
argv: { type: "boolean" as const },
|
|
405
436
|
help: { type: "boolean" as const },
|
package/src/cli/config.ts
CHANGED
|
@@ -65,6 +65,7 @@ const KNOWN_KEYS = new Set([
|
|
|
65
65
|
"shell",
|
|
66
66
|
"maxSteps",
|
|
67
67
|
"toolsets",
|
|
68
|
+
"timeout",
|
|
68
69
|
]);
|
|
69
70
|
|
|
70
71
|
const LIST_KEYS = new Set(["tools", "excludeTools"]);
|
|
@@ -115,6 +116,15 @@ export const parseUserConfig = (text: string): Partial<TurnOptions> => {
|
|
|
115
116
|
(out as Record<string, unknown>)[key] = value;
|
|
116
117
|
continue;
|
|
117
118
|
}
|
|
119
|
+
if (key === "timeout") {
|
|
120
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
121
|
+
throw new ConfigError(
|
|
122
|
+
'config key "timeout" must be a whole number of seconds, >= 0 (0 disables)',
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
(out as Record<string, unknown>)[key] = value;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
118
128
|
if (LIST_KEYS.has(key)) {
|
|
119
129
|
if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
|
|
120
130
|
throw new ConfigError(`config key ${JSON.stringify(key)} must be an array of strings`);
|
package/src/cli/help.ts
CHANGED
|
@@ -32,7 +32,10 @@ Options:
|
|
|
32
32
|
--effort <value> Effort level (validated per harness/model)
|
|
33
33
|
--sandbox <value> Sandbox mode (codex only)
|
|
34
34
|
--provider <value> Provider (pi only)
|
|
35
|
-
--tools <a,b> Tool
|
|
35
|
+
--tools <a,b> Tool grant allowlist (claude, pi; a bare name
|
|
36
|
+
matching a configured toolset expands to it)
|
|
37
|
+
--exclude-tools <a,b> Complement over known tools (claude, pi;
|
|
38
|
+
mutually exclusive with --tools)
|
|
36
39
|
--autonomy Enable autonomy flag (claude/codex/muse)
|
|
37
40
|
--no-autonomy Disable autonomy
|
|
38
41
|
--write Enable write (muse)
|
|
@@ -40,6 +43,8 @@ Options:
|
|
|
40
43
|
--shell Enable shell (muse)
|
|
41
44
|
--no-shell Disable shell
|
|
42
45
|
--max-steps <n> Max steps (muse, 1-10000)
|
|
46
|
+
--timeout <seconds> Wall-clock budget for the run (all harnesses,
|
|
47
|
+
hcn-enforced; 0 disables; no default)
|
|
43
48
|
--no-tools Disable tools discovery facet
|
|
44
49
|
--no-instruction-files Disable instructionFiles discovery facet
|
|
45
50
|
--no-extensions Disable extensions discovery facet
|
|
@@ -47,9 +52,20 @@ Options:
|
|
|
47
52
|
--cwd <path> Working directory for spawn
|
|
48
53
|
--env KEY=VAL Environment (repeatable; KEY= deletes)
|
|
49
54
|
--resume <uuid> Resume session id (UUID)
|
|
55
|
+
-- Passthrough: native harness args verbatim
|
|
56
|
+
(failures surface as labeled native errors)
|
|
50
57
|
--json NDJSON HarnessEvent to stdout
|
|
51
58
|
-h, --help Show help
|
|
52
59
|
-V, --version Show version
|
|
60
|
+
|
|
61
|
+
Defaults with no flags:
|
|
62
|
+
Every launch resolves args > project config (.hcn/config.json at the
|
|
63
|
+
git root) > user config (~/.config/hcn/config.json) > built-in profile
|
|
64
|
+
> harness default. The profile pins: effort medium, sandbox
|
|
65
|
+
workspace-write (codex only; other harnesses report divergence),
|
|
66
|
+
discovery on, autonomy off, write/shell on. timeout and max-steps have
|
|
67
|
+
no default. Provenance prints to stderr on every run; see
|
|
68
|
+
'hcn inspect <harness>' for the resolved argv of a bare run.
|
|
53
69
|
`;
|
|
54
70
|
|
|
55
71
|
export const SESSION_HELP = `hcn session - Interactive session (Claude-only)
|
package/src/cli/run.ts
CHANGED
|
@@ -186,16 +186,42 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
186
186
|
|
|
187
187
|
const isExplicit = promptSource !== "positional";
|
|
188
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
|
+
|
|
189
214
|
// Defaults profile + user config: LAUNCH-ONLY. A resumed session keeps
|
|
190
215
|
// its own settings; the resolver never runs on resume paths.
|
|
191
216
|
let resolvedProvenance: readonly ProvenanceEntry[] = [];
|
|
192
217
|
let resolvedUnrenderable: readonly string[] = [];
|
|
193
218
|
let effectiveTurnOpts: ReturnType<typeof parseTurnOptions> = turnOpts;
|
|
219
|
+
const resolvedTiers: {
|
|
220
|
+
user?: Partial<ReturnType<typeof parseTurnOptions>>;
|
|
221
|
+
project?: Partial<ReturnType<typeof parseTurnOptions>>;
|
|
222
|
+
} = {};
|
|
194
223
|
if (extra.resume === undefined) {
|
|
195
|
-
const tiers
|
|
196
|
-
user?: Partial<ReturnType<typeof parseTurnOptions>>;
|
|
197
|
-
project?: Partial<ReturnType<typeof parseTurnOptions>>;
|
|
198
|
-
} = {};
|
|
224
|
+
const tiers = resolvedTiers;
|
|
199
225
|
const { loadUserConfig, loadProjectConfig, ConfigError } = await import("./config.js");
|
|
200
226
|
try {
|
|
201
227
|
const loaded = loadUserConfig();
|
|
@@ -277,6 +303,11 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
277
303
|
prompt,
|
|
278
304
|
__explicitPrompt: isExplicit,
|
|
279
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
|
+
}
|
|
280
311
|
}
|
|
281
312
|
_validated = true;
|
|
282
313
|
} catch (err) {
|
|
@@ -311,7 +342,17 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
311
342
|
// Delete HERDR_ENV before spawn
|
|
312
343
|
delete (process.env as Record<string, string | undefined>).HERDR_ENV;
|
|
313
344
|
|
|
314
|
-
|
|
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();
|
|
315
356
|
|
|
316
357
|
// Signal handling
|
|
317
358
|
const _currentProc: { signal: (sig: "SIGTERM" | "SIGKILL") => void } | null = null;
|
|
@@ -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
|
@@ -31,6 +31,7 @@ export const FAILURE_CLASSES = Object.freeze([
|
|
|
31
31
|
"transport",
|
|
32
32
|
"rejected",
|
|
33
33
|
"native",
|
|
34
|
+
"timeout",
|
|
34
35
|
] as const);
|
|
35
36
|
export type FailureClass = (typeof FAILURE_CLASSES)[number];
|
|
36
37
|
|
|
@@ -56,7 +57,7 @@ export interface FailureSummary {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export const retryableOf = (cls: FailureClass): boolean =>
|
|
59
|
-
cls !== "task" && cls !== "budget" && cls !== "rejected" && cls !== "native";
|
|
60
|
+
cls !== "task" && cls !== "budget" && cls !== "rejected" && cls !== "native" && cls !== "timeout";
|
|
60
61
|
|
|
61
62
|
const messageFor = (cls: FailureClass, detail?: string): string => {
|
|
62
63
|
switch (cls) {
|
|
@@ -76,6 +77,11 @@ const messageFor = (cls: FailureClass, detail?: string): string => {
|
|
|
76
77
|
return `Transport failure${detail ? ` (${detail})` : ""} - retry or route to another provider`;
|
|
77
78
|
case "rejected":
|
|
78
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`;
|
|
79
85
|
case "native":
|
|
80
86
|
// D6: labeled NATIVE so it can never be confused with an hcn error.
|
|
81
87
|
// The harness's own message follows verbatim; the process exit code
|
|
@@ -86,7 +92,14 @@ const messageFor = (cls: FailureClass, detail?: string): string => {
|
|
|
86
92
|
default:
|
|
87
93
|
return `Failure${detail ? ` (${detail})` : ""}`;
|
|
88
94
|
}
|
|
89
|
-
}; /**
|
|
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
|
|
90
103
|
* native stderr verbatim and the native exit code as data. */
|
|
91
104
|
export const failureFromNative = (
|
|
92
105
|
nativeExitCode: number | null,
|
|
@@ -173,6 +186,7 @@ const PRECEDENCE: Record<FailureClass, number> = {
|
|
|
173
186
|
budget: 3,
|
|
174
187
|
task: 3,
|
|
175
188
|
transport: 4,
|
|
189
|
+
timeout: 3,
|
|
176
190
|
// rejected stands alone (checked before precedence applies); native is
|
|
177
191
|
// terminal-by-classification, never reduced into anything else.
|
|
178
192
|
rejected: 0,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
failureFromLimit,
|
|
31
31
|
failureFromNative,
|
|
32
32
|
failureFromRejected,
|
|
33
|
+
failureFromTimeout,
|
|
33
34
|
failureFromTransport,
|
|
34
35
|
reduceFailures,
|
|
35
36
|
} from "./failure.js";
|
|
@@ -152,6 +153,13 @@ export async function* streamTurn(
|
|
|
152
153
|
if (opts.passthrough !== undefined && opts.passthrough.length > 0) {
|
|
153
154
|
argv = [...argv, "--", ...opts.passthrough];
|
|
154
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
|
+
}
|
|
155
163
|
granularity = streamingGranularityOf(h, argv);
|
|
156
164
|
} catch (e) {
|
|
157
165
|
if (e instanceof ArgvRefusalError) {
|
|
@@ -434,7 +442,12 @@ export async function* streamTurn(
|
|
|
434
442
|
}
|
|
435
443
|
// Stall watchdog also implies a transport failure if not already present
|
|
436
444
|
if (killedByWatchdog && failures.length === 0) {
|
|
437
|
-
|
|
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"}`);
|
|
438
451
|
failures.push(f);
|
|
439
452
|
yield { kind: "failure", ...f };
|
|
440
453
|
}
|
|
@@ -442,7 +455,9 @@ export async function* streamTurn(
|
|
|
442
455
|
let cause: ExitCause = state.limitSeen
|
|
443
456
|
? "limit"
|
|
444
457
|
: killedByWatchdog && exitCode !== 0
|
|
445
|
-
? "
|
|
458
|
+
? watchdogReason === "turn-deadline"
|
|
459
|
+
? "killed" // D11: the run was killed on budget, not stalled
|
|
460
|
+
: "stall"
|
|
446
461
|
: exitCode === 0
|
|
447
462
|
? "clean"
|
|
448
463
|
: exitCode === null
|
|
@@ -8,6 +8,7 @@ import type { HarnessDescriptor, StreamingGranularity } from "../knowledge/descr
|
|
|
8
8
|
import { defaultDescriptors } from "../knowledge/overrides.js";
|
|
9
9
|
import { ArgvRefusalError } from "./refusal.js";
|
|
10
10
|
import { assertUsableSessionId } from "./session-id.js";
|
|
11
|
+
import { renderSkillsSelection } from "./skills-selection.js";
|
|
11
12
|
import { supportedBy } from "./support.js";
|
|
12
13
|
import { renderToolSelection } from "./tool-selection.js";
|
|
13
14
|
import { renderTurnOptions } from "./turn-options.js";
|
|
@@ -54,6 +55,11 @@ export interface TurnOptions {
|
|
|
54
55
|
readonly prompt: string;
|
|
55
56
|
readonly tools?: readonly string[];
|
|
56
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[];
|
|
57
63
|
readonly model?: string;
|
|
58
64
|
readonly autonomy?: boolean;
|
|
59
65
|
readonly effort?: string;
|
|
@@ -112,6 +118,9 @@ const turnTail = (h: HarnessDescriptor, opts: TurnOptions): string[] => {
|
|
|
112
118
|
});
|
|
113
119
|
tail.push(...rendered.tokens);
|
|
114
120
|
}
|
|
121
|
+
if (opts.skills !== undefined && opts.skills.length > 0) {
|
|
122
|
+
tail.push(...renderSkillsSelection(h, opts.skills));
|
|
123
|
+
}
|
|
115
124
|
return tail;
|
|
116
125
|
};
|
|
117
126
|
|
|
@@ -54,6 +54,9 @@ const EXPRESSIBLE: Readonly<Record<ProfileKey, (h: HarnessDescriptor) => boolean
|
|
|
54
54
|
sandbox: (h) => h.turnOptions.sandbox !== undefined,
|
|
55
55
|
discovery: () => true,
|
|
56
56
|
autonomy: () => true,
|
|
57
|
+
write: () => true,
|
|
58
|
+
shell: () => true,
|
|
59
|
+
tools: (h) => h.tools.includeFlag !== null || h.tools.excludeFlag !== null,
|
|
57
60
|
};
|
|
58
61
|
|
|
59
62
|
export interface ConfigTiers {
|
|
@@ -155,12 +158,33 @@ export const resolveEffectiveOptions = (
|
|
|
155
158
|
provenance.push({ key, value, tier: "harness" });
|
|
156
159
|
continue;
|
|
157
160
|
}
|
|
161
|
+
// D13: the tools marker expands per descriptor. On a harness whose
|
|
162
|
+
// default is already everything (claude), expansion emits nothing -
|
|
163
|
+
// the emit-nothing rule, recorded in provenance. On a harness with
|
|
164
|
+
// dormant built-ins (pi), it becomes the enabling include list.
|
|
165
|
+
if (key === "tools" && value === "all-known") {
|
|
166
|
+
const enabled = h.tools.builtins.filter((t) => t.defaultEnabled).length;
|
|
167
|
+
const all = h.tools.builtins.length;
|
|
168
|
+
if (enabled === all) {
|
|
169
|
+
provenance.push({ key, value: "all known (already default)", tier: "profile" });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const expanded = h.tools.builtins.map((t) => t.name);
|
|
173
|
+
resolved[key] = expanded;
|
|
174
|
+
provenance.push({ key, value: expanded, tier: "profile" });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
158
177
|
// Dimensions whose value reduces to "emit nothing" (autonomy false,
|
|
159
|
-
// discovery all-on) stay ABSENT from the resolved
|
|
160
|
-
// harness's default already satisfies the profile, and
|
|
161
|
-
// explicit on-flags would change resume grammar and add
|
|
162
|
-
// surface for no semantic gain. Provenance still records
|
|
163
|
-
|
|
178
|
+
// discovery all-on, write/shell true) stay ABSENT from the resolved
|
|
179
|
+
// options - the harness's default already satisfies the profile, and
|
|
180
|
+
// emitting explicit on-flags would change resume grammar and add
|
|
181
|
+
// breakage surface for no semantic gain. Provenance still records
|
|
182
|
+
// the tier.
|
|
183
|
+
if (
|
|
184
|
+
(key === "autonomy" && value === false) ||
|
|
185
|
+
(key === "discovery" && emitsNothing(value)) ||
|
|
186
|
+
((key === "write" || key === "shell") && value === true)
|
|
187
|
+
) {
|
|
164
188
|
provenance.push({ key, value, tier: "profile" });
|
|
165
189
|
continue;
|
|
166
190
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caller-directed skills allowlist rendering (issue #38). The delegating
|
|
3
|
+
* agent picks the subset from its own registry; this module turns the
|
|
4
|
+
* resolved paths into per-harness argv tokens.
|
|
5
|
+
*
|
|
6
|
+
* - pi: `-ns` (discovery off) + one `--skill <path>` per entry - the
|
|
7
|
+
* allowlist is exact: only the caller's picks load.
|
|
8
|
+
* - claude: no per-skill load flag; the registry is already present via
|
|
9
|
+
* the personal skills dir, so the allowlist renders as the complement
|
|
10
|
+
* OFF - `--settings '{"skillOverrides":{"<name>":"off",...}}'` for every
|
|
11
|
+
* known skill except the picks. Known set comes from the caller's root
|
|
12
|
+
* listing (same source that resolved the names).
|
|
13
|
+
* - codex/muse: refuse (structural) with the standard hint shape.
|
|
14
|
+
*/
|
|
15
|
+
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
16
|
+
import { ArgvRefusalError } from "./refusal.js";
|
|
17
|
+
|
|
18
|
+
export const basenameOf = (p: string): string => {
|
|
19
|
+
const i = p.lastIndexOf("/");
|
|
20
|
+
return i === -1 ? p : p.slice(i + 1);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const renderSkillsSelection = (
|
|
24
|
+
h: HarnessDescriptor,
|
|
25
|
+
skills: readonly string[],
|
|
26
|
+
): readonly string[] => {
|
|
27
|
+
if (skills.length === 0) return [];
|
|
28
|
+
|
|
29
|
+
if (h.skills === null) {
|
|
30
|
+
throw new ArgvRefusalError({
|
|
31
|
+
issue: "unsupported-option",
|
|
32
|
+
harness: h.name,
|
|
33
|
+
option: "skills",
|
|
34
|
+
supported: ["caller-directed skill sets"],
|
|
35
|
+
supportedBy: [
|
|
36
|
+
{ harness: "pi", spelling: "--skill" },
|
|
37
|
+
{ harness: "claude", spelling: "skillOverrides" },
|
|
38
|
+
],
|
|
39
|
+
hint:
|
|
40
|
+
h.name === "codex"
|
|
41
|
+
? "codex discovers skills from its own directory with no call-time surface - stage the skills into $CODEX_HOME/skills or pass their content in the prompt"
|
|
42
|
+
: "muse scopes skills by workspace trust with no per-skill surface - include the skill content in the prompt or use --trust-workspace for the whole registry",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (h.skills.loadFlag !== null) {
|
|
47
|
+
// pi: discovery off so ONLY the picks load.
|
|
48
|
+
const tokens: string[] = ["-ns"];
|
|
49
|
+
for (const path of skills) tokens.push(h.skills.loadFlag, path);
|
|
50
|
+
return tokens;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// claude: complement-off via settings JSON. Names are the skill dir
|
|
54
|
+
// basenames; unknown names in knownSkills would be turned off
|
|
55
|
+
// pointlessly, so the caller passes exactly the known set.
|
|
56
|
+
return [];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** The claude complement form, given the full known registry: every known
|
|
60
|
+
* name except the picks gets "off". Exported for the CLI layer, which owns
|
|
61
|
+
* the registry listing (an fs read - never in interpretation). */
|
|
62
|
+
export const claudeSkillOverridesArg = (
|
|
63
|
+
knownSkills: readonly string[],
|
|
64
|
+
pickedPaths: readonly string[],
|
|
65
|
+
): string[] => {
|
|
66
|
+
const picks = new Set(pickedPaths.map(basenameOf));
|
|
67
|
+
const offs: Record<string, string> = {};
|
|
68
|
+
for (const name of knownSkills) {
|
|
69
|
+
if (!picks.has(name)) offs[name] = "off";
|
|
70
|
+
}
|
|
71
|
+
const json = JSON.stringify({ skillOverrides: offs });
|
|
72
|
+
return ["--settings", json];
|
|
73
|
+
};
|
|
@@ -161,6 +161,7 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
|
|
|
161
161
|
// compose, deny winning on overlap. Patterns (Bash(git *)) valid in both
|
|
162
162
|
// lists; unknown PATTERN spellings warn on stderr, unknown exact names
|
|
163
163
|
// are the silent-acceptance hazard the curated vocabulary guards.
|
|
164
|
+
skills: { loadFlag: null, overridesVia: "settings-skilloverrides" },
|
|
164
165
|
tools: {
|
|
165
166
|
includeFlag: "--allowedTools",
|
|
166
167
|
excludeFlag: "--disallowedTools",
|
package/src/knowledge/codex.ts
CHANGED
|
@@ -119,6 +119,7 @@ export const codexCli: HarnessDescriptor = deepFreeze({
|
|
|
119
119
|
// (reachable per-call via -c key=value / --enable/--disable FEATURE),
|
|
120
120
|
// sandbox, and approval policy. MCP servers do have per-tool keys
|
|
121
121
|
// (mcp_servers.<id>.tools.<tool>) but built-ins do not.
|
|
122
|
+
skills: null,
|
|
122
123
|
tools: {
|
|
123
124
|
includeFlag: null,
|
|
124
125
|
excludeFlag: null,
|
|
@@ -356,6 +356,13 @@ export interface HarnessDescriptor {
|
|
|
356
356
|
* tools register at runtime and are NEVER enumerated here - name
|
|
357
357
|
* validation is a default, not a refusal authority (same stance as the
|
|
358
358
|
* pi model registry, D-008). */
|
|
359
|
+
/** Caller-directed skills allowlist surface (issue #38). pi: repeatable
|
|
360
|
+
* load flag. claude: per-name "off" overrides via settings JSON.
|
|
361
|
+
* codex/muse: null (structural gap - trust/config scoped only). */
|
|
362
|
+
readonly skills: {
|
|
363
|
+
readonly loadFlag: string | null;
|
|
364
|
+
readonly overridesVia: "settings-skilloverrides" | null;
|
|
365
|
+
} | null;
|
|
359
366
|
readonly tools: {
|
|
360
367
|
readonly includeFlag: string | null;
|
|
361
368
|
readonly excludeFlag: string | null;
|
package/src/knowledge/muse.ts
CHANGED
|
@@ -116,6 +116,7 @@ export const museCode: HarnessDescriptor = deepFreeze({
|
|
|
116
116
|
// Tool names follow muse.<name> (write_file, edit_file, bash, bash_input,
|
|
117
117
|
// add_memory, edit_memory, web_search). --disable-web-tools has no
|
|
118
118
|
// normalized turnOption yet - candidate for a web toggle or passthrough.
|
|
119
|
+
skills: null,
|
|
119
120
|
tools: {
|
|
120
121
|
includeFlag: null,
|
|
121
122
|
excludeFlag: null,
|
package/src/knowledge/pi.ts
CHANGED
|
@@ -122,6 +122,7 @@ export const piCli: HarnessDescriptor = deepFreeze({
|
|
|
122
122
|
// exclude subtracts from include. --tools is strict over BUILT-INS but
|
|
123
123
|
// does not strip MCP/extension registrations (additive over them);
|
|
124
124
|
// -nbt (built-ins only off) exists but has no normalized spelling yet.
|
|
125
|
+
skills: { loadFlag: "--skill", overridesVia: null },
|
|
125
126
|
tools: {
|
|
126
127
|
includeFlag: "--tools",
|
|
127
128
|
excludeFlag: "--exclude-tools",
|