@dungle-scrubs/harness-cli-normalizer 0.4.5 → 0.4.6
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 +10 -4
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +8 -0
- package/dist/cli/args.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 +3 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/inspect.d.ts.map +1 -1
- package/dist/cli/inspect.js +18 -1
- 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/run.d.ts.map +1 -1
- package/dist/cli/run.js +18 -28
- 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.map +1 -1
- package/dist/interpretation/argv.js +4 -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/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/tool-selection.d.ts.map +1 -1
- package/dist/interpretation/tool-selection.js +16 -0
- package/dist/interpretation/tool-selection.js.map +1 -1
- package/dist/interpretation/versions.d.ts +1 -1
- package/dist/interpretation/versions.d.ts.map +1 -1
- package/dist/knowledge/descriptor.d.ts +4 -0
- package/dist/knowledge/descriptor.d.ts.map +1 -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/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +4 -1
- package/dist/knowledge/pi.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/args.ts +8 -0
- package/src/cli/help.ts +3 -0
- package/src/cli/inspect.ts +17 -1
- package/src/cli/provenance.ts +19 -0
- package/src/cli/run.ts +18 -36
- 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 +4 -1
- package/src/interpretation/content.ts +5 -1
- package/src/interpretation/limits.ts +29 -0
- package/src/interpretation/tool-selection.ts +16 -0
- package/src/interpretation/versions.ts +1 -1
- package/src/knowledge/descriptor.ts +5 -0
- package/src/knowledge/index.ts +5 -1
- package/src/knowledge/matchers.ts +17 -1
- package/src/knowledge/pi.ts +4 -1
package/src/cli/run.ts
CHANGED
|
@@ -238,7 +238,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
238
238
|
if (rawSkills !== undefined && rawSkills.length > 0) {
|
|
239
239
|
try {
|
|
240
240
|
const { resolveSkillNames, listKnownSkills } = await import("./skills-root.js");
|
|
241
|
-
const resolvedSkills = resolveSkillNames(rawSkills);
|
|
241
|
+
const resolvedSkills = resolveSkillNames(rawSkills, h.name);
|
|
242
242
|
const claudeTokens: string[] = [];
|
|
243
243
|
if (h.name === "claude") {
|
|
244
244
|
const { claudeSkillOverridesArg } = await import("../interpretation/skills-selection.js");
|
|
@@ -302,30 +302,8 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
302
302
|
effectiveTurnOpts = rest as ReturnType<typeof parseTurnOptions>;
|
|
303
303
|
// Provenance is diagnostic data like the spawn line - stderr in BOTH
|
|
304
304
|
// 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
|
-
}
|
|
305
|
+
const { writeProvenance } = await import("./provenance.js");
|
|
306
|
+
writeProvenance(h.name, provenance, unrenderable);
|
|
329
307
|
}
|
|
330
308
|
|
|
331
309
|
// issue #41: question-escalation precedence arg > project > user >
|
|
@@ -490,7 +468,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
490
468
|
const _currentProc: { signal: (sig: "SIGTERM" | "SIGKILL") => void } | null = null;
|
|
491
469
|
// We'll need to track the spawned process via deps.signal; but streamTurn owns process handle.
|
|
492
470
|
// Instead we intercept deps.signal via a wrapper that captures proc.
|
|
493
|
-
// Simpler: use deps directly and handle SIGINT
|
|
471
|
+
// Simpler: use deps directly and handle SIGINT via injected signal.
|
|
494
472
|
// We'll create a wrapper deps where signal captures last proc.
|
|
495
473
|
let lastProc: import("../execution/deps.js").SpawnedProcess | null = null;
|
|
496
474
|
const originalSignal = deps.signal;
|
|
@@ -509,6 +487,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
509
487
|
|
|
510
488
|
const abortController = new AbortController();
|
|
511
489
|
let interrupted = false;
|
|
490
|
+
let escalationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
512
491
|
const onSig = async () => {
|
|
513
492
|
if (interrupted) return;
|
|
514
493
|
interrupted = true;
|
|
@@ -516,16 +495,15 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
516
495
|
if (lastProc) {
|
|
517
496
|
try {
|
|
518
497
|
wrappedDeps.signal(lastProc, "SIGTERM");
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
});
|
|
498
|
+
escalationTimer = setTimeout(() => {
|
|
499
|
+
if (lastProc) {
|
|
500
|
+
try {
|
|
501
|
+
wrappedDeps.signal(lastProc, "SIGKILL");
|
|
502
|
+
} catch {}
|
|
503
|
+
}
|
|
504
|
+
escalationTimer = null;
|
|
505
|
+
}, KILL_GRACE_MS);
|
|
506
|
+
escalationTimer.unref?.();
|
|
529
507
|
} catch {}
|
|
530
508
|
}
|
|
531
509
|
};
|
|
@@ -591,6 +569,10 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
591
569
|
process.off("SIGTERM", onSig);
|
|
592
570
|
return;
|
|
593
571
|
} finally {
|
|
572
|
+
if (escalationTimer !== null) {
|
|
573
|
+
clearTimeout(escalationTimer);
|
|
574
|
+
escalationTimer = null;
|
|
575
|
+
}
|
|
594
576
|
process.off("SIGINT", onSig);
|
|
595
577
|
process.off("SIGTERM", onSig);
|
|
596
578
|
}
|
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();
|
|
@@ -188,10 +188,13 @@ export interface SessionOptions {
|
|
|
188
188
|
|
|
189
189
|
export const buildSessionArgv = (h: HarnessDescriptor, opts: SessionOptions): string[] => {
|
|
190
190
|
if (!h.sessionMode) {
|
|
191
|
+
const supported = Object.values(defaultDescriptors())
|
|
192
|
+
.filter((d): d is HarnessDescriptor => d !== undefined && d.sessionMode !== null)
|
|
193
|
+
.map((d) => d.name);
|
|
191
194
|
throw new ArgvRefusalError({
|
|
192
195
|
issue: "no-session-mode",
|
|
193
196
|
harness: h.name,
|
|
194
|
-
supported
|
|
197
|
+
supported,
|
|
195
198
|
});
|
|
196
199
|
}
|
|
197
200
|
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
|
];
|
|
@@ -23,7 +23,9 @@ import type {
|
|
|
23
23
|
HarnessDescriptor,
|
|
24
24
|
LimitCode,
|
|
25
25
|
LimitMatcher,
|
|
26
|
+
TransportMatcher,
|
|
26
27
|
} from "../knowledge/descriptor.js";
|
|
28
|
+
import { SHARED_TRANSPORT_MATCHERS } from "../knowledge/matchers.js";
|
|
27
29
|
|
|
28
30
|
/** Bottom-up batch scans stop after this many non-empty lines: the wall is
|
|
29
31
|
* virtually always the last thing a dying turn printed, and an unbounded
|
|
@@ -152,3 +154,30 @@ export const detectAuthFailureInLine = (
|
|
|
152
154
|
|
|
153
155
|
export const detectAuthFailure = (h: HarnessDescriptor, output: string): AuthFailureKind | null =>
|
|
154
156
|
scanTail(output, compileAuthMatchers(h.authMatchers));
|
|
157
|
+
|
|
158
|
+
const transportCache = new WeakMap<
|
|
159
|
+
ReadonlyArray<TransportMatcher>,
|
|
160
|
+
ReadonlyArray<readonly [RegExp, boolean]>
|
|
161
|
+
>();
|
|
162
|
+
|
|
163
|
+
const compileTransportMatchers = (
|
|
164
|
+
matchers: ReadonlyArray<TransportMatcher>,
|
|
165
|
+
): ReadonlyArray<readonly [RegExp, boolean]> => {
|
|
166
|
+
const cached = transportCache.get(matchers);
|
|
167
|
+
if (cached !== undefined) return cached;
|
|
168
|
+
if (matchers.length > MAX_MATCHERS_PER_KIND) {
|
|
169
|
+
throw new Error(`more than ${MAX_MATCHERS_PER_KIND} matchers per harness per kind`);
|
|
170
|
+
}
|
|
171
|
+
const compiled = matchers.map((m) => [validateAndCompile(m.pattern, m.flags), true] as const);
|
|
172
|
+
transportCache.set(matchers, compiled);
|
|
173
|
+
return compiled;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export const detectTransportInLine = (line: string): boolean => {
|
|
177
|
+
const compiled = compileTransportMatchers(SHARED_TRANSPORT_MATCHERS);
|
|
178
|
+
const windowed = line.slice(0, WINDOW).trim();
|
|
179
|
+
for (const [re] of compiled) {
|
|
180
|
+
if (re.test(windowed)) return true;
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
};
|
|
@@ -107,6 +107,22 @@ export const renderToolSelection = (
|
|
|
107
107
|
|
|
108
108
|
if (hasInclude) {
|
|
109
109
|
const names = validateNames(h, selection.include!);
|
|
110
|
+
if (names.length === 0) {
|
|
111
|
+
// Empty include list = deny everything. On pi --tools "" has no
|
|
112
|
+
// defined meaning, so refuse; on claude emit the deny complement
|
|
113
|
+
// without an empty include token.
|
|
114
|
+
if (h.tools.includeIsStrictAllowlist || h.tools.excludeFlag === null) {
|
|
115
|
+
throw new ArgvRefusalError({
|
|
116
|
+
issue: "invalid-tool-grant",
|
|
117
|
+
harness: h.name,
|
|
118
|
+
option: "tools",
|
|
119
|
+
supported: ["non-empty tool list (empty include would deny everything)"],
|
|
120
|
+
detail: "empty include list",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const known = h.tools.builtins.map((t) => t.name);
|
|
124
|
+
return { tokens: [h.tools.excludeFlag, known.join(",")], unmapped: [] };
|
|
125
|
+
}
|
|
110
126
|
const { mapped, unmapped } = resolveNames(h, names);
|
|
111
127
|
if (!h.tools.includeIsStrictAllowlist) {
|
|
112
128
|
if (mapped.length === 0 && unmapped.length > 0) {
|
|
@@ -71,6 +71,11 @@ export interface AuthMatcher {
|
|
|
71
71
|
readonly kind: AuthFailureKind;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
export interface TransportMatcher {
|
|
75
|
+
readonly pattern: string;
|
|
76
|
+
readonly flags?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
/** The turn-option vocabulary is closed for the same reason `LimitCode` is:
|
|
75
80
|
* a descriptor must not invent an option a consumer has no field for. Every
|
|
76
81
|
* key here maps to a field on `TurnOptions`; adding a key without a consumer
|
package/src/knowledge/index.ts
CHANGED
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
export { claudeCode } from "./claude-code.js";
|
|
13
13
|
export { codexCli } from "./codex.js";
|
|
14
14
|
export * from "./descriptor.js";
|
|
15
|
-
export {
|
|
15
|
+
export {
|
|
16
|
+
SHARED_AUTH_MATCHERS,
|
|
17
|
+
SHARED_LIMIT_MATCHERS,
|
|
18
|
+
SHARED_TRANSPORT_MATCHERS,
|
|
19
|
+
} from "./matchers.js";
|
|
16
20
|
export { museCode } from "./muse.js";
|
|
17
21
|
export {
|
|
18
22
|
type DescriptorSet,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* or a provider 401 must classify no matter which CLI printed it. Each
|
|
5
5
|
* descriptor spreads these after its harness-specific phrasings.
|
|
6
6
|
*/
|
|
7
|
-
import type { AuthMatcher, LimitMatcher } from "./descriptor.js";
|
|
7
|
+
import type { AuthMatcher, LimitMatcher, TransportMatcher } from "./descriptor.js";
|
|
8
8
|
|
|
9
9
|
export const SHARED_LIMIT_MATCHERS: ReadonlyArray<LimitMatcher> = [
|
|
10
10
|
{ pattern: "you'?ve hit your usage limit", flags: "i", code: "usage-limit" },
|
|
@@ -37,3 +37,19 @@ export const SHARED_AUTH_MATCHERS: ReadonlyArray<AuthMatcher> = [
|
|
|
37
37
|
{ pattern: "401 unauthorized", flags: "i", kind: "expired" },
|
|
38
38
|
{ pattern: "invalid api key", flags: "i", kind: "invalid-key" },
|
|
39
39
|
];
|
|
40
|
+
|
|
41
|
+
export const SHARED_TRANSPORT_MATCHERS: ReadonlyArray<TransportMatcher> = [
|
|
42
|
+
{ pattern: "connection error", flags: "i" },
|
|
43
|
+
{ pattern: "ECONNREFUSED", flags: "i" },
|
|
44
|
+
{ pattern: "ECONNRESET", flags: "i" },
|
|
45
|
+
{ pattern: "ENOTFOUND", flags: "i" },
|
|
46
|
+
{ pattern: "EAI_AGAIN", flags: "i" },
|
|
47
|
+
{ pattern: "ETIMEDOUT", flags: "i" },
|
|
48
|
+
{ pattern: "fetch failed", flags: "i" },
|
|
49
|
+
{ pattern: "socket hang up", flags: "i" },
|
|
50
|
+
{ pattern: "network error", flags: "i" },
|
|
51
|
+
{ pattern: "service unavailable", flags: "i" },
|
|
52
|
+
{ pattern: "bad gateway", flags: "i" },
|
|
53
|
+
{ pattern: "gateway time-?out", flags: "i" },
|
|
54
|
+
{ pattern: "\\b(?:HTTP|status(?:[_ ]?code)?|code)\\b\\W*[:=]?\\W*50[234]\\b", flags: "i" },
|
|
55
|
+
];
|
package/src/knowledge/pi.ts
CHANGED
|
@@ -78,7 +78,10 @@ export const piCli: HarnessDescriptor = deepFreeze({
|
|
|
78
78
|
announce: { match: { type: "session" }, idField: "id" },
|
|
79
79
|
},
|
|
80
80
|
limitMatchers: [...SHARED_LIMIT_MATCHERS],
|
|
81
|
-
authMatchers: [
|
|
81
|
+
authMatchers: [
|
|
82
|
+
{ pattern: "No API key found for", flags: "i", kind: "not-logged-in" },
|
|
83
|
+
...SHARED_AUTH_MATCHERS,
|
|
84
|
+
],
|
|
82
85
|
autonomy: null,
|
|
83
86
|
vocabulary: {
|
|
84
87
|
modelFlag: "--model",
|