@dungle-scrubs/harness-cli-normalizer 0.5.7 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -8
- package/dist/cli/args.d.ts +9 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +31 -26
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/check.d.ts +14 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +10 -3
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/config.d.ts +4 -4
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +6 -6
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/exit-codes.d.ts +0 -1
- package/dist/cli/exit-codes.d.ts.map +1 -1
- package/dist/cli/exit-codes.js +0 -1
- package/dist/cli/exit-codes.js.map +1 -1
- package/dist/cli/help.d.ts +3 -3
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +9 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/inspect.d.ts.map +1 -1
- package/dist/cli/inspect.js +84 -265
- package/dist/cli/inspect.js.map +1 -1
- package/dist/cli/plan-turn.d.ts +68 -0
- package/dist/cli/plan-turn.d.ts.map +1 -0
- package/dist/cli/plan-turn.js +266 -0
- package/dist/cli/plan-turn.js.map +1 -0
- package/dist/cli/refuse.d.ts.map +1 -1
- package/dist/cli/refuse.js +2 -1
- package/dist/cli/refuse.js.map +1 -1
- package/dist/cli/resolve-harness.d.ts.map +1 -1
- package/dist/cli/resolve-harness.js +2 -3
- package/dist/cli/resolve-harness.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +33 -367
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/session-json.d.ts +1 -1
- package/dist/cli/session-json.d.ts.map +1 -1
- package/dist/cli/session-json.js +1 -3
- package/dist/cli/session-json.js.map +1 -1
- package/dist/cli/session.d.ts.map +1 -1
- package/dist/cli/session.js +27 -45
- package/dist/cli/session.js.map +1 -1
- package/dist/cli/skills-root.d.ts +1 -1
- package/dist/cli/skills-root.d.ts.map +1 -1
- package/dist/cli/skills-root.js +1 -1
- package/dist/cli/skills-root.js.map +1 -1
- package/dist/execution/decode.d.ts.map +1 -1
- package/dist/execution/decode.js +11 -36
- package/dist/execution/decode.js.map +1 -1
- package/dist/execution/events.d.ts +4 -1
- package/dist/execution/events.d.ts.map +1 -1
- package/dist/execution/events.js.map +1 -1
- package/dist/execution/failure.d.ts +3 -1
- package/dist/execution/failure.d.ts.map +1 -1
- package/dist/execution/failure.js +14 -11
- package/dist/execution/failure.js.map +1 -1
- package/dist/execution/open-session.d.ts +6 -1
- package/dist/execution/open-session.d.ts.map +1 -1
- package/dist/execution/open-session.js +151 -223
- package/dist/execution/open-session.js.map +1 -1
- package/dist/execution/stream-turn.d.ts +9 -14
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +85 -188
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/execution/supervisor.d.ts +71 -0
- package/dist/execution/supervisor.d.ts.map +1 -0
- package/dist/execution/supervisor.js +127 -0
- package/dist/execution/supervisor.js.map +1 -0
- package/dist/interpretation/argv.d.ts +44 -12
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +38 -24
- package/dist/interpretation/argv.js.map +1 -1
- package/dist/interpretation/content.d.ts +10 -1
- package/dist/interpretation/content.d.ts.map +1 -1
- package/dist/interpretation/content.js +20 -11
- package/dist/interpretation/content.js.map +1 -1
- package/dist/interpretation/hints.d.ts +1 -1
- package/dist/interpretation/hints.d.ts.map +1 -1
- package/dist/interpretation/hints.js +10 -11
- package/dist/interpretation/hints.js.map +1 -1
- package/dist/interpretation/index.d.ts +0 -1
- package/dist/interpretation/index.d.ts.map +1 -1
- package/dist/interpretation/index.js +0 -1
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/limits.d.ts +2 -5
- package/dist/interpretation/limits.d.ts.map +1 -1
- package/dist/interpretation/limits.js +8 -62
- package/dist/interpretation/limits.js.map +1 -1
- package/dist/interpretation/question.d.ts +27 -0
- package/dist/interpretation/question.d.ts.map +1 -1
- package/dist/interpretation/question.js +28 -0
- package/dist/interpretation/question.js.map +1 -1
- package/dist/interpretation/refusal.d.ts +10 -5
- package/dist/interpretation/refusal.d.ts.map +1 -1
- package/dist/interpretation/refusal.js +24 -15
- package/dist/interpretation/refusal.js.map +1 -1
- package/dist/interpretation/resolve-options.d.ts +36 -2
- package/dist/interpretation/resolve-options.d.ts.map +1 -1
- package/dist/interpretation/resolve-options.js +38 -21
- package/dist/interpretation/resolve-options.js.map +1 -1
- package/dist/interpretation/session-input.d.ts +44 -1
- package/dist/interpretation/session-input.d.ts.map +1 -1
- package/dist/interpretation/session-input.js +59 -4
- package/dist/interpretation/session-input.js.map +1 -1
- package/dist/interpretation/shape.d.ts +1 -0
- package/dist/interpretation/shape.d.ts.map +1 -1
- package/dist/interpretation/shape.js +10 -0
- package/dist/interpretation/shape.js.map +1 -1
- package/dist/interpretation/skills-selection.d.ts +25 -33
- package/dist/interpretation/skills-selection.d.ts.map +1 -1
- package/dist/interpretation/skills-selection.js +41 -48
- package/dist/interpretation/skills-selection.js.map +1 -1
- package/dist/interpretation/support.d.ts.map +1 -1
- package/dist/interpretation/support.js +23 -8
- package/dist/interpretation/support.js.map +1 -1
- package/dist/interpretation/tool-selection.d.ts +7 -6
- package/dist/interpretation/tool-selection.d.ts.map +1 -1
- package/dist/interpretation/tool-selection.js +54 -116
- package/dist/interpretation/tool-selection.js.map +1 -1
- package/dist/interpretation/tool-vocabulary.d.ts +31 -12
- package/dist/interpretation/tool-vocabulary.d.ts.map +1 -1
- package/dist/interpretation/tool-vocabulary.js +19 -41
- package/dist/interpretation/tool-vocabulary.js.map +1 -1
- package/dist/interpretation/turn-options.d.ts +4 -0
- package/dist/interpretation/turn-options.d.ts.map +1 -1
- package/dist/interpretation/turn-options.js +105 -241
- package/dist/interpretation/turn-options.js.map +1 -1
- package/dist/interpretation/vocabulary.d.ts +8 -6
- package/dist/interpretation/vocabulary.d.ts.map +1 -1
- package/dist/interpretation/vocabulary.js +22 -9
- package/dist/interpretation/vocabulary.js.map +1 -1
- package/dist/knowledge/claude-code.js +3 -3
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +31 -7
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +67 -36
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/descriptor.js +49 -15
- package/dist/knowledge/descriptor.js.map +1 -1
- package/dist/knowledge/matchers.d.ts +12 -0
- package/dist/knowledge/matchers.d.ts.map +1 -1
- package/dist/knowledge/matchers.js +34 -0
- package/dist/knowledge/matchers.js.map +1 -1
- package/dist/knowledge/muse.d.ts.map +1 -1
- package/dist/knowledge/muse.js +7 -5
- package/dist/knowledge/muse.js.map +1 -1
- package/dist/knowledge/overrides.d.ts.map +1 -1
- package/dist/knowledge/overrides.js +9 -20
- package/dist/knowledge/overrides.js.map +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +8 -6
- package/dist/knowledge/pi.js.map +1 -1
- package/dist/knowledge/profile.d.ts +1 -0
- package/dist/knowledge/profile.d.ts.map +1 -1
- package/dist/knowledge/profile.js +1 -0
- package/dist/knowledge/profile.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/args.ts +35 -27
- package/src/cli/check.ts +10 -3
- package/src/cli/config.ts +12 -11
- package/src/cli/exit-codes.ts +0 -2
- package/src/cli/help.ts +9 -2
- package/src/cli/index.ts +3 -1
- package/src/cli/inspect.ts +88 -269
- package/src/cli/plan-turn.ts +346 -0
- package/src/cli/refuse.ts +2 -1
- package/src/cli/resolve-harness.ts +4 -5
- package/src/cli/run.ts +36 -399
- package/src/cli/session-json.ts +1 -6
- package/src/cli/session.ts +36 -45
- package/src/cli/skills-root.ts +1 -4
- package/src/execution/decode.ts +10 -36
- package/src/execution/events.ts +4 -1
- package/src/execution/failure.ts +18 -11
- package/src/execution/open-session.ts +160 -228
- package/src/execution/stream-turn.ts +90 -193
- package/src/execution/supervisor.ts +199 -0
- package/src/interpretation/argv.ts +86 -38
- package/src/interpretation/content.ts +30 -13
- package/src/interpretation/hints.ts +2 -12
- package/src/interpretation/index.ts +0 -1
- package/src/interpretation/limits.ts +14 -75
- package/src/interpretation/question.ts +46 -0
- package/src/interpretation/refusal.ts +28 -18
- package/src/interpretation/resolve-options.ts +85 -32
- package/src/interpretation/session-input.ts +88 -5
- package/src/interpretation/shape.ts +10 -0
- package/src/interpretation/skills-selection.ts +70 -74
- package/src/interpretation/support.ts +19 -6
- package/src/interpretation/tool-selection.ts +67 -122
- package/src/interpretation/tool-vocabulary.ts +48 -53
- package/src/interpretation/turn-options.ts +123 -220
- package/src/interpretation/vocabulary.ts +15 -10
- package/src/knowledge/claude-code.ts +3 -3
- package/src/knowledge/codex.ts +31 -7
- package/src/knowledge/descriptor.ts +105 -49
- package/src/knowledge/matchers.ts +35 -0
- package/src/knowledge/muse.ts +7 -5
- package/src/knowledge/overrides.ts +17 -32
- package/src/knowledge/pi.ts +8 -6
- package/src/knowledge/profile.ts +1 -0
- package/dist/interpretation/dimensions.d.ts +0 -11
- package/dist/interpretation/dimensions.d.ts.map +0 -1
- package/dist/interpretation/dimensions.js +0 -4
- package/dist/interpretation/dimensions.js.map +0 -1
- package/src/interpretation/dimensions.ts +0 -14
package/src/cli/run.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
+
import type { SpawnedProcess, SpawnOptions } from "../execution/deps.js";
|
|
1
2
|
import type { HarnessEvent } from "../execution/events.js";
|
|
2
3
|
import { nodeRunnerDeps } from "../execution/node-deps.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { composeEscalatedPrompt } from "../interpretation/question.js";
|
|
4
|
+
import { streamTurn } from "../execution/stream-turn.js";
|
|
5
|
+
import { KILL_GRACE_MS } from "../execution/supervisor.js";
|
|
6
6
|
import { ArgvRefusalError } from "../interpretation/refusal.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { defaultDescriptors } from "../knowledge/overrides.js";
|
|
10
|
-
import { parseRunExtra, parseTurnOptions, resolvePromptAsync } from "./args.js";
|
|
7
|
+
import { EXIT_FAILURE, exitCodeForCause } from "./exit-codes.js";
|
|
8
|
+
import { planTurn, writePlanDiagnostics } from "./plan-turn.js";
|
|
11
9
|
import { refusalOf, refuse } from "./refuse.js";
|
|
12
10
|
import { createRenderState, renderEvent, writeEventNdjsonAsync } from "./render.js";
|
|
13
11
|
import { resolveHarness } from "./resolve-harness.js";
|
|
@@ -22,365 +20,32 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
22
20
|
return;
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// the stream a failure and a done.
|
|
31
|
-
const wantJson = normalized.includes("--json");
|
|
32
|
-
const injection = detectPositionalPromptInjection(rawArgs);
|
|
33
|
-
if (injection) {
|
|
34
|
-
const err = new ArgvRefusalError({
|
|
35
|
-
issue: "prompt-flag-injection",
|
|
36
|
-
harness: h.name,
|
|
37
|
-
supported: ["prompt must not start with '-'"],
|
|
38
|
-
detail: `it would be parsed as a flag by ${h.bin}`,
|
|
39
|
-
});
|
|
40
|
-
refuse(refusalOf(err), wantJson);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
let parsed: ReturnType<typeof parseCommonFlags>;
|
|
44
|
-
try {
|
|
45
|
-
parsed = parseCommonFlags(rawArgs);
|
|
46
|
-
} catch (err) {
|
|
47
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
48
|
-
// D7 part B: a native spelling passed before the separator gets
|
|
49
|
-
// recognized and redirected to the normalized flag instead of a
|
|
50
|
-
// generic unknown-flag error.
|
|
51
|
-
// parseArgs reports unknown long flags as "Unknown option '--x'" but
|
|
52
|
-
// splits bundled short flags ("-nt" -> "Unknown option 'n'"). Match the
|
|
53
|
-
// reported token back against the ORIGINAL argv: a short-flag bundle
|
|
54
|
-
// that some descriptor spells exactly (pi's -nt) is recognizable; a
|
|
55
|
-
// lone unknown token keeps the plain error.
|
|
56
|
-
const flagMatch = message.match(/Unknown option '([A-Za-z0-9_-]+)'/);
|
|
57
|
-
let rawFlag: string | undefined;
|
|
58
|
-
if (flagMatch?.[1] !== undefined) {
|
|
59
|
-
const reported = flagMatch[1].startsWith("-") ? flagMatch[1] : `-${flagMatch[1]}`;
|
|
60
|
-
// Exact long flag: use it. Reported short flag (e.g. -n): the caller
|
|
61
|
-
// may have typed a BUNDLE (-nt) that parseArgs split - find the argv
|
|
62
|
-
// token that starts with the reported short and is longer; recognition
|
|
63
|
-
// then decides whether the whole bundle is a descriptor spelling.
|
|
64
|
-
const fromArgv =
|
|
65
|
-
rawArgs.find((a) => a === reported) ??
|
|
66
|
-
(reported.length === 2
|
|
67
|
-
? rawArgs.find((a) => a.length > 2 && a.startsWith(reported))
|
|
68
|
-
: undefined);
|
|
69
|
-
rawFlag = fromArgv ?? reported;
|
|
70
|
-
}
|
|
71
|
-
const native =
|
|
72
|
-
rawFlag !== undefined ? recognizeNativeSpelling(defaultDescriptors(), rawFlag) : null;
|
|
73
|
-
if (native !== null) {
|
|
74
|
-
const by = native.option.startsWith("discovery.")
|
|
75
|
-
? native.entries
|
|
76
|
-
: supportedBy(defaultDescriptors(), native.option);
|
|
77
|
-
const normalizedSpelling =
|
|
78
|
-
native.option === "excludeTools"
|
|
79
|
-
? "--exclude-tools"
|
|
80
|
-
: native.option.startsWith("discovery.")
|
|
81
|
-
? `--no-${native.option.split(".")[1] === "instructionFiles" ? "instruction-files" : native.option.split(".")[1]}`
|
|
82
|
-
: `--${native.option}`;
|
|
83
|
-
refuse(
|
|
84
|
-
{
|
|
85
|
-
message: `unknown flag: ${rawFlag} is a native spelling (used by ${native.entries.map((e) => e.harness).join(", ")}) - use the normalized ${normalizedSpelling} flag instead`,
|
|
86
|
-
issue: "invalid-option-value",
|
|
87
|
-
supportedBy: by,
|
|
88
|
-
trailer: ["Run 'hcn run --help' for usage."],
|
|
89
|
-
},
|
|
90
|
-
wantJson,
|
|
91
|
-
);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
refuse(
|
|
95
|
-
{
|
|
96
|
-
message: `unknown flag: ${message}`,
|
|
97
|
-
issue: "invalid-option-value",
|
|
98
|
-
trailer: ["Run 'hcn run --help' for usage."],
|
|
99
|
-
},
|
|
100
|
-
wantJson,
|
|
101
|
-
);
|
|
23
|
+
// One owner turns the arguments into the plan the runner spawns from
|
|
24
|
+
// (RFC-02 change 10); every refusal it raises is structured.
|
|
25
|
+
const outcome = await planTurn(h, rawArgs, { command: "run" });
|
|
26
|
+
if (outcome.kind === "refusal") {
|
|
27
|
+
refuse(outcome.refusal, outcome.wantJson);
|
|
102
28
|
return;
|
|
103
29
|
}
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
// positionals may contain prompt if not using flag; harness already consumed so first positional is prompt
|
|
108
|
-
const positionalPrompt = positionals.length > 0 ? positionals[0] : undefined;
|
|
109
|
-
if (positionals.length > 1) {
|
|
110
|
-
refuse(
|
|
111
|
-
{
|
|
112
|
-
message: "too many positionals for run; expected one prompt",
|
|
113
|
-
issue: "invalid-option-value",
|
|
114
|
-
},
|
|
115
|
-
wantJson,
|
|
116
|
-
);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (passthrough.length === 0 && rawArgs.includes("--")) {
|
|
120
|
-
refuse(
|
|
121
|
-
{
|
|
122
|
-
message: "-- separator given but no passthrough tokens followed it",
|
|
123
|
-
issue: "invalid-option-value",
|
|
124
|
-
},
|
|
125
|
-
wantJson,
|
|
126
|
-
);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Resolve prompt (async for --prompt-file -)
|
|
131
|
-
let prompt: string;
|
|
132
|
-
let promptSource: string;
|
|
133
|
-
try {
|
|
134
|
-
const resolved = await resolvePromptAsync({
|
|
135
|
-
positionalPrompt,
|
|
136
|
-
promptFlag: values.prompt as string | undefined,
|
|
137
|
-
promptFile: values["prompt-file"] as string | undefined,
|
|
138
|
-
});
|
|
139
|
-
prompt = resolved.prompt;
|
|
140
|
-
promptSource = resolved.source;
|
|
141
|
-
} catch (err) {
|
|
142
|
-
if (err instanceof ArgvRefusalError) {
|
|
143
|
-
refuse(refusalOf(err), wantJson);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (err instanceof Error && (err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
147
|
-
refuse(
|
|
148
|
-
{ message: `prompt file not found: ${err.message}`, issue: "invalid-option-value" },
|
|
149
|
-
wantJson,
|
|
150
|
-
);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
throw err;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Build turn options
|
|
157
|
-
let turnOpts: ReturnType<typeof parseTurnOptions>;
|
|
158
|
-
try {
|
|
159
|
-
turnOpts = parseTurnOptions(values);
|
|
160
|
-
} catch (err) {
|
|
161
|
-
if (err instanceof ArgvRefusalError) {
|
|
162
|
-
refuse(refusalOf(err), wantJson);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
throw err;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
let extra: ReturnType<typeof parseRunExtra>;
|
|
169
|
-
try {
|
|
170
|
-
extra = parseRunExtra(values);
|
|
171
|
-
} catch (err) {
|
|
172
|
-
if (err instanceof ArgvRefusalError) {
|
|
173
|
-
refuse(refusalOf(err), wantJson);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
throw err;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const isExplicit = promptSource !== "positional";
|
|
180
|
-
|
|
181
|
-
// issue #38: resolve --skills names against the caller's registry root,
|
|
182
|
-
// then hand the harness its native rendering (pi loads; claude narrows).
|
|
183
|
-
const rawSkills = (turnOpts as unknown as { skills?: string[] }).skills;
|
|
184
|
-
if (rawSkills !== undefined && rawSkills.length > 0) {
|
|
185
|
-
try {
|
|
186
|
-
const { resolveSkillNames, listKnownSkills } = await import("./skills-root.js");
|
|
187
|
-
const resolvedSkills = resolveSkillNames(rawSkills, h.name);
|
|
188
|
-
const skillTokens: string[] = [];
|
|
189
|
-
if (h.name === "claude") {
|
|
190
|
-
const { claudeSkillOverridesArg } = await import("../interpretation/skills-selection.js");
|
|
191
|
-
skillTokens.push(...claudeSkillOverridesArg(listKnownSkills(), resolvedSkills));
|
|
192
|
-
} else if (h.name === "codex") {
|
|
193
|
-
const { codexSkillConfigArg } = await import("../interpretation/skills-selection.js");
|
|
194
|
-
skillTokens.push(...codexSkillConfigArg(listKnownSkills(), resolvedSkills));
|
|
195
|
-
}
|
|
196
|
-
(turnOpts as unknown as Record<string, unknown>).skills = resolvedSkills;
|
|
197
|
-
(turnOpts as unknown as Record<string, unknown>).__skillTokens = skillTokens;
|
|
198
|
-
} catch (err) {
|
|
199
|
-
if (err instanceof ArgvRefusalError) {
|
|
200
|
-
refuse(refusalOf(err), wantJson);
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
throw err;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Defaults profile + user config: LAUNCH-ONLY. A resumed session keeps
|
|
208
|
-
// its own settings; the resolver never runs on resume paths.
|
|
209
|
-
let effectiveTurnOpts: ReturnType<typeof parseTurnOptions> = turnOpts;
|
|
210
|
-
const resolvedTiers: {
|
|
211
|
-
user?: Partial<ReturnType<typeof parseTurnOptions>>;
|
|
212
|
-
project?: Partial<ReturnType<typeof parseTurnOptions>>;
|
|
213
|
-
} = {};
|
|
214
|
-
// Config files load on EVERY run, launch or resume: the tiers feed the
|
|
215
|
-
// defaults profile on launch, and issue #41's questions (a
|
|
216
|
-
// behavior instruction, not a turn option) resolves from them on resume
|
|
217
|
-
// too - otherwise a no-ask session would flip its preamble on the
|
|
218
|
-
// answer turn. Resolution of TURN options stays launch-only.
|
|
219
|
-
const tiers = resolvedTiers;
|
|
220
|
-
{
|
|
221
|
-
const { loadUserConfig, loadProjectConfig, ConfigError } = await import("./config.js");
|
|
222
|
-
try {
|
|
223
|
-
const loaded = loadUserConfig();
|
|
224
|
-
if (loaded !== null) tiers.user = loaded.config;
|
|
225
|
-
const proj = loadProjectConfig();
|
|
226
|
-
if (proj !== null) tiers.project = proj.config;
|
|
227
|
-
} catch (configErr) {
|
|
228
|
-
if (configErr instanceof ConfigError) {
|
|
229
|
-
refuse(
|
|
230
|
-
{ message: `config error: ${configErr.message}`, issue: "invalid-option-value" },
|
|
231
|
-
wantJson,
|
|
232
|
-
);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
throw configErr;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
if (extra.resume === undefined) {
|
|
239
|
-
let resolved: ReturnType<typeof resolveEffectiveOptions>;
|
|
240
|
-
try {
|
|
241
|
-
resolved = resolveEffectiveOptions(h, { ...turnOpts, prompt } as never, tiers);
|
|
242
|
-
} catch (resErr) {
|
|
243
|
-
if (resErr instanceof FloorExceededError) {
|
|
244
|
-
refuse({ message: resErr.message, issue: "invalid-tool-grant" }, wantJson);
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
if (resErr instanceof ArgvRefusalError) {
|
|
248
|
-
refuse(refusalOf(resErr), wantJson);
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
throw resErr;
|
|
252
|
-
}
|
|
253
|
-
const { provenance, unrenderable } = resolved;
|
|
254
|
-
const { prompt: _p, ...rest } = resolved.options as { prompt: string };
|
|
255
|
-
effectiveTurnOpts = rest as ReturnType<typeof parseTurnOptions>;
|
|
256
|
-
// Provenance is diagnostic data like the spawn line - stderr in BOTH
|
|
257
|
-
// render modes, never stdout (stdout carries the NDJSON contract).
|
|
258
|
-
const { writeProvenance } = await import("./provenance.js");
|
|
259
|
-
writeProvenance(h.name, provenance, unrenderable);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// question mode precedence arg > project > user > default (ask)
|
|
263
|
-
const projectQuestions = (resolvedTiers.project as { questions?: string } | undefined)?.questions;
|
|
264
|
-
const userQuestions = (resolvedTiers.user as { questions?: string } | undefined)?.questions;
|
|
265
|
-
const rawMode = (turnOpts as { questions?: string }).questions;
|
|
266
|
-
const questionMode =
|
|
267
|
-
rawMode !== undefined
|
|
268
|
-
? (rawMode as import("../interpretation/question.js").QuestionMode)
|
|
269
|
-
: projectQuestions !== undefined
|
|
270
|
-
? (projectQuestions as import("../interpretation/question.js").QuestionMode)
|
|
271
|
-
: userQuestions !== undefined
|
|
272
|
-
? (userQuestions as import("../interpretation/question.js").QuestionMode)
|
|
273
|
-
: ("ask" as const);
|
|
274
|
-
const questionTier =
|
|
275
|
-
rawMode !== undefined
|
|
276
|
-
? "arg"
|
|
277
|
-
: projectQuestions !== undefined
|
|
278
|
-
? "project-config"
|
|
279
|
-
: userQuestions !== undefined
|
|
280
|
-
? "user-config"
|
|
281
|
-
: "default";
|
|
282
|
-
|
|
283
|
-
const fullOpts = {
|
|
284
|
-
...effectiveTurnOpts,
|
|
285
|
-
prompt: composeEscalatedPrompt(prompt, questionMode),
|
|
286
|
-
cwd: extra.cwd,
|
|
287
|
-
env: extra.env,
|
|
288
|
-
resume: extra.resume,
|
|
289
|
-
questions: questionMode,
|
|
290
|
-
...(passthrough.length > 0 ? { passthrough } : {}),
|
|
291
|
-
...(isExplicit ? { __explicitPrompt: true as const } : {}),
|
|
292
|
-
} as Parameters<typeof streamTurn>[1] & {
|
|
293
|
-
resume?: string;
|
|
294
|
-
__explicitPrompt?: boolean;
|
|
295
|
-
passthrough?: readonly string[];
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
// Pre-validate via building argv to catch refusals before spawn (so we don't spawn on bad args)
|
|
299
|
-
let _validated = false;
|
|
300
|
-
let preArgv: string[] | null = null;
|
|
301
|
-
try {
|
|
302
|
-
if (fullOpts.resume) {
|
|
303
|
-
// Resume never carries TURN-option profile resolution (launch-only
|
|
304
|
-
// rule), so it builds from the raw turn options; hcn-owned behavior
|
|
305
|
-
// (questions preamble, timeout budget) still applies.
|
|
306
|
-
preArgv = buildResumeArgv(h, {
|
|
307
|
-
...(turnOpts as object),
|
|
308
|
-
prompt: fullOpts.prompt,
|
|
309
|
-
sessionId: fullOpts.resume,
|
|
310
|
-
__explicitPrompt: isExplicit,
|
|
311
|
-
} as never);
|
|
312
|
-
} else {
|
|
313
|
-
// Launch builds from the RESOLVED options so the spawn line and the
|
|
314
|
-
// real argv agree. The prompt here is the COMPOSED one (escalation
|
|
315
|
-
// preamble included) - redactArgv masks by position, so an argv
|
|
316
|
-
// built from the raw prompt would leak it into the spawn line.
|
|
317
|
-
preArgv = buildLaunchArgv(h, {
|
|
318
|
-
...(effectiveTurnOpts as object),
|
|
319
|
-
prompt: fullOpts.prompt,
|
|
320
|
-
__explicitPrompt: isExplicit,
|
|
321
|
-
} as never);
|
|
322
|
-
const skillTokens = (effectiveTurnOpts as unknown as { __skillTokens?: string[] })
|
|
323
|
-
.__skillTokens;
|
|
324
|
-
if (skillTokens !== undefined && skillTokens.length > 0) {
|
|
325
|
-
preArgv.push(...skillTokens);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
_validated = true;
|
|
329
|
-
} catch (err) {
|
|
330
|
-
if (err instanceof ArgvRefusalError) {
|
|
331
|
-
refuse(refusalOf(err), wantJson);
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
throw err;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// preArgv is already set via validated build; no extra handling needed for explicit prompt bypass
|
|
338
|
-
// since buildLaunchArgv now respects __explicitPrompt.
|
|
339
|
-
|
|
340
|
-
if (preArgv) {
|
|
341
|
-
const redacted = redactArgv(preArgv, fullOpts.prompt);
|
|
342
|
-
if (!wantJson) {
|
|
343
|
-
process.stderr.write(`spawn: ${redacted.join(" ")}\n`);
|
|
344
|
-
} else {
|
|
345
|
-
// In JSON mode, diagnostics to stderr only
|
|
346
|
-
process.stderr.write(`spawn: ${redacted.join(" ")}\n`);
|
|
347
|
-
}
|
|
348
|
-
// issue #41: the escalation mode rides stderr as provenance, like
|
|
349
|
-
// every other resolution the turn depends on.
|
|
350
|
-
process.stderr.write(`provenance: questions = ${questionMode} (${questionTier})\n`);
|
|
351
|
-
// On a harness whose include flag is not a strict allowlist (claude),
|
|
352
|
-
// a name outside the curated set passes through ungated; say which
|
|
353
|
-
// ones so a wrong-case name is visible. A grant with no known name at
|
|
354
|
-
// all refuses inside buildLaunchArgv below, so a throw here is left
|
|
355
|
-
// to that path.
|
|
356
|
-
const grant = (fullOpts.resume ? turnOpts : effectiveTurnOpts).tools;
|
|
357
|
-
if (grant !== undefined && grant.length > 0) {
|
|
358
|
-
try {
|
|
359
|
-
const { renderToolSelection } = await import("../interpretation/tool-selection.js");
|
|
360
|
-
const { passthrough } = renderToolSelection(h, { include: [...grant] });
|
|
361
|
-
if (passthrough.length > 0) {
|
|
362
|
-
process.stderr.write(`provenance: native tools = ${JSON.stringify(passthrough)}\n`);
|
|
363
|
-
}
|
|
364
|
-
} catch {
|
|
365
|
-
// refused below with the structured message
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
30
|
+
const { plan } = outcome;
|
|
31
|
+
const { wantJson } = plan;
|
|
32
|
+
writePlanDiagnostics(h, plan, "spawn");
|
|
369
33
|
|
|
370
34
|
// A harness that creates a session when the id is unknown (pi, muse)
|
|
371
35
|
// would turn a stale --resume into a silent blank session. Refuse when
|
|
372
36
|
// the session store path does not exist; where the path cannot be
|
|
373
37
|
// computed, the runner's pre-spawn warning is the only guard.
|
|
374
|
-
|
|
38
|
+
const resume = plan.options.resume;
|
|
39
|
+
if (resume !== undefined && h.resume.onMissing === "create") {
|
|
375
40
|
const { path, exists } = resumeStore(h, {
|
|
376
41
|
home: process.env.HOME ?? process.env.USERPROFILE ?? "",
|
|
377
|
-
cwd:
|
|
378
|
-
sessionId:
|
|
42
|
+
cwd: plan.options.cwd ?? process.cwd(),
|
|
43
|
+
sessionId: resume,
|
|
379
44
|
});
|
|
380
45
|
if (path !== null && !exists) {
|
|
381
46
|
refuse(
|
|
382
47
|
{
|
|
383
|
-
message: `no ${h.name} session ${
|
|
48
|
+
message: `no ${h.name} session ${resume} found at ${path}`,
|
|
384
49
|
issue: "invalid-option-value",
|
|
385
50
|
supported: [`a session id that exists in ${h.name}'s store`],
|
|
386
51
|
},
|
|
@@ -390,36 +55,28 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
390
55
|
}
|
|
391
56
|
}
|
|
392
57
|
|
|
393
|
-
//
|
|
58
|
+
// D-025: a child harness must not inherit Herdr's environment.
|
|
394
59
|
delete (process.env as Record<string, string | undefined>).HERDR_ENV;
|
|
395
60
|
|
|
396
|
-
// D11: opt-in wall-clock budget
|
|
397
|
-
//
|
|
398
|
-
const timeoutSeconds =
|
|
399
|
-
extra.timeoutSeconds !== undefined
|
|
400
|
-
? extra.timeoutSeconds
|
|
401
|
-
: ((resolvedTiers?.project as { timeout?: number } | undefined)?.timeout ??
|
|
402
|
-
(resolvedTiers?.user as { timeout?: number } | undefined)?.timeout);
|
|
61
|
+
// D11: opt-in wall-clock budget (no profile entry by ratification);
|
|
62
|
+
// 0 = explicit disable.
|
|
63
|
+
const timeoutSeconds = plan.behavior.timeoutSeconds.value;
|
|
403
64
|
const deps =
|
|
404
65
|
timeoutSeconds !== undefined && timeoutSeconds > 0
|
|
405
66
|
? nodeRunnerDeps({ turnTimeoutMs: timeoutSeconds * 1000 })
|
|
406
67
|
: nodeRunnerDeps();
|
|
407
68
|
|
|
408
|
-
// Signal handling
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
// Instead we intercept deps.signal via a wrapper that captures proc.
|
|
412
|
-
// Simpler: use deps directly and handle SIGINT via injected signal.
|
|
413
|
-
// We'll create a wrapper deps where signal captures last proc.
|
|
414
|
-
let lastProc: import("../execution/deps.js").SpawnedProcess | null = null;
|
|
69
|
+
// Signal handling: the runner owns the process handle, so the deps are
|
|
70
|
+
// wrapped to remember the last spawned process for SIGINT/SIGTERM.
|
|
71
|
+
let lastProc: SpawnedProcess | null = null;
|
|
415
72
|
const originalSignal = deps.signal;
|
|
416
73
|
const wrappedDeps = {
|
|
417
74
|
...deps,
|
|
418
|
-
signal: (proc:
|
|
75
|
+
signal: (proc: SpawnedProcess, sig: "SIGTERM" | "SIGKILL") => {
|
|
419
76
|
lastProc = proc;
|
|
420
77
|
originalSignal(proc, sig);
|
|
421
78
|
},
|
|
422
|
-
spawn: (argv: readonly string[], opts:
|
|
79
|
+
spawn: (argv: readonly string[], opts: SpawnOptions) => {
|
|
423
80
|
const proc = deps.spawn(argv, opts);
|
|
424
81
|
lastProc = proc;
|
|
425
82
|
return proc;
|
|
@@ -456,8 +113,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
456
113
|
let exitCode = 0;
|
|
457
114
|
|
|
458
115
|
try {
|
|
459
|
-
|
|
460
|
-
const events = streamTurn(h, { ...fullOpts, signal: abortController.signal }, wrappedDeps);
|
|
116
|
+
const events = streamTurn(h, { ...plan.options, signal: abortController.signal }, wrappedDeps);
|
|
461
117
|
for await (const event of events) {
|
|
462
118
|
lastEvent = event;
|
|
463
119
|
if (wantJson) {
|
|
@@ -467,35 +123,18 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
467
123
|
} else {
|
|
468
124
|
renderEvent(event, state);
|
|
469
125
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
// If failure class is rejected? But done.cause for rejected would be failed? Still 1 per mapping, but refusal before spawn is 2.
|
|
474
|
-
// The RFC says limit/auth ->1, transport ->1, refusal ->2 (already handled). So done non-clean =>1.
|
|
475
|
-
}
|
|
476
|
-
if (event.kind === "limit") {
|
|
477
|
-
// Also ensure exitCode will be 1 (handled via done)
|
|
478
|
-
}
|
|
126
|
+
// A refusal before spawn exits 2 above; every done maps through the
|
|
127
|
+
// one exit-code rule.
|
|
128
|
+
if (event.kind === "done") exitCode = exitCodeForCause(event.cause);
|
|
479
129
|
}
|
|
480
130
|
} catch (err) {
|
|
481
131
|
if (err instanceof ArgvRefusalError) {
|
|
482
|
-
|
|
483
|
-
if (err.hint) process.stderr.write(`hint: ${err.hint}\n`);
|
|
484
|
-
if (err.supportedBy?.length) {
|
|
485
|
-
process.stderr.write(
|
|
486
|
-
`supported on: ${err.supportedBy.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
490
|
-
process.exitCode = 2;
|
|
491
|
-
process.off("SIGINT", onSig);
|
|
492
|
-
process.off("SIGTERM", onSig);
|
|
132
|
+
refuse(refusalOf(err), wantJson);
|
|
493
133
|
return;
|
|
494
134
|
}
|
|
495
135
|
// Transport / spawn failure
|
|
496
136
|
process.stderr.write(`run failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
497
137
|
if (wantJson && lastEvent?.kind !== "done") {
|
|
498
|
-
// Emit failure+done if stream didn't? streamTurn should already emit done, but if we crashed before spawn, synthesize?
|
|
499
138
|
const failure = {
|
|
500
139
|
kind: "failure" as const,
|
|
501
140
|
class: "transport" as const,
|
|
@@ -507,9 +146,7 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
507
146
|
`${JSON.stringify({ kind: "done", exitCode: null, cause: "failed", failure })}\n`,
|
|
508
147
|
);
|
|
509
148
|
}
|
|
510
|
-
process.exitCode =
|
|
511
|
-
process.off("SIGINT", onSig);
|
|
512
|
-
process.off("SIGTERM", onSig);
|
|
149
|
+
process.exitCode = EXIT_FAILURE;
|
|
513
150
|
return;
|
|
514
151
|
} finally {
|
|
515
152
|
if (escalationTimer !== null) {
|
|
@@ -522,15 +159,15 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
522
159
|
|
|
523
160
|
// If no done event was seen (should not happen), exit 1
|
|
524
161
|
if (lastEvent === null) {
|
|
525
|
-
process.exitCode =
|
|
162
|
+
process.exitCode = EXIT_FAILURE;
|
|
526
163
|
return;
|
|
527
164
|
}
|
|
528
165
|
if (lastEvent.kind === "done") {
|
|
529
166
|
process.exitCode = exitCode;
|
|
530
167
|
} else {
|
|
531
168
|
// Stream ended without done (e.g., consumer break via head)
|
|
532
|
-
process.exitCode = interrupted ?
|
|
169
|
+
process.exitCode = interrupted ? EXIT_FAILURE : 0;
|
|
533
170
|
}
|
|
534
171
|
|
|
535
|
-
if (interrupted && process.exitCode === 0) process.exitCode =
|
|
172
|
+
if (interrupted && process.exitCode === 0) process.exitCode = EXIT_FAILURE;
|
|
536
173
|
};
|
package/src/cli/session-json.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
type SessionHandle,
|
|
17
17
|
type SessionSendResult,
|
|
18
18
|
} from "../execution/open-session.js";
|
|
19
|
-
import type
|
|
19
|
+
import { composeAnswer, type QuestionMode } from "../interpretation/question.js";
|
|
20
20
|
|
|
21
21
|
/** What the CLI reads back after a close, captured from the runner's
|
|
22
22
|
* `session_close` boundary log. */
|
|
@@ -70,11 +70,6 @@ const parseCommand = (line: string): ParsedCommand => {
|
|
|
70
70
|
return { malformed: `unknown op ${JSON.stringify(rec.op)}` };
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
-
/** The answer wrapper hcn composes so a consumer never re-derives it
|
|
74
|
-
* (RFC-01: the preamble is normalizer knowledge). */
|
|
75
|
-
const composeAnswer = (question: string, answer: string): string =>
|
|
76
|
-
`The user answered the question: "${question}" with: ${answer}. Continue accordingly.`;
|
|
77
|
-
|
|
78
73
|
export const runJsonSession = async (a: JsonSessionArgs): Promise<number> => {
|
|
79
74
|
const rawWrite = a.write ?? ((line: string) => process.stdout.write(line));
|
|
80
75
|
const onDrain = a.onDrain ?? ((fn: () => void) => process.stdout.once("drain", fn));
|