@dungle-scrubs/harness-cli-normalizer 0.4.4 → 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 +23 -8
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +16 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +4 -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 +10 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/inspect.d.ts.map +1 -1
- package/dist/cli/inspect.js +74 -24
- 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 +135 -85
- 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 +3 -3
- package/dist/execution/decode.d.ts.map +1 -1
- package/dist/execution/decode.js +46 -13
- package/dist/execution/decode.js.map +1 -1
- package/dist/execution/deps.d.ts +4 -1
- package/dist/execution/deps.d.ts.map +1 -1
- package/dist/execution/deps.js +1 -1
- package/dist/execution/events.d.ts +7 -2
- package/dist/execution/events.d.ts.map +1 -1
- package/dist/execution/events.js.map +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/node-deps.d.ts.map +1 -1
- package/dist/execution/node-deps.js +1 -0
- package/dist/execution/node-deps.js.map +1 -1
- package/dist/execution/open-session.d.ts.map +1 -1
- package/dist/execution/open-session.js +54 -8
- package/dist/execution/open-session.js.map +1 -1
- package/dist/execution/stream-turn.d.ts +4 -0
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +147 -33
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/interpretation/argv.d.ts +7 -0
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +28 -5
- package/dist/interpretation/argv.js.map +1 -1
- package/dist/interpretation/capabilities.d.ts.map +1 -1
- package/dist/interpretation/capabilities.js +4 -1
- package/dist/interpretation/capabilities.js.map +1 -1
- package/dist/interpretation/content.d.ts +9 -1
- package/dist/interpretation/content.d.ts.map +1 -1
- package/dist/interpretation/content.js +16 -4
- package/dist/interpretation/content.js.map +1 -1
- package/dist/interpretation/hints.d.ts.map +1 -1
- package/dist/interpretation/hints.js +5 -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 +16 -0
- package/dist/interpretation/resolve-options.js.map +1 -1
- package/dist/interpretation/tool-selection.d.ts.map +1 -1
- package/dist/interpretation/tool-selection.js +25 -0
- package/dist/interpretation/tool-selection.js.map +1 -1
- package/dist/interpretation/turn-options.d.ts.map +1 -1
- package/dist/interpretation/turn-options.js +35 -3
- 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/knowledge/claude-code.d.ts.map +1 -1
- package/dist/knowledge/claude-code.js +21 -0
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +9 -0
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +25 -10
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/descriptor.js +6 -0
- 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 +23 -1
- package/dist/knowledge/matchers.js.map +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +24 -1
- package/dist/knowledge/pi.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/args.ts +15 -0
- package/src/cli/config.ts +4 -0
- package/src/cli/help.ts +10 -0
- package/src/cli/inspect.ts +70 -22
- package/src/cli/provenance.ts +19 -0
- package/src/cli/run.ts +172 -91
- package/src/cli/session.ts +4 -3
- package/src/cli/skills-root.ts +11 -4
- package/src/execution/decode.ts +46 -12
- package/src/execution/deps.ts +4 -1
- package/src/execution/events.ts +5 -2
- package/src/execution/failure.ts +17 -1
- package/src/execution/index.ts +2 -2
- package/src/execution/node-deps.ts +1 -0
- package/src/execution/open-session.ts +63 -6
- package/src/execution/stream-turn.ts +150 -31
- package/src/interpretation/argv.ts +34 -5
- package/src/interpretation/capabilities.ts +4 -1
- package/src/interpretation/content.ts +22 -5
- package/src/interpretation/hints.ts +8 -0
- package/src/interpretation/limits.ts +29 -0
- package/src/interpretation/resolve-options.ts +17 -0
- package/src/interpretation/tool-selection.ts +25 -0
- package/src/interpretation/turn-options.ts +31 -3
- package/src/interpretation/versions.ts +1 -1
- package/src/knowledge/claude-code.ts +21 -0
- package/src/knowledge/codex.ts +9 -0
- package/src/knowledge/descriptor.ts +33 -10
- package/src/knowledge/index.ts +5 -1
- package/src/knowledge/matchers.ts +25 -2
- package/src/knowledge/pi.ts +24 -1
package/src/cli/run.ts
CHANGED
|
@@ -1,16 +1,72 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import type { HarnessEvent } from "../execution/events.js";
|
|
3
|
+
import { failureFromRejected } from "../execution/failure.js";
|
|
2
4
|
import { nodeRunnerDeps } from "../execution/node-deps.js";
|
|
3
5
|
import { KILL_GRACE_MS, redactArgv, streamTurn } from "../execution/stream-turn.js";
|
|
4
6
|
import { buildLaunchArgv, buildResumeArgv } from "../interpretation/argv.js";
|
|
5
7
|
import { composeEscalatedPrompt } from "../interpretation/question.js";
|
|
8
|
+
import type { RefusalIssue } from "../interpretation/refusal.js";
|
|
6
9
|
import { ArgvRefusalError } from "../interpretation/refusal.js";
|
|
7
10
|
import { FloorExceededError, resolveEffectiveOptions } from "../interpretation/resolve-options.js";
|
|
11
|
+
import { storePath } from "../interpretation/store.js";
|
|
8
12
|
import { recognizeNativeSpelling, supportedBy } from "../interpretation/support.js";
|
|
9
13
|
import { defaultDescriptors } from "../knowledge/overrides.js";
|
|
10
14
|
import { parseRunExtra, parseTurnOptions, resolvePromptAsync } from "./args.js";
|
|
11
15
|
import { createRenderState, renderEvent, writeEventNdjson } from "./render.js";
|
|
12
16
|
import { resolveHarness } from "./resolve-harness.js";
|
|
13
17
|
|
|
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
|
+
|
|
14
70
|
export const run = async (harnessName: string, rawArgs: string[]): Promise<void> => {
|
|
15
71
|
const h = resolveHarness(harnessName);
|
|
16
72
|
|
|
@@ -23,7 +79,10 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
23
79
|
const { parseCommonFlags, detectPositionalPromptInjection, splitPassthrough } = await import(
|
|
24
80
|
"./args.js"
|
|
25
81
|
);
|
|
26
|
-
const { passthrough } = splitPassthrough(rawArgs);
|
|
82
|
+
const { normalized, passthrough } = splitPassthrough(rawArgs);
|
|
83
|
+
// Decided before any refusal can fire: a refused --json run still owes
|
|
84
|
+
// the stream a failure and a done.
|
|
85
|
+
const wantJson = normalized.includes("--json");
|
|
27
86
|
const injection = detectPositionalPromptInjection(rawArgs);
|
|
28
87
|
if (injection) {
|
|
29
88
|
const err = new ArgvRefusalError({
|
|
@@ -32,9 +91,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
32
91
|
supported: ["prompt must not start with '-'"],
|
|
33
92
|
detail: `it would be parsed as a flag by ${h.bin}`,
|
|
34
93
|
});
|
|
35
|
-
|
|
36
|
-
process.stderr.write(`supported: prompt must not start with '-'\n`);
|
|
37
|
-
process.exitCode = 2;
|
|
94
|
+
refuse(refusalOf(err), wantJson);
|
|
38
95
|
return;
|
|
39
96
|
}
|
|
40
97
|
let parsed: ReturnType<typeof parseCommonFlags>;
|
|
@@ -77,19 +134,25 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
77
134
|
: native.option.startsWith("discovery.")
|
|
78
135
|
? `--no-${native.option.split(".")[1] === "instructionFiles" ? "instruction-files" : native.option.split(".")[1]}`
|
|
79
136
|
: `--${native.option}`;
|
|
80
|
-
|
|
81
|
-
|
|
137
|
+
refuse(
|
|
138
|
+
{
|
|
139
|
+
message: `unknown flag: ${rawFlag} is a native spelling (used by ${native.entries.map((e) => e.harness).join(", ")}) - use the normalized ${normalizedSpelling} flag instead`,
|
|
140
|
+
issue: "invalid-option-value",
|
|
141
|
+
supportedBy: by,
|
|
142
|
+
trailer: ["Run 'hcn run --help' for usage."],
|
|
143
|
+
},
|
|
144
|
+
wantJson,
|
|
82
145
|
);
|
|
83
|
-
|
|
84
|
-
process.stderr.write(
|
|
85
|
-
`supported on: ${by.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
} else {
|
|
89
|
-
process.stderr.write(`unknown flag: ${message}\n`);
|
|
146
|
+
return;
|
|
90
147
|
}
|
|
91
|
-
|
|
92
|
-
|
|
148
|
+
refuse(
|
|
149
|
+
{
|
|
150
|
+
message: `unknown flag: ${message}`,
|
|
151
|
+
issue: "invalid-option-value",
|
|
152
|
+
trailer: ["Run 'hcn run --help' for usage."],
|
|
153
|
+
},
|
|
154
|
+
wantJson,
|
|
155
|
+
);
|
|
93
156
|
return;
|
|
94
157
|
}
|
|
95
158
|
|
|
@@ -98,18 +161,26 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
98
161
|
// positionals may contain prompt if not using flag; harness already consumed so first positional is prompt
|
|
99
162
|
const positionalPrompt = positionals.length > 0 ? positionals[0] : undefined;
|
|
100
163
|
if (positionals.length > 1) {
|
|
101
|
-
|
|
102
|
-
|
|
164
|
+
refuse(
|
|
165
|
+
{
|
|
166
|
+
message: "too many positionals for run; expected one prompt",
|
|
167
|
+
issue: "invalid-option-value",
|
|
168
|
+
},
|
|
169
|
+
wantJson,
|
|
170
|
+
);
|
|
103
171
|
return;
|
|
104
172
|
}
|
|
105
173
|
if (passthrough.length === 0 && rawArgs.includes("--")) {
|
|
106
|
-
|
|
107
|
-
|
|
174
|
+
refuse(
|
|
175
|
+
{
|
|
176
|
+
message: "-- separator given but no passthrough tokens followed it",
|
|
177
|
+
issue: "invalid-option-value",
|
|
178
|
+
},
|
|
179
|
+
wantJson,
|
|
180
|
+
);
|
|
108
181
|
return;
|
|
109
182
|
}
|
|
110
183
|
|
|
111
|
-
const wantJson = values.json === true;
|
|
112
|
-
|
|
113
184
|
// Resolve prompt (async for --prompt-file -)
|
|
114
185
|
let prompt: string;
|
|
115
186
|
let promptSource: string;
|
|
@@ -123,20 +194,14 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
123
194
|
promptSource = resolved.source;
|
|
124
195
|
} catch (err) {
|
|
125
196
|
if (err instanceof ArgvRefusalError) {
|
|
126
|
-
|
|
127
|
-
if (err.hint) process.stderr.write(`hint: ${err.hint}\n`);
|
|
128
|
-
if (err.supportedBy?.length) {
|
|
129
|
-
process.stderr.write(
|
|
130
|
-
`supported on: ${err.supportedBy.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
134
|
-
process.exitCode = 2;
|
|
197
|
+
refuse(refusalOf(err), wantJson);
|
|
135
198
|
return;
|
|
136
199
|
}
|
|
137
200
|
if (err instanceof Error && (err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
138
|
-
|
|
139
|
-
|
|
201
|
+
refuse(
|
|
202
|
+
{ message: `prompt file not found: ${err.message}`, issue: "invalid-option-value" },
|
|
203
|
+
wantJson,
|
|
204
|
+
);
|
|
140
205
|
return;
|
|
141
206
|
}
|
|
142
207
|
throw err;
|
|
@@ -148,15 +213,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
148
213
|
turnOpts = parseTurnOptions(values);
|
|
149
214
|
} catch (err) {
|
|
150
215
|
if (err instanceof ArgvRefusalError) {
|
|
151
|
-
|
|
152
|
-
if (err.hint) process.stderr.write(`hint: ${err.hint}\n`);
|
|
153
|
-
if (err.supportedBy?.length) {
|
|
154
|
-
process.stderr.write(
|
|
155
|
-
`supported on: ${err.supportedBy.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
159
|
-
process.exitCode = 2;
|
|
216
|
+
refuse(refusalOf(err), wantJson);
|
|
160
217
|
return;
|
|
161
218
|
}
|
|
162
219
|
throw err;
|
|
@@ -167,15 +224,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
167
224
|
extra = parseRunExtra(values);
|
|
168
225
|
} catch (err) {
|
|
169
226
|
if (err instanceof ArgvRefusalError) {
|
|
170
|
-
|
|
171
|
-
if (err.hint) process.stderr.write(`hint: ${err.hint}\n`);
|
|
172
|
-
if (err.supportedBy?.length) {
|
|
173
|
-
process.stderr.write(
|
|
174
|
-
`supported on: ${err.supportedBy.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
178
|
-
process.exitCode = 2;
|
|
227
|
+
refuse(refusalOf(err), wantJson);
|
|
179
228
|
return;
|
|
180
229
|
}
|
|
181
230
|
throw err;
|
|
@@ -189,7 +238,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
189
238
|
if (rawSkills !== undefined && rawSkills.length > 0) {
|
|
190
239
|
try {
|
|
191
240
|
const { resolveSkillNames, listKnownSkills } = await import("./skills-root.js");
|
|
192
|
-
const resolvedSkills = resolveSkillNames(rawSkills);
|
|
241
|
+
const resolvedSkills = resolveSkillNames(rawSkills, h.name);
|
|
193
242
|
const claudeTokens: string[] = [];
|
|
194
243
|
if (h.name === "claude") {
|
|
195
244
|
const { claudeSkillOverridesArg } = await import("../interpretation/skills-selection.js");
|
|
@@ -199,9 +248,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
199
248
|
(turnOpts as unknown as Record<string, unknown>).__claudeSkillTokens = claudeTokens;
|
|
200
249
|
} catch (err) {
|
|
201
250
|
if (err instanceof ArgvRefusalError) {
|
|
202
|
-
|
|
203
|
-
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
204
|
-
process.exitCode = 2;
|
|
251
|
+
refuse(refusalOf(err), wantJson);
|
|
205
252
|
return;
|
|
206
253
|
}
|
|
207
254
|
throw err;
|
|
@@ -230,8 +277,10 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
230
277
|
if (proj !== null) tiers.project = proj.config;
|
|
231
278
|
} catch (configErr) {
|
|
232
279
|
if (configErr instanceof ConfigError) {
|
|
233
|
-
|
|
234
|
-
|
|
280
|
+
refuse(
|
|
281
|
+
{ message: `config error: ${configErr.message}`, issue: "invalid-option-value" },
|
|
282
|
+
wantJson,
|
|
283
|
+
);
|
|
235
284
|
return;
|
|
236
285
|
}
|
|
237
286
|
throw configErr;
|
|
@@ -243,8 +292,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
243
292
|
resolved = resolveEffectiveOptions(h, { ...turnOpts, prompt } as never, tiers);
|
|
244
293
|
} catch (resErr) {
|
|
245
294
|
if (resErr instanceof FloorExceededError) {
|
|
246
|
-
|
|
247
|
-
process.exitCode = 2;
|
|
295
|
+
refuse({ message: resErr.message, issue: "invalid-tool-grant" }, wantJson);
|
|
248
296
|
return;
|
|
249
297
|
}
|
|
250
298
|
throw resErr;
|
|
@@ -254,18 +302,8 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
254
302
|
effectiveTurnOpts = rest as ReturnType<typeof parseTurnOptions>;
|
|
255
303
|
// Provenance is diagnostic data like the spawn line - stderr in BOTH
|
|
256
304
|
// render modes, never stdout (stdout carries the NDJSON contract).
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
process.stderr.write(
|
|
260
|
-
`provenance: ${entry.key} = ${JSON.stringify(entry.value)} (${entry.tier})\n`,
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
for (const key of unrenderable) {
|
|
264
|
-
process.stderr.write(
|
|
265
|
-
`divergence: profile ${JSON.stringify(key)} not expressible on ${h.name}; harness default applies\n`,
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
305
|
+
const { writeProvenance } = await import("./provenance.js");
|
|
306
|
+
writeProvenance(h.name, provenance, unrenderable);
|
|
269
307
|
}
|
|
270
308
|
|
|
271
309
|
// issue #41: question-escalation precedence arg > project > user >
|
|
@@ -342,15 +380,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
342
380
|
_validated = true;
|
|
343
381
|
} catch (err) {
|
|
344
382
|
if (err instanceof ArgvRefusalError) {
|
|
345
|
-
|
|
346
|
-
if (err.hint) process.stderr.write(`hint: ${err.hint}\n`);
|
|
347
|
-
if (err.supportedBy?.length) {
|
|
348
|
-
process.stderr.write(
|
|
349
|
-
`supported on: ${err.supportedBy.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
353
|
-
process.exitCode = 2;
|
|
383
|
+
refuse(refusalOf(err), wantJson);
|
|
354
384
|
return;
|
|
355
385
|
}
|
|
356
386
|
throw err;
|
|
@@ -372,6 +402,51 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
372
402
|
process.stderr.write(
|
|
373
403
|
`provenance: escalateQuestions = ${escalateQuestions} (${escalateTier})\n`,
|
|
374
404
|
);
|
|
405
|
+
// On a harness whose include flag is not a strict allowlist (claude),
|
|
406
|
+
// a name outside the curated set passes through ungated; say which
|
|
407
|
+
// ones so a wrong-case name is visible. A grant with no known name at
|
|
408
|
+
// all refuses inside buildLaunchArgv below, so a throw here is left
|
|
409
|
+
// to that path.
|
|
410
|
+
const grant = (fullOpts.resume ? turnOpts : effectiveTurnOpts).tools;
|
|
411
|
+
if (grant !== undefined && grant.length > 0 && !h.tools.includeIsStrictAllowlist) {
|
|
412
|
+
try {
|
|
413
|
+
const { renderToolSelection } = await import("../interpretation/tool-selection.js");
|
|
414
|
+
const { unmapped } = renderToolSelection(h, { include: [...grant] });
|
|
415
|
+
if (unmapped.length > 0) {
|
|
416
|
+
process.stderr.write(`provenance: unmapped tools = ${JSON.stringify(unmapped)}\n`);
|
|
417
|
+
}
|
|
418
|
+
} catch {
|
|
419
|
+
// refused below with the structured message
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// A harness that creates a session when the id is unknown (pi, muse)
|
|
425
|
+
// would turn a stale --resume into a silent blank session. Refuse when
|
|
426
|
+
// the session store path does not exist; where the path cannot be
|
|
427
|
+
// computed, the runner's pre-spawn warning is the only guard.
|
|
428
|
+
if (fullOpts.resume !== undefined && h.resume.onMissing === "create") {
|
|
429
|
+
let path: string | null = null;
|
|
430
|
+
try {
|
|
431
|
+
path = storePath(h, {
|
|
432
|
+
home: process.env.HOME ?? process.env.USERPROFILE ?? "",
|
|
433
|
+
cwd: extra.cwd ?? process.cwd(),
|
|
434
|
+
sessionId: fullOpts.resume,
|
|
435
|
+
});
|
|
436
|
+
} catch {
|
|
437
|
+
path = null;
|
|
438
|
+
}
|
|
439
|
+
if (path !== null && !existsSync(path)) {
|
|
440
|
+
refuse(
|
|
441
|
+
{
|
|
442
|
+
message: `no ${h.name} session ${fullOpts.resume} found at ${path}`,
|
|
443
|
+
issue: "invalid-option-value",
|
|
444
|
+
supported: [`a session id that exists in ${h.name}'s store`],
|
|
445
|
+
},
|
|
446
|
+
wantJson,
|
|
447
|
+
);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
375
450
|
}
|
|
376
451
|
|
|
377
452
|
// Delete HERDR_ENV before spawn
|
|
@@ -393,7 +468,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
393
468
|
const _currentProc: { signal: (sig: "SIGTERM" | "SIGKILL") => void } | null = null;
|
|
394
469
|
// We'll need to track the spawned process via deps.signal; but streamTurn owns process handle.
|
|
395
470
|
// Instead we intercept deps.signal via a wrapper that captures proc.
|
|
396
|
-
// Simpler: use deps directly and handle SIGINT
|
|
471
|
+
// Simpler: use deps directly and handle SIGINT via injected signal.
|
|
397
472
|
// We'll create a wrapper deps where signal captures last proc.
|
|
398
473
|
let lastProc: import("../execution/deps.js").SpawnedProcess | null = null;
|
|
399
474
|
const originalSignal = deps.signal;
|
|
@@ -410,23 +485,25 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
410
485
|
},
|
|
411
486
|
};
|
|
412
487
|
|
|
488
|
+
const abortController = new AbortController();
|
|
413
489
|
let interrupted = false;
|
|
490
|
+
let escalationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
414
491
|
const onSig = async () => {
|
|
415
492
|
if (interrupted) return;
|
|
416
493
|
interrupted = true;
|
|
494
|
+
abortController.abort();
|
|
417
495
|
if (lastProc) {
|
|
418
496
|
try {
|
|
419
497
|
wrappedDeps.signal(lastProc, "SIGTERM");
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
});
|
|
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?.();
|
|
430
507
|
} catch {}
|
|
431
508
|
}
|
|
432
509
|
};
|
|
@@ -439,7 +516,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
439
516
|
|
|
440
517
|
try {
|
|
441
518
|
// streamTurn handles both launch and resume via TurnRunOptions
|
|
442
|
-
const events = streamTurn(h, fullOpts, wrappedDeps);
|
|
519
|
+
const events = streamTurn(h, { ...fullOpts, signal: abortController.signal }, wrappedDeps);
|
|
443
520
|
for await (const event of events) {
|
|
444
521
|
lastEvent = event;
|
|
445
522
|
if (wantJson) {
|
|
@@ -492,6 +569,10 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
492
569
|
process.off("SIGTERM", onSig);
|
|
493
570
|
return;
|
|
494
571
|
} finally {
|
|
572
|
+
if (escalationTimer !== null) {
|
|
573
|
+
clearTimeout(escalationTimer);
|
|
574
|
+
escalationTimer = null;
|
|
575
|
+
}
|
|
495
576
|
process.off("SIGINT", onSig);
|
|
496
577
|
process.off("SIGTERM", onSig);
|
|
497
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
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* caller threads (identity dedupe per D-022). Unparseable lines on a
|
|
5
5
|
* structured stream are tolerated - scanned for walls, never fatal.
|
|
6
6
|
*/
|
|
7
|
-
import { capabilitiesOf } from "../interpretation/capabilities.js";
|
|
7
|
+
import { type CapabilityResult, capabilitiesOf } from "../interpretation/capabilities.js";
|
|
8
8
|
import { contentEventsOf } from "../interpretation/content.js";
|
|
9
9
|
import { decodeIdentity } from "../interpretation/identity.js";
|
|
10
10
|
import { detectLimitInLine } from "../interpretation/limits.js";
|
|
11
|
-
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
11
|
+
import type { HarnessDescriptor, StreamingGranularity } from "../knowledge/descriptor.js";
|
|
12
12
|
import type { HarnessEvent } from "./events.js";
|
|
13
|
+
import { failureFromBudget } from "./failure.js";
|
|
13
14
|
|
|
14
15
|
export interface DecodeState {
|
|
15
16
|
lastSeenId: string | null;
|
|
@@ -30,6 +31,7 @@ export const decodeLine = (
|
|
|
30
31
|
line: string,
|
|
31
32
|
state: DecodeState,
|
|
32
33
|
model: string,
|
|
34
|
+
streaming?: StreamingGranularity,
|
|
33
35
|
): HarnessEvent[] => {
|
|
34
36
|
let raw: unknown;
|
|
35
37
|
try {
|
|
@@ -43,7 +45,7 @@ export const decodeLine = (
|
|
|
43
45
|
}
|
|
44
46
|
return [];
|
|
45
47
|
}
|
|
46
|
-
return decodeParsed(h, raw, state, model);
|
|
48
|
+
return decodeParsed(h, raw, state, model, streaming);
|
|
47
49
|
};
|
|
48
50
|
|
|
49
51
|
/** The parsed-record half of decodeLine, for pumps that already parsed the
|
|
@@ -54,27 +56,59 @@ export const decodeParsed = (
|
|
|
54
56
|
raw: unknown,
|
|
55
57
|
state: DecodeState,
|
|
56
58
|
model: string,
|
|
59
|
+
streaming?: StreamingGranularity,
|
|
57
60
|
): HarnessEvent[] => {
|
|
58
61
|
const events: HarnessEvent[] = [];
|
|
62
|
+
// Streaming is a property of the spawned argv, not of the model, so the
|
|
63
|
+
// runner passes what streamingGranularityOf computed for it; the curated
|
|
64
|
+
// baseline supplies the rest. An unknown model stays unknown throughout.
|
|
65
|
+
const capabilities = (): CapabilityResult => {
|
|
66
|
+
const base = capabilitiesOf(h, model, "headless-turn");
|
|
67
|
+
return streaming !== undefined && base.source !== "unknown" ? { ...base, streaming } : base;
|
|
68
|
+
};
|
|
59
69
|
const decoded = decodeIdentity(h, raw, state.lastSeenId, state.requestedId);
|
|
60
70
|
if (decoded.sessionId !== null) state.lastSeenId = decoded.sessionId;
|
|
61
71
|
if (decoded.identity !== null) {
|
|
72
|
+
const authority = state.requestedId !== null ? "caller-assigned" : "harness-minted";
|
|
62
73
|
events.push({
|
|
63
74
|
kind: "identity",
|
|
64
75
|
sessionId: decoded.identity,
|
|
65
|
-
authority
|
|
66
|
-
capabilities:
|
|
76
|
+
authority,
|
|
77
|
+
capabilities: capabilities(),
|
|
67
78
|
});
|
|
68
79
|
} else if (decoded.outcome === "malformed" || decoded.outcome === "rotated") {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
if (decoded.outcome === "rotated") {
|
|
81
|
+
const requested = state.requestedId ?? "unknown";
|
|
82
|
+
const announced = decoded.sessionId ?? "unknown";
|
|
83
|
+
events.push({
|
|
84
|
+
kind: "error",
|
|
85
|
+
message: `identity rotated: requested ${requested} but announced ${announced}`,
|
|
86
|
+
});
|
|
87
|
+
// The harness answered under a different id: hand the consumer the
|
|
88
|
+
// id it can actually resume, marked as minted by the harness.
|
|
89
|
+
if (decoded.sessionId !== null) {
|
|
90
|
+
events.push({
|
|
91
|
+
kind: "identity",
|
|
92
|
+
sessionId: decoded.sessionId,
|
|
93
|
+
authority: "harness-minted",
|
|
94
|
+
capabilities: capabilities(),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
events.push({ kind: "error", message: `identity ${decoded.outcome}` });
|
|
99
|
+
}
|
|
73
100
|
}
|
|
74
101
|
|
|
75
|
-
// Content (message/token/tool/error) is per-harness; identity
|
|
76
|
-
// descriptor-driven. contentEventsOf dispatches by harness
|
|
77
|
-
|
|
102
|
+
// Content (message/token/tool/error/budget) is per-harness; identity
|
|
103
|
+
// above is descriptor-driven. contentEventsOf dispatches by harness
|
|
104
|
+
// name. budget is not a HarnessEvent kind and must not leak out.
|
|
105
|
+
for (const content of contentEventsOf(h.name, raw)) {
|
|
106
|
+
if (content.kind === "budget") {
|
|
107
|
+
events.push({ kind: "failure", ...failureFromBudget(content.detail) });
|
|
108
|
+
} else {
|
|
109
|
+
events.push(content);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
78
112
|
|
|
79
113
|
// claude's rate_limit_event: only non-"allowed" statuses are failures.
|
|
80
114
|
// overageStatus is deliberately not classified - it is a separate
|
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
|
*/
|
|
@@ -16,6 +16,9 @@ export interface SpawnedProcess {
|
|
|
16
16
|
/** Resolves with the exit code, or null when the process died to a
|
|
17
17
|
* signal without one. */
|
|
18
18
|
readonly exited: Promise<number | null>;
|
|
19
|
+
/** After `exited` settles, the spawn-time error message when the child
|
|
20
|
+
* never started (async ENOENT and the like), else null. */
|
|
21
|
+
readonly startupError?: () => string | null;
|
|
19
22
|
/** Present when the spawner opened a writable stdin (session mode needs
|
|
20
23
|
* it; one-shot turns close or inherit stdin instead). */
|
|
21
24
|
readonly stdin?: {
|
package/src/execution/events.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* concatenate both.
|
|
16
16
|
*/
|
|
17
17
|
import type { CapabilityResult } from "../interpretation/capabilities.js";
|
|
18
|
+
import type { LimitCode } from "../knowledge/descriptor.js";
|
|
18
19
|
import type { FailureSummary } from "./failure.js";
|
|
19
20
|
|
|
20
21
|
export type ExitCause =
|
|
@@ -49,8 +50,10 @@ export type HarnessEvent =
|
|
|
49
50
|
readonly options: readonly string[];
|
|
50
51
|
readonly recommended?: string;
|
|
51
52
|
}
|
|
52
|
-
| { readonly kind: "limit"; readonly code:
|
|
53
|
-
|
|
53
|
+
| { readonly kind: "limit"; readonly code: LimitCode; readonly message: string }
|
|
54
|
+
/** `terminal: true` marks an error that ended the turn; the matching
|
|
55
|
+
* `failure` (class task) follows it. */
|
|
56
|
+
| { readonly kind: "error"; readonly message: string; readonly terminal?: boolean }
|
|
54
57
|
| ({ readonly kind: "failure" } & FailureSummary)
|
|
55
58
|
| {
|
|
56
59
|
readonly kind: "done";
|
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";
|
|
@@ -157,6 +157,7 @@ const realSpawn = (argv: readonly string[], opts: SpawnOptions): SpawnedProcess
|
|
|
157
157
|
stdout: stdoutOutput === null ? emptyStream() : stdoutOutput.stream,
|
|
158
158
|
stderr: stderrWithError(),
|
|
159
159
|
exited,
|
|
160
|
+
startupError: () => spawnError?.message ?? null,
|
|
160
161
|
disposeOutput: (): void => {
|
|
161
162
|
if (outputDisposed) return;
|
|
162
163
|
outputDisposed = true;
|