@dungle-scrubs/harness-cli-normalizer 0.4.5 → 0.5.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 +53 -7
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +13 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +65 -0
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/help.d.ts +2 -2
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +28 -7
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/inspect.d.ts.map +1 -1
- package/dist/cli/inspect.js +81 -25
- package/dist/cli/inspect.js.map +1 -1
- package/dist/cli/provenance.d.ts +3 -0
- package/dist/cli/provenance.d.ts.map +1 -0
- package/dist/cli/provenance.js +11 -0
- package/dist/cli/provenance.js.map +1 -0
- package/dist/cli/refuse.d.ts +14 -0
- package/dist/cli/refuse.d.ts.map +1 -0
- package/dist/cli/refuse.js +38 -0
- package/dist/cli/refuse.js.map +1 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +27 -68
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/session.d.ts.map +1 -1
- package/dist/cli/session.js +3 -3
- package/dist/cli/session.js.map +1 -1
- package/dist/cli/skills-root.d.ts +2 -1
- package/dist/cli/skills-root.d.ts.map +1 -1
- package/dist/cli/skills-root.js +6 -4
- package/dist/cli/skills-root.js.map +1 -1
- package/dist/execution/decode.d.ts.map +1 -1
- package/dist/execution/decode.js +2 -1
- package/dist/execution/decode.js.map +1 -1
- package/dist/execution/deps.d.ts +1 -1
- package/dist/execution/deps.js +1 -1
- package/dist/execution/failure.d.ts +5 -1
- package/dist/execution/failure.d.ts.map +1 -1
- package/dist/execution/failure.js +12 -0
- package/dist/execution/failure.js.map +1 -1
- package/dist/execution/index.d.ts +2 -2
- package/dist/execution/index.js +2 -2
- package/dist/execution/open-session.d.ts.map +1 -1
- package/dist/execution/open-session.js +23 -8
- package/dist/execution/open-session.js.map +1 -1
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +18 -10
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/interpretation/argv.d.ts +3 -0
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +6 -1
- package/dist/interpretation/argv.js.map +1 -1
- package/dist/interpretation/content.d.ts.map +1 -1
- package/dist/interpretation/content.js +4 -1
- package/dist/interpretation/content.js.map +1 -1
- package/dist/interpretation/hints.d.ts.map +1 -1
- package/dist/interpretation/hints.js +4 -0
- package/dist/interpretation/hints.js.map +1 -1
- package/dist/interpretation/limits.d.ts +1 -0
- package/dist/interpretation/limits.d.ts.map +1 -1
- package/dist/interpretation/limits.js +22 -0
- package/dist/interpretation/limits.js.map +1 -1
- package/dist/interpretation/resolve-options.d.ts.map +1 -1
- package/dist/interpretation/resolve-options.js +124 -3
- package/dist/interpretation/resolve-options.js.map +1 -1
- package/dist/interpretation/support.d.ts +1 -0
- package/dist/interpretation/support.d.ts.map +1 -1
- package/dist/interpretation/support.js +29 -0
- package/dist/interpretation/support.js.map +1 -1
- package/dist/interpretation/tool-selection.d.ts +6 -16
- package/dist/interpretation/tool-selection.d.ts.map +1 -1
- package/dist/interpretation/tool-selection.js +216 -80
- package/dist/interpretation/tool-selection.js.map +1 -1
- package/dist/interpretation/tool-vocabulary.d.ts +38 -0
- package/dist/interpretation/tool-vocabulary.d.ts.map +1 -0
- package/dist/interpretation/tool-vocabulary.js +142 -0
- package/dist/interpretation/tool-vocabulary.js.map +1 -0
- package/dist/interpretation/turn-options.d.ts.map +1 -1
- package/dist/interpretation/turn-options.js +107 -2
- package/dist/interpretation/turn-options.js.map +1 -1
- package/dist/interpretation/versions.d.ts +1 -1
- package/dist/interpretation/versions.d.ts.map +1 -1
- package/dist/interpretation/vocabulary.d.ts +1 -0
- package/dist/interpretation/vocabulary.d.ts.map +1 -1
- package/dist/interpretation/vocabulary.js +5 -0
- package/dist/interpretation/vocabulary.js.map +1 -1
- package/dist/knowledge/claude-code.d.ts.map +1 -1
- package/dist/knowledge/claude-code.js +14 -13
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +10 -4
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +20 -1
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/descriptor.js +18 -1
- package/dist/knowledge/descriptor.js.map +1 -1
- package/dist/knowledge/index.d.ts +1 -1
- package/dist/knowledge/index.d.ts.map +1 -1
- package/dist/knowledge/index.js +1 -1
- package/dist/knowledge/index.js.map +1 -1
- package/dist/knowledge/matchers.d.ts +2 -1
- package/dist/knowledge/matchers.d.ts.map +1 -1
- package/dist/knowledge/matchers.js +15 -0
- package/dist/knowledge/matchers.js.map +1 -1
- package/dist/knowledge/muse.d.ts.map +1 -1
- package/dist/knowledge/muse.js +18 -3
- package/dist/knowledge/muse.js.map +1 -1
- package/dist/knowledge/overrides.d.ts.map +1 -1
- package/dist/knowledge/overrides.js +3 -2
- package/dist/knowledge/overrides.js.map +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +12 -8
- package/dist/knowledge/pi.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/args.ts +13 -0
- package/src/cli/config.ts +88 -0
- package/src/cli/help.ts +28 -7
- package/src/cli/inspect.ts +78 -21
- package/src/cli/provenance.ts +19 -0
- package/src/cli/refuse.ts +50 -0
- package/src/cli/run.ts +27 -94
- package/src/cli/session.ts +4 -3
- package/src/cli/skills-root.ts +11 -4
- package/src/execution/decode.ts +2 -1
- package/src/execution/deps.ts +1 -1
- package/src/execution/failure.ts +17 -1
- package/src/execution/index.ts +2 -2
- package/src/execution/open-session.ts +33 -7
- package/src/execution/stream-turn.ts +21 -9
- package/src/interpretation/argv.ts +9 -1
- package/src/interpretation/content.ts +5 -1
- package/src/interpretation/hints.ts +8 -0
- package/src/interpretation/limits.ts +29 -0
- package/src/interpretation/resolve-options.ts +129 -3
- package/src/interpretation/support.ts +30 -0
- package/src/interpretation/tool-selection.ts +236 -96
- package/src/interpretation/tool-vocabulary.ts +183 -0
- package/src/interpretation/turn-options.ts +99 -2
- package/src/interpretation/versions.ts +1 -1
- package/src/interpretation/vocabulary.ts +5 -0
- package/src/knowledge/claude-code.ts +14 -13
- package/src/knowledge/codex.ts +10 -4
- package/src/knowledge/descriptor.ts +42 -2
- package/src/knowledge/index.ts +5 -1
- package/src/knowledge/matchers.ts +17 -1
- package/src/knowledge/muse.ts +18 -3
- package/src/knowledge/overrides.ts +4 -2
- package/src/knowledge/pi.ts +12 -8
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ProvenanceEntry } from "../interpretation/resolve-options.js";
|
|
2
|
+
|
|
3
|
+
export const writeProvenance = (
|
|
4
|
+
harnessName: string,
|
|
5
|
+
provenance: readonly ProvenanceEntry[],
|
|
6
|
+
unrenderable: readonly string[],
|
|
7
|
+
): void => {
|
|
8
|
+
if (provenance.length === 0 && unrenderable.length === 0) return;
|
|
9
|
+
for (const entry of provenance) {
|
|
10
|
+
process.stderr.write(
|
|
11
|
+
`provenance: ${entry.key} = ${JSON.stringify(entry.value)} (${entry.tier})\n`,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
for (const key of unrenderable) {
|
|
15
|
+
process.stderr.write(
|
|
16
|
+
`divergence: profile ${JSON.stringify(key)} not expressible on ${harnessName}; harness default applies\n`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { failureFromRejected } from "../execution/failure.js";
|
|
2
|
+
import type { ArgvRefusalError, RefusalIssue } from "../interpretation/refusal.js";
|
|
3
|
+
import { writeEventNdjson } from "./render.js";
|
|
4
|
+
|
|
5
|
+
export interface Refusal {
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly issue: RefusalIssue;
|
|
8
|
+
readonly option?: ArgvRefusalError["option"];
|
|
9
|
+
readonly facet?: ArgvRefusalError["facet"];
|
|
10
|
+
readonly supported?: readonly string[];
|
|
11
|
+
readonly supportedBy?: ArgvRefusalError["supportedBy"];
|
|
12
|
+
readonly hint?: string;
|
|
13
|
+
readonly trailer?: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const refusalOf = (err: ArgvRefusalError): Refusal => ({
|
|
17
|
+
message: err.message,
|
|
18
|
+
issue: err.issue,
|
|
19
|
+
option: err.option,
|
|
20
|
+
facet: err.facet,
|
|
21
|
+
supported: err.supported,
|
|
22
|
+
supportedBy: err.supportedBy,
|
|
23
|
+
hint: err.hint,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const refuse = (r: Refusal, json: boolean): void => {
|
|
27
|
+
process.stderr.write(`${r.message}\n`);
|
|
28
|
+
if (r.hint) process.stderr.write(`hint: ${r.hint}\n`);
|
|
29
|
+
if (r.supportedBy?.length) {
|
|
30
|
+
process.stderr.write(
|
|
31
|
+
`supported on: ${r.supportedBy.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (r.supported?.length) process.stderr.write(`supported: ${r.supported.join(", ")}\n`);
|
|
35
|
+
for (const line of r.trailer ?? []) process.stderr.write(`${line}\n`);
|
|
36
|
+
if (json) {
|
|
37
|
+
const summary = failureFromRejected({
|
|
38
|
+
issue: r.issue,
|
|
39
|
+
option: r.option,
|
|
40
|
+
facet: r.facet,
|
|
41
|
+
supported: r.supported,
|
|
42
|
+
supportedBy: r.supportedBy,
|
|
43
|
+
hint: r.hint,
|
|
44
|
+
detail: r.message,
|
|
45
|
+
});
|
|
46
|
+
writeEventNdjson({ kind: "failure", ...summary });
|
|
47
|
+
writeEventNdjson({ kind: "done", exitCode: null, cause: "failed", failure: summary });
|
|
48
|
+
}
|
|
49
|
+
process.exitCode = 2;
|
|
50
|
+
};
|
package/src/cli/run.ts
CHANGED
|
@@ -1,72 +1,19 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import type { HarnessEvent } from "../execution/events.js";
|
|
3
|
-
import { failureFromRejected } from "../execution/failure.js";
|
|
4
3
|
import { nodeRunnerDeps } from "../execution/node-deps.js";
|
|
5
4
|
import { KILL_GRACE_MS, redactArgv, streamTurn } from "../execution/stream-turn.js";
|
|
6
5
|
import { buildLaunchArgv, buildResumeArgv } from "../interpretation/argv.js";
|
|
7
6
|
import { composeEscalatedPrompt } from "../interpretation/question.js";
|
|
8
|
-
import type { RefusalIssue } from "../interpretation/refusal.js";
|
|
9
7
|
import { ArgvRefusalError } from "../interpretation/refusal.js";
|
|
10
8
|
import { FloorExceededError, resolveEffectiveOptions } from "../interpretation/resolve-options.js";
|
|
11
9
|
import { storePath } from "../interpretation/store.js";
|
|
12
10
|
import { recognizeNativeSpelling, supportedBy } from "../interpretation/support.js";
|
|
13
11
|
import { defaultDescriptors } from "../knowledge/overrides.js";
|
|
14
12
|
import { parseRunExtra, parseTurnOptions, resolvePromptAsync } from "./args.js";
|
|
13
|
+
import { refusalOf, refuse } from "./refuse.js";
|
|
15
14
|
import { createRenderState, renderEvent, writeEventNdjson } from "./render.js";
|
|
16
15
|
import { resolveHarness } from "./resolve-harness.js";
|
|
17
16
|
|
|
18
|
-
/** One shape for every hcn-side refusal. Stderr keeps the prose (message,
|
|
19
|
-
* hint, support lists, usage pointer); under --json the same fields also
|
|
20
|
-
* ride stdout as a `failure` event plus a `done`, so a stream reader never
|
|
21
|
-
* meets an empty stream on exit 2. */
|
|
22
|
-
interface Refusal {
|
|
23
|
-
readonly message: string;
|
|
24
|
-
readonly issue: RefusalIssue;
|
|
25
|
-
readonly option?: ArgvRefusalError["option"];
|
|
26
|
-
readonly facet?: ArgvRefusalError["facet"];
|
|
27
|
-
readonly supported?: readonly string[];
|
|
28
|
-
readonly supportedBy?: ArgvRefusalError["supportedBy"];
|
|
29
|
-
readonly hint?: string;
|
|
30
|
-
/** Plain stderr lines written after the structured ones. */
|
|
31
|
-
readonly trailer?: readonly string[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const refusalOf = (err: ArgvRefusalError): Refusal => ({
|
|
35
|
-
message: err.message,
|
|
36
|
-
issue: err.issue,
|
|
37
|
-
option: err.option,
|
|
38
|
-
facet: err.facet,
|
|
39
|
-
supported: err.supported,
|
|
40
|
-
supportedBy: err.supportedBy,
|
|
41
|
-
hint: err.hint,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const refuse = (r: Refusal, json: boolean): void => {
|
|
45
|
-
process.stderr.write(`${r.message}\n`);
|
|
46
|
-
if (r.hint) process.stderr.write(`hint: ${r.hint}\n`);
|
|
47
|
-
if (r.supportedBy?.length) {
|
|
48
|
-
process.stderr.write(
|
|
49
|
-
`supported on: ${r.supportedBy.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
if (r.supported?.length) process.stderr.write(`supported: ${r.supported.join(", ")}\n`);
|
|
53
|
-
for (const line of r.trailer ?? []) process.stderr.write(`${line}\n`);
|
|
54
|
-
if (json) {
|
|
55
|
-
const summary = failureFromRejected({
|
|
56
|
-
issue: r.issue,
|
|
57
|
-
option: r.option,
|
|
58
|
-
facet: r.facet,
|
|
59
|
-
supported: r.supported,
|
|
60
|
-
supportedBy: r.supportedBy,
|
|
61
|
-
hint: r.hint,
|
|
62
|
-
detail: r.message,
|
|
63
|
-
});
|
|
64
|
-
writeEventNdjson({ kind: "failure", ...summary });
|
|
65
|
-
writeEventNdjson({ kind: "done", exitCode: null, cause: "failed", failure: summary });
|
|
66
|
-
}
|
|
67
|
-
process.exitCode = 2;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
17
|
export const run = async (harnessName: string, rawArgs: string[]): Promise<void> => {
|
|
71
18
|
const h = resolveHarness(harnessName);
|
|
72
19
|
|
|
@@ -238,7 +185,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
238
185
|
if (rawSkills !== undefined && rawSkills.length > 0) {
|
|
239
186
|
try {
|
|
240
187
|
const { resolveSkillNames, listKnownSkills } = await import("./skills-root.js");
|
|
241
|
-
const resolvedSkills = resolveSkillNames(rawSkills);
|
|
188
|
+
const resolvedSkills = resolveSkillNames(rawSkills, h.name);
|
|
242
189
|
const claudeTokens: string[] = [];
|
|
243
190
|
if (h.name === "claude") {
|
|
244
191
|
const { claudeSkillOverridesArg } = await import("../interpretation/skills-selection.js");
|
|
@@ -295,6 +242,10 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
295
242
|
refuse({ message: resErr.message, issue: "invalid-tool-grant" }, wantJson);
|
|
296
243
|
return;
|
|
297
244
|
}
|
|
245
|
+
if (resErr instanceof ArgvRefusalError) {
|
|
246
|
+
refuse(refusalOf(resErr), wantJson);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
298
249
|
throw resErr;
|
|
299
250
|
}
|
|
300
251
|
const { provenance, unrenderable } = resolved;
|
|
@@ -302,30 +253,8 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
302
253
|
effectiveTurnOpts = rest as ReturnType<typeof parseTurnOptions>;
|
|
303
254
|
// Provenance is diagnostic data like the spawn line - stderr in BOTH
|
|
304
255
|
// render modes, never stdout (stdout carries the NDJSON contract).
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
process.stderr.write(
|
|
308
|
-
`provenance: ${entry.key} = ${JSON.stringify(entry.value)} (${entry.tier})\n`,
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
for (const key of unrenderable) {
|
|
312
|
-
process.stderr.write(
|
|
313
|
-
`divergence: profile ${JSON.stringify(key)} not expressible on ${h.name}; harness default applies\n`,
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
// issue #48 (ratified): when claude's instruction-files isolation
|
|
317
|
-
// rides (the --setting-sources project spelling), surface its known
|
|
318
|
-
// cost as a divergence line - legal to use, priced in stderr.
|
|
319
|
-
if (
|
|
320
|
-
h.name === "claude" &&
|
|
321
|
-
(resolved.options as { discovery?: { instructionFiles?: boolean } }).discovery
|
|
322
|
-
?.instructionFiles === false
|
|
323
|
-
) {
|
|
324
|
-
process.stderr.write(
|
|
325
|
-
"divergence: setting-sources isolation also skips hooks, LSP and keychain reads on claude - weighed, not refused\n",
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
256
|
+
const { writeProvenance } = await import("./provenance.js");
|
|
257
|
+
writeProvenance(h.name, provenance, unrenderable);
|
|
329
258
|
}
|
|
330
259
|
|
|
331
260
|
// issue #41: question-escalation precedence arg > project > user >
|
|
@@ -430,12 +359,12 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
430
359
|
// all refuses inside buildLaunchArgv below, so a throw here is left
|
|
431
360
|
// to that path.
|
|
432
361
|
const grant = (fullOpts.resume ? turnOpts : effectiveTurnOpts).tools;
|
|
433
|
-
if (grant !== undefined && grant.length > 0
|
|
362
|
+
if (grant !== undefined && grant.length > 0) {
|
|
434
363
|
try {
|
|
435
364
|
const { renderToolSelection } = await import("../interpretation/tool-selection.js");
|
|
436
|
-
const {
|
|
437
|
-
if (
|
|
438
|
-
process.stderr.write(`provenance:
|
|
365
|
+
const { passthrough } = renderToolSelection(h, { include: [...grant] });
|
|
366
|
+
if (passthrough.length > 0) {
|
|
367
|
+
process.stderr.write(`provenance: native tools = ${JSON.stringify(passthrough)}\n`);
|
|
439
368
|
}
|
|
440
369
|
} catch {
|
|
441
370
|
// refused below with the structured message
|
|
@@ -490,7 +419,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
490
419
|
const _currentProc: { signal: (sig: "SIGTERM" | "SIGKILL") => void } | null = null;
|
|
491
420
|
// We'll need to track the spawned process via deps.signal; but streamTurn owns process handle.
|
|
492
421
|
// Instead we intercept deps.signal via a wrapper that captures proc.
|
|
493
|
-
// Simpler: use deps directly and handle SIGINT
|
|
422
|
+
// Simpler: use deps directly and handle SIGINT via injected signal.
|
|
494
423
|
// We'll create a wrapper deps where signal captures last proc.
|
|
495
424
|
let lastProc: import("../execution/deps.js").SpawnedProcess | null = null;
|
|
496
425
|
const originalSignal = deps.signal;
|
|
@@ -509,6 +438,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
509
438
|
|
|
510
439
|
const abortController = new AbortController();
|
|
511
440
|
let interrupted = false;
|
|
441
|
+
let escalationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
512
442
|
const onSig = async () => {
|
|
513
443
|
if (interrupted) return;
|
|
514
444
|
interrupted = true;
|
|
@@ -516,16 +446,15 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
516
446
|
if (lastProc) {
|
|
517
447
|
try {
|
|
518
448
|
wrappedDeps.signal(lastProc, "SIGTERM");
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
});
|
|
449
|
+
escalationTimer = setTimeout(() => {
|
|
450
|
+
if (lastProc) {
|
|
451
|
+
try {
|
|
452
|
+
wrappedDeps.signal(lastProc, "SIGKILL");
|
|
453
|
+
} catch {}
|
|
454
|
+
}
|
|
455
|
+
escalationTimer = null;
|
|
456
|
+
}, KILL_GRACE_MS);
|
|
457
|
+
escalationTimer.unref?.();
|
|
529
458
|
} catch {}
|
|
530
459
|
}
|
|
531
460
|
};
|
|
@@ -591,6 +520,10 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
591
520
|
process.off("SIGTERM", onSig);
|
|
592
521
|
return;
|
|
593
522
|
} finally {
|
|
523
|
+
if (escalationTimer !== null) {
|
|
524
|
+
clearTimeout(escalationTimer);
|
|
525
|
+
escalationTimer = null;
|
|
526
|
+
}
|
|
594
527
|
process.off("SIGINT", onSig);
|
|
595
528
|
process.off("SIGTERM", onSig);
|
|
596
529
|
}
|
package/src/cli/session.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { createInterface } from "node:readline/promises";
|
|
|
3
3
|
import { nodeRunnerDeps } from "../execution/node-deps.js";
|
|
4
4
|
import { CLOSE_GRACE_MS, openSession } from "../execution/open-session.js";
|
|
5
5
|
import { ArgvRefusalError } from "../interpretation/refusal.js";
|
|
6
|
+
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
7
|
+
import { defaultDescriptors } from "../knowledge/overrides.js";
|
|
6
8
|
import { createRenderState, renderEvent } from "./render.js";
|
|
7
9
|
import { resolveHarness } from "./resolve-harness.js";
|
|
8
10
|
|
|
@@ -12,9 +14,8 @@ export const session = async (harnessName: string, rawArgs: string[]): Promise<v
|
|
|
12
14
|
// session mode is available the moment its descriptor declares one.
|
|
13
15
|
const h = resolveHarness(harnessName);
|
|
14
16
|
if (h.sessionMode === null) {
|
|
15
|
-
const supported =
|
|
16
|
-
.
|
|
17
|
-
.filter((d) => d.sessionMode !== null)
|
|
17
|
+
const supported = Object.values(defaultDescriptors())
|
|
18
|
+
.filter((d): d is HarnessDescriptor => d !== undefined && d.sessionMode !== null)
|
|
18
19
|
.map((d) => d.name);
|
|
19
20
|
const err = new ArgvRefusalError({
|
|
20
21
|
issue: "no-session-mode",
|
package/src/cli/skills-root.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { existsSync, readdirSync, statSync } from "node:fs";
|
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { ArgvRefusalError } from "../interpretation/refusal.js";
|
|
15
|
+
import type { HarnessName } from "../knowledge/descriptor.js";
|
|
15
16
|
|
|
16
17
|
export const skillsRoot = (): string =>
|
|
17
18
|
process.env.HCN_SKILLS_ROOT ?? join(homedir(), ".agents", "skills");
|
|
@@ -30,16 +31,22 @@ export const listKnownSkills = (): string[] => {
|
|
|
30
31
|
});
|
|
31
32
|
};
|
|
32
33
|
|
|
33
|
-
export const resolveSkillNames = (
|
|
34
|
+
export const resolveSkillNames = (
|
|
35
|
+
names: readonly string[],
|
|
36
|
+
harness: HarnessName = "claude",
|
|
37
|
+
): string[] => {
|
|
34
38
|
const root = skillsRoot();
|
|
35
39
|
const known = new Set(listKnownSkills());
|
|
36
40
|
const unknown = names.filter((n) => !known.has(n));
|
|
37
41
|
if (unknown.length > 0) {
|
|
38
42
|
throw new ArgvRefusalError({
|
|
39
|
-
issue: "
|
|
40
|
-
harness
|
|
43
|
+
issue: "invalid-option-value",
|
|
44
|
+
harness,
|
|
41
45
|
option: "skills",
|
|
42
|
-
supported:
|
|
46
|
+
supported:
|
|
47
|
+
known.size > 0
|
|
48
|
+
? [`registry at ${root}`, [...known].join(", ")]
|
|
49
|
+
: [`registry at ${root} (empty)`],
|
|
43
50
|
detail: `unknown skill name(s): ${unknown.join(", ")}`,
|
|
44
51
|
});
|
|
45
52
|
}
|
package/src/execution/decode.ts
CHANGED
|
@@ -69,10 +69,11 @@ export const decodeParsed = (
|
|
|
69
69
|
const decoded = decodeIdentity(h, raw, state.lastSeenId, state.requestedId);
|
|
70
70
|
if (decoded.sessionId !== null) state.lastSeenId = decoded.sessionId;
|
|
71
71
|
if (decoded.identity !== null) {
|
|
72
|
+
const authority = state.requestedId !== null ? "caller-assigned" : "harness-minted";
|
|
72
73
|
events.push({
|
|
73
74
|
kind: "identity",
|
|
74
75
|
sessionId: decoded.identity,
|
|
75
|
-
authority
|
|
76
|
+
authority,
|
|
76
77
|
capabilities: capabilities(),
|
|
77
78
|
});
|
|
78
79
|
} else if (decoded.outcome === "malformed" || decoded.outcome === "rotated") {
|
package/src/execution/deps.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Injected runtime primitives (D-005): the execution layer never imports
|
|
3
|
-
* child_process, never
|
|
3
|
+
* child_process, never signals the process directly, and never reads a wall clock -
|
|
4
4
|
* spawn, clock, and signalling arrive as data so Node/Bun is a real
|
|
5
5
|
* portability boundary and tests run against fakes deterministically.
|
|
6
6
|
*/
|
package/src/execution/failure.ts
CHANGED
|
@@ -13,8 +13,14 @@
|
|
|
13
13
|
* options or a different harness, not a different model.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { detectAuthFailureInLine, detectTransportInLine } from "../interpretation/limits.js";
|
|
16
17
|
import type { RefusalIssue } from "../interpretation/refusal.js";
|
|
17
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
AuthFailureKind,
|
|
20
|
+
DiscoveryFacet,
|
|
21
|
+
HarnessDescriptor,
|
|
22
|
+
LimitCode,
|
|
23
|
+
} from "../knowledge/descriptor.js";
|
|
18
24
|
|
|
19
25
|
export const FAILURE_CLASSES = Object.freeze([
|
|
20
26
|
"rate-limit",
|
|
@@ -131,6 +137,16 @@ export const failureFromAuth = (kind: AuthFailureKind): FailureSummary => ({
|
|
|
131
137
|
authKind: kind,
|
|
132
138
|
});
|
|
133
139
|
|
|
140
|
+
/** A terminal error the harness reported on its stream: classify by what
|
|
141
|
+
* it says. An auth wall or a transport fault reached no verdict on the
|
|
142
|
+
* work (retryable); anything else is the model's own failure (task). */
|
|
143
|
+
export const failureFromTerminalError = (h: HarnessDescriptor, message: string): FailureSummary => {
|
|
144
|
+
const auth = detectAuthFailureInLine(h, message);
|
|
145
|
+
if (auth !== null) return failureFromAuth(auth);
|
|
146
|
+
if (detectTransportInLine(message)) return failureFromTransport(message);
|
|
147
|
+
return failureFromTask(message);
|
|
148
|
+
};
|
|
149
|
+
|
|
134
150
|
export const failureFromTask = (detail?: string): FailureSummary => ({
|
|
135
151
|
class: "task",
|
|
136
152
|
retryable: false,
|
package/src/execution/index.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* It exposes openSession() and streamTurn(), which drive a CLI process via
|
|
5
5
|
* injected {spawn, clock, signal} primitives and emit HarnessEvent values.
|
|
6
6
|
* This module knows nothing about any chat protocol. Because it must run
|
|
7
|
-
* identically on Node and Bun, it never imports child_process and never
|
|
8
|
-
* process
|
|
7
|
+
* identically on Node and Bun, it never imports child_process and never
|
|
8
|
+
* signals the process directly outside the node-deps adapter - all process I/O and
|
|
9
9
|
* signalling flows through the injected primitives.
|
|
10
10
|
*/
|
|
11
11
|
export { AsyncChannel } from "./channel.js";
|
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { buildSessionArgv } from "../interpretation/argv.js";
|
|
17
17
|
import { capabilitiesOf } from "../interpretation/capabilities.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
detectAuthFailureInLine,
|
|
20
|
+
detectLimitInLine,
|
|
21
|
+
detectTransportInLine,
|
|
22
|
+
} from "../interpretation/limits.js";
|
|
19
23
|
import { composeEscalatedPrompt, detectQuestionBlock } from "../interpretation/question.js";
|
|
20
24
|
import {
|
|
21
25
|
encodeSessionInput,
|
|
@@ -28,7 +32,14 @@ import { decodeParsed, freshDecodeState } from "./decode.js";
|
|
|
28
32
|
import type { RunnerDeps, SpawnedProcess } from "./deps.js";
|
|
29
33
|
import type { ExitCause, HarnessEvent } from "./events.js";
|
|
30
34
|
import type { FailureSummary } from "./failure.js";
|
|
31
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
failureFromAuth,
|
|
37
|
+
failureFromLimit,
|
|
38
|
+
failureFromTask,
|
|
39
|
+
failureFromTerminalError,
|
|
40
|
+
failureFromTransport,
|
|
41
|
+
reduceFailures,
|
|
42
|
+
} from "./failure.js";
|
|
32
43
|
import { LineBuffer } from "./lines.js";
|
|
33
44
|
import { KILL_GRACE_MS, PIPE_GRACE_MS, redactArgv, StderrTail } from "./stream-turn.js";
|
|
34
45
|
|
|
@@ -210,6 +221,9 @@ export const openSession = (
|
|
|
210
221
|
if (detection === null) return;
|
|
211
222
|
if ("malformed" in detection) {
|
|
212
223
|
activeTurn?.push({ kind: "error", message: detection.malformed });
|
|
224
|
+
const failure = failureFromTask(`malformed hcn-question block: ${detection.malformed}`);
|
|
225
|
+
turnFailures.push(failure);
|
|
226
|
+
void activeTurn?.push({ kind: "failure", ...failure });
|
|
213
227
|
return;
|
|
214
228
|
}
|
|
215
229
|
turnAsked = true;
|
|
@@ -240,7 +254,10 @@ export const openSession = (
|
|
|
240
254
|
// Every failure was already emitted as an event through pushFailure;
|
|
241
255
|
// the turn's done carries the reduced summary, as streamTurn's does.
|
|
242
256
|
const reduced = reduceFailures(turnFailures);
|
|
243
|
-
if (reduced !== undefined)
|
|
257
|
+
if (reduced !== undefined) {
|
|
258
|
+
if (done.cause === "clean") done = { ...done, cause: "failed", failure: reduced };
|
|
259
|
+
else done = { ...done, failure: reduced };
|
|
260
|
+
}
|
|
244
261
|
activeTurn.push(done);
|
|
245
262
|
activeTurn.close();
|
|
246
263
|
log({
|
|
@@ -287,6 +304,15 @@ export const openSession = (
|
|
|
287
304
|
return routeEvent({ kind: "failure", ...f });
|
|
288
305
|
};
|
|
289
306
|
|
|
307
|
+
/** A decoded event other than a failure: a terminal error also records
|
|
308
|
+
* the failure it stands for, the way streamTurn does. */
|
|
309
|
+
const routeDecoded = async (event: HarnessEvent): Promise<void> => {
|
|
310
|
+
await routeEvent(event);
|
|
311
|
+
if (event.kind === "error" && event.terminal === true) {
|
|
312
|
+
await pushFailure(failureFromTerminalError(h, event.message));
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
290
316
|
const pumpStdout = async (): Promise<void> => {
|
|
291
317
|
const lines = new LineBuffer();
|
|
292
318
|
const matches = (
|
|
@@ -351,7 +377,7 @@ export const openSession = (
|
|
|
351
377
|
await routeEvent({
|
|
352
378
|
kind: "identity",
|
|
353
379
|
sessionId: announced,
|
|
354
|
-
authority:
|
|
380
|
+
authority: "harness-minted",
|
|
355
381
|
capabilities: capabilitiesOf(h, opts.model ?? "", "headless-session"),
|
|
356
382
|
});
|
|
357
383
|
}
|
|
@@ -361,7 +387,7 @@ export const openSession = (
|
|
|
361
387
|
await routeEvent({
|
|
362
388
|
kind: "identity",
|
|
363
389
|
sessionId: announced,
|
|
364
|
-
authority:
|
|
390
|
+
authority: "caller-assigned",
|
|
365
391
|
capabilities: capabilitiesOf(h, opts.model ?? "", "headless-session"),
|
|
366
392
|
});
|
|
367
393
|
}
|
|
@@ -392,7 +418,7 @@ export const openSession = (
|
|
|
392
418
|
// still track resultError here to classify the done cause.
|
|
393
419
|
for (const event of events) {
|
|
394
420
|
if (event.kind === "failure") await pushFailure(summaryOf(event));
|
|
395
|
-
else await
|
|
421
|
+
else await routeDecoded(event);
|
|
396
422
|
}
|
|
397
423
|
if (parsed.is_error === true) resultError = true;
|
|
398
424
|
endTurn({
|
|
@@ -404,7 +430,7 @@ export const openSession = (
|
|
|
404
430
|
}
|
|
405
431
|
for (const event of events) {
|
|
406
432
|
if (event.kind === "failure") await pushFailure(summaryOf(event));
|
|
407
|
-
else await
|
|
433
|
+
else await routeDecoded(event);
|
|
408
434
|
}
|
|
409
435
|
};
|
|
410
436
|
for await (const chunk of proc.stdout) {
|
|
@@ -16,7 +16,11 @@ import {
|
|
|
16
16
|
streamingGranularityOf,
|
|
17
17
|
} from "../interpretation/argv.js";
|
|
18
18
|
import { stdinPolicyOf } from "../interpretation/dimensions.js";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
detectAuthFailureInLine,
|
|
21
|
+
detectLimitInLine,
|
|
22
|
+
detectTransportInLine,
|
|
23
|
+
} from "../interpretation/limits.js";
|
|
20
24
|
import { composeEscalatedPrompt, detectQuestionBlock } from "../interpretation/question.js";
|
|
21
25
|
import { ArgvRefusalError } from "../interpretation/refusal.js";
|
|
22
26
|
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
@@ -32,6 +36,7 @@ import {
|
|
|
32
36
|
failureFromNative,
|
|
33
37
|
failureFromRejected,
|
|
34
38
|
failureFromTask,
|
|
39
|
+
failureFromTerminalError,
|
|
35
40
|
failureFromTimeout,
|
|
36
41
|
failureFromTransport,
|
|
37
42
|
reduceFailures,
|
|
@@ -312,6 +317,9 @@ export async function* streamTurn(
|
|
|
312
317
|
|
|
313
318
|
const failures: FailureSummary[] = [];
|
|
314
319
|
const pushFailure = async (f: FailureSummary): Promise<void> => {
|
|
320
|
+
// Suppress a failure identical in class and message to the previous one
|
|
321
|
+
const prev = failures[failures.length - 1];
|
|
322
|
+
if (prev !== undefined && prev.class === f.class && prev.message === f.message) return;
|
|
315
323
|
failures.push(f);
|
|
316
324
|
await queue.push({ kind: "failure", ...f });
|
|
317
325
|
};
|
|
@@ -426,7 +434,7 @@ export async function* streamTurn(
|
|
|
426
434
|
}
|
|
427
435
|
if (event.kind === "error") {
|
|
428
436
|
await queue.push(event);
|
|
429
|
-
if (event.terminal === true) await pushFailure(
|
|
437
|
+
if (event.terminal === true) await pushFailure(failureFromTerminalError(h, event.message));
|
|
430
438
|
return;
|
|
431
439
|
}
|
|
432
440
|
if (escalateQuestions && event.kind === "message" && event.role === "assistant") {
|
|
@@ -468,6 +476,7 @@ export async function* streamTurn(
|
|
|
468
476
|
if (detection === null) return;
|
|
469
477
|
if ("malformed" in detection) {
|
|
470
478
|
await queue.push({ kind: "error", message: detection.malformed });
|
|
479
|
+
await pushFailure(failureFromTask(`malformed hcn-question block: ${detection.malformed}`));
|
|
471
480
|
return;
|
|
472
481
|
}
|
|
473
482
|
log({
|
|
@@ -585,10 +594,13 @@ export async function* streamTurn(
|
|
|
585
594
|
!state.limitSeen
|
|
586
595
|
) {
|
|
587
596
|
const tailForNative = stderrTail.snapshot();
|
|
597
|
+
const transportLine = tailForNative.find((line) => detectTransportInLine(line));
|
|
588
598
|
const f =
|
|
589
|
-
|
|
590
|
-
?
|
|
591
|
-
:
|
|
599
|
+
transportLine !== undefined
|
|
600
|
+
? failureFromTransport(transportLine)
|
|
601
|
+
: tailForNative.length > 0
|
|
602
|
+
? failureFromNative(exitCode, tailForNative)
|
|
603
|
+
: failureFromTransport(`nonzero exit ${exitCode}`);
|
|
592
604
|
failures.push(f);
|
|
593
605
|
// Need to emit this failure before done, even though queue is closed
|
|
594
606
|
yield { kind: "failure", ...f };
|
|
@@ -624,10 +636,10 @@ export async function* streamTurn(
|
|
|
624
636
|
: "crash";
|
|
625
637
|
const reduced = reduceFailures(failures);
|
|
626
638
|
if (reduced && cause === "clean") cause = "failed";
|
|
627
|
-
// A
|
|
628
|
-
// turn
|
|
629
|
-
//
|
|
630
|
-
if (
|
|
639
|
+
// A classified failure other than native on a nonzero exit is a failed
|
|
640
|
+
// turn, not a crash: the failure taxonomy already captured the reason.
|
|
641
|
+
// Crash stays for unclassified exits and native failures.
|
|
642
|
+
if (reduced && reduced.class !== "native" && cause === "crash") {
|
|
631
643
|
cause = "failed";
|
|
632
644
|
}
|
|
633
645
|
const tail = stderrTail.snapshot();
|
|
@@ -83,6 +83,9 @@ export interface TurnOptions {
|
|
|
83
83
|
readonly escalateQuestions?: boolean;
|
|
84
84
|
/** Internal: set by CLI when prompt came from --prompt/--prompt-file to bypass leading '-' guard */
|
|
85
85
|
readonly __explicitPrompt?: boolean;
|
|
86
|
+
/** toolMap extensible vocabulary per harness (issue toolMap) */
|
|
87
|
+
readonly toolMap?: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
88
|
+
readonly access?: "read" | "write";
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
export interface ResumeOptions extends TurnOptions {
|
|
@@ -124,9 +127,11 @@ const turnTail = (h: HarnessDescriptor, opts: TurnOptions): string[] => {
|
|
|
124
127
|
tail.push(h.autonomy.flag);
|
|
125
128
|
}
|
|
126
129
|
if (opts.tools !== undefined || opts.excludeTools !== undefined) {
|
|
130
|
+
const perHarnessMap = opts.toolMap?.[h.name];
|
|
127
131
|
const rendered = renderToolSelection(h, {
|
|
128
132
|
include: opts.tools,
|
|
129
133
|
exclude: opts.excludeTools,
|
|
134
|
+
toolMap: perHarnessMap,
|
|
130
135
|
});
|
|
131
136
|
tail.push(...rendered.tokens);
|
|
132
137
|
}
|
|
@@ -188,10 +193,13 @@ export interface SessionOptions {
|
|
|
188
193
|
|
|
189
194
|
export const buildSessionArgv = (h: HarnessDescriptor, opts: SessionOptions): string[] => {
|
|
190
195
|
if (!h.sessionMode) {
|
|
196
|
+
const supported = Object.values(defaultDescriptors())
|
|
197
|
+
.filter((d): d is HarnessDescriptor => d !== undefined && d.sessionMode !== null)
|
|
198
|
+
.map((d) => d.name);
|
|
191
199
|
throw new ArgvRefusalError({
|
|
192
200
|
issue: "no-session-mode",
|
|
193
201
|
harness: h.name,
|
|
194
|
-
supported
|
|
202
|
+
supported,
|
|
195
203
|
});
|
|
196
204
|
}
|
|
197
205
|
refuseUnusableSessionId(h, opts.sessionId);
|
|
@@ -130,10 +130,14 @@ const pi = (r: Record<string, unknown>): ContentEvent[] => {
|
|
|
130
130
|
// an expired minimax token: empty content, stopReason error, clean
|
|
131
131
|
// exit). Without this the failure is invisible - a silent empty turn.
|
|
132
132
|
if (message.stopReason === "error") {
|
|
133
|
+
const msg =
|
|
134
|
+
typeof message.errorMessage === "string"
|
|
135
|
+
? `pi turn ended with stopReason error: ${message.errorMessage}`
|
|
136
|
+
: "pi turn ended with stopReason error (provider/auth failure)";
|
|
133
137
|
return [
|
|
134
138
|
{
|
|
135
139
|
kind: "error",
|
|
136
|
-
message:
|
|
140
|
+
message: msg,
|
|
137
141
|
terminal: true,
|
|
138
142
|
},
|
|
139
143
|
];
|
|
@@ -27,6 +27,10 @@ const HINTS: Readonly<Record<string, Readonly<Record<string, string>>>> = deepFr
|
|
|
27
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
28
|
},
|
|
29
29
|
codex: {
|
|
30
|
+
tools:
|
|
31
|
+
"codex has no per-tool name lists; use --sandbox read-only for a read-only grant or category switches via config keys (features.shell_tool, web_search)",
|
|
32
|
+
excludeTools:
|
|
33
|
+
"codex has no per-tool name lists; use category switches via config keys (features.shell_tool) or --sandbox modes",
|
|
30
34
|
write:
|
|
31
35
|
"codex has no write toggle; use --sandbox read-only (config: sandbox_mode) so shell commands cannot write either",
|
|
32
36
|
// issue #48: the append half has no codex spelling.
|
|
@@ -58,6 +62,10 @@ const HINTS: Readonly<Record<string, Readonly<Record<string, string>>>> = deepFr
|
|
|
58
62
|
"pi has no step cap flag; bound the work in the prompt or impose a wall-clock timeout at the caller",
|
|
59
63
|
},
|
|
60
64
|
muse: {
|
|
65
|
+
tools:
|
|
66
|
+
"muse has no per-tool name lists; gate execution with --disable-write, --disable-shell, --disable-web-tools - there is no allowlist",
|
|
67
|
+
excludeTools:
|
|
68
|
+
"muse has no per-tool name lists; gate execution with --disable-write, --disable-shell, --disable-web-tools category switches",
|
|
61
69
|
sandbox:
|
|
62
70
|
"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",
|
|
63
71
|
provider:
|