@dungle-scrubs/harness-cli-normalizer 0.6.0 → 0.6.2

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.
Files changed (219) hide show
  1. package/README.md +33 -6
  2. package/dist/cli/args.d.ts +9 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +36 -26
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/check.d.ts +14 -0
  7. package/dist/cli/check.d.ts.map +1 -1
  8. package/dist/cli/check.js +10 -3
  9. package/dist/cli/check.js.map +1 -1
  10. package/dist/cli/config.d.ts +4 -4
  11. package/dist/cli/config.d.ts.map +1 -1
  12. package/dist/cli/config.js +8 -6
  13. package/dist/cli/config.js.map +1 -1
  14. package/dist/cli/exit-codes.d.ts +0 -1
  15. package/dist/cli/exit-codes.d.ts.map +1 -1
  16. package/dist/cli/exit-codes.js +0 -1
  17. package/dist/cli/exit-codes.js.map +1 -1
  18. package/dist/cli/help.d.ts +2 -2
  19. package/dist/cli/help.d.ts.map +1 -1
  20. package/dist/cli/help.js +10 -2
  21. package/dist/cli/help.js.map +1 -1
  22. package/dist/cli/index.d.ts.map +1 -1
  23. package/dist/cli/index.js +3 -1
  24. package/dist/cli/index.js.map +1 -1
  25. package/dist/cli/inspect.d.ts.map +1 -1
  26. package/dist/cli/inspect.js +84 -265
  27. package/dist/cli/inspect.js.map +1 -1
  28. package/dist/cli/plan-turn.d.ts +68 -0
  29. package/dist/cli/plan-turn.d.ts.map +1 -0
  30. package/dist/cli/plan-turn.js +268 -0
  31. package/dist/cli/plan-turn.js.map +1 -0
  32. package/dist/cli/refuse.d.ts.map +1 -1
  33. package/dist/cli/refuse.js +2 -1
  34. package/dist/cli/refuse.js.map +1 -1
  35. package/dist/cli/resolve-harness.d.ts.map +1 -1
  36. package/dist/cli/resolve-harness.js +2 -3
  37. package/dist/cli/resolve-harness.js.map +1 -1
  38. package/dist/cli/run.d.ts.map +1 -1
  39. package/dist/cli/run.js +33 -367
  40. package/dist/cli/run.js.map +1 -1
  41. package/dist/cli/session-json.d.ts +1 -1
  42. package/dist/cli/session-json.d.ts.map +1 -1
  43. package/dist/cli/session-json.js +1 -3
  44. package/dist/cli/session-json.js.map +1 -1
  45. package/dist/cli/session.d.ts.map +1 -1
  46. package/dist/cli/session.js +35 -43
  47. package/dist/cli/session.js.map +1 -1
  48. package/dist/cli/skills-root.d.ts +1 -1
  49. package/dist/cli/skills-root.d.ts.map +1 -1
  50. package/dist/cli/skills-root.js +1 -1
  51. package/dist/cli/skills-root.js.map +1 -1
  52. package/dist/execution/decode.d.ts.map +1 -1
  53. package/dist/execution/decode.js +11 -36
  54. package/dist/execution/decode.js.map +1 -1
  55. package/dist/execution/events.d.ts +4 -1
  56. package/dist/execution/events.d.ts.map +1 -1
  57. package/dist/execution/events.js.map +1 -1
  58. package/dist/execution/failure.d.ts +3 -1
  59. package/dist/execution/failure.d.ts.map +1 -1
  60. package/dist/execution/failure.js +14 -11
  61. package/dist/execution/failure.js.map +1 -1
  62. package/dist/execution/open-session.d.ts +1 -1
  63. package/dist/execution/open-session.d.ts.map +1 -1
  64. package/dist/execution/open-session.js +150 -223
  65. package/dist/execution/open-session.js.map +1 -1
  66. package/dist/execution/stream-turn.d.ts +9 -14
  67. package/dist/execution/stream-turn.d.ts.map +1 -1
  68. package/dist/execution/stream-turn.js +85 -188
  69. package/dist/execution/stream-turn.js.map +1 -1
  70. package/dist/execution/supervisor.d.ts +71 -0
  71. package/dist/execution/supervisor.d.ts.map +1 -0
  72. package/dist/execution/supervisor.js +127 -0
  73. package/dist/execution/supervisor.js.map +1 -0
  74. package/dist/interpretation/argv.d.ts +39 -12
  75. package/dist/interpretation/argv.d.ts.map +1 -1
  76. package/dist/interpretation/argv.js +28 -20
  77. package/dist/interpretation/argv.js.map +1 -1
  78. package/dist/interpretation/content.d.ts +10 -1
  79. package/dist/interpretation/content.d.ts.map +1 -1
  80. package/dist/interpretation/content.js +20 -11
  81. package/dist/interpretation/content.js.map +1 -1
  82. package/dist/interpretation/hints.d.ts +1 -1
  83. package/dist/interpretation/hints.d.ts.map +1 -1
  84. package/dist/interpretation/hints.js +10 -11
  85. package/dist/interpretation/hints.js.map +1 -1
  86. package/dist/interpretation/index.d.ts +0 -1
  87. package/dist/interpretation/index.d.ts.map +1 -1
  88. package/dist/interpretation/index.js +0 -1
  89. package/dist/interpretation/index.js.map +1 -1
  90. package/dist/interpretation/isolation.d.ts +10 -0
  91. package/dist/interpretation/isolation.d.ts.map +1 -0
  92. package/dist/interpretation/isolation.js +30 -0
  93. package/dist/interpretation/isolation.js.map +1 -0
  94. package/dist/interpretation/limits.d.ts +2 -5
  95. package/dist/interpretation/limits.d.ts.map +1 -1
  96. package/dist/interpretation/limits.js +8 -62
  97. package/dist/interpretation/limits.js.map +1 -1
  98. package/dist/interpretation/question.d.ts +27 -0
  99. package/dist/interpretation/question.d.ts.map +1 -1
  100. package/dist/interpretation/question.js +28 -0
  101. package/dist/interpretation/question.js.map +1 -1
  102. package/dist/interpretation/refusal.d.ts +10 -5
  103. package/dist/interpretation/refusal.d.ts.map +1 -1
  104. package/dist/interpretation/refusal.js +24 -15
  105. package/dist/interpretation/refusal.js.map +1 -1
  106. package/dist/interpretation/resolve-options.d.ts +36 -2
  107. package/dist/interpretation/resolve-options.d.ts.map +1 -1
  108. package/dist/interpretation/resolve-options.js +53 -22
  109. package/dist/interpretation/resolve-options.js.map +1 -1
  110. package/dist/interpretation/session-input.d.ts +44 -1
  111. package/dist/interpretation/session-input.d.ts.map +1 -1
  112. package/dist/interpretation/session-input.js +59 -4
  113. package/dist/interpretation/session-input.js.map +1 -1
  114. package/dist/interpretation/shape.d.ts +1 -0
  115. package/dist/interpretation/shape.d.ts.map +1 -1
  116. package/dist/interpretation/shape.js +10 -0
  117. package/dist/interpretation/shape.js.map +1 -1
  118. package/dist/interpretation/skills-selection.d.ts +25 -33
  119. package/dist/interpretation/skills-selection.d.ts.map +1 -1
  120. package/dist/interpretation/skills-selection.js +41 -48
  121. package/dist/interpretation/skills-selection.js.map +1 -1
  122. package/dist/interpretation/support.d.ts.map +1 -1
  123. package/dist/interpretation/support.js +25 -8
  124. package/dist/interpretation/support.js.map +1 -1
  125. package/dist/interpretation/tool-selection.d.ts +7 -6
  126. package/dist/interpretation/tool-selection.d.ts.map +1 -1
  127. package/dist/interpretation/tool-selection.js +54 -116
  128. package/dist/interpretation/tool-selection.js.map +1 -1
  129. package/dist/interpretation/tool-vocabulary.d.ts +31 -12
  130. package/dist/interpretation/tool-vocabulary.d.ts.map +1 -1
  131. package/dist/interpretation/tool-vocabulary.js +19 -41
  132. package/dist/interpretation/tool-vocabulary.js.map +1 -1
  133. package/dist/interpretation/turn-options.d.ts +4 -0
  134. package/dist/interpretation/turn-options.d.ts.map +1 -1
  135. package/dist/interpretation/turn-options.js +107 -241
  136. package/dist/interpretation/turn-options.js.map +1 -1
  137. package/dist/interpretation/vocabulary.d.ts +8 -6
  138. package/dist/interpretation/vocabulary.d.ts.map +1 -1
  139. package/dist/interpretation/vocabulary.js +22 -9
  140. package/dist/interpretation/vocabulary.js.map +1 -1
  141. package/dist/knowledge/claude-code.d.ts +3 -1
  142. package/dist/knowledge/claude-code.d.ts.map +1 -1
  143. package/dist/knowledge/claude-code.js +17 -4
  144. package/dist/knowledge/claude-code.js.map +1 -1
  145. package/dist/knowledge/codex.d.ts.map +1 -1
  146. package/dist/knowledge/codex.js +31 -7
  147. package/dist/knowledge/codex.js.map +1 -1
  148. package/dist/knowledge/descriptor.d.ts +67 -36
  149. package/dist/knowledge/descriptor.d.ts.map +1 -1
  150. package/dist/knowledge/descriptor.js +50 -15
  151. package/dist/knowledge/descriptor.js.map +1 -1
  152. package/dist/knowledge/matchers.d.ts +12 -0
  153. package/dist/knowledge/matchers.d.ts.map +1 -1
  154. package/dist/knowledge/matchers.js +34 -0
  155. package/dist/knowledge/matchers.js.map +1 -1
  156. package/dist/knowledge/muse.d.ts.map +1 -1
  157. package/dist/knowledge/muse.js +13 -6
  158. package/dist/knowledge/muse.js.map +1 -1
  159. package/dist/knowledge/overrides.d.ts.map +1 -1
  160. package/dist/knowledge/overrides.js +9 -20
  161. package/dist/knowledge/overrides.js.map +1 -1
  162. package/dist/knowledge/pi.d.ts.map +1 -1
  163. package/dist/knowledge/pi.js +8 -6
  164. package/dist/knowledge/pi.js.map +1 -1
  165. package/dist/knowledge/profile.d.ts +1 -0
  166. package/dist/knowledge/profile.d.ts.map +1 -1
  167. package/dist/knowledge/profile.js +1 -0
  168. package/dist/knowledge/profile.js.map +1 -1
  169. package/package.json +1 -1
  170. package/src/cli/args.ts +39 -27
  171. package/src/cli/check.ts +10 -3
  172. package/src/cli/config.ts +14 -11
  173. package/src/cli/exit-codes.ts +0 -2
  174. package/src/cli/help.ts +10 -2
  175. package/src/cli/index.ts +3 -1
  176. package/src/cli/inspect.ts +88 -269
  177. package/src/cli/plan-turn.ts +350 -0
  178. package/src/cli/refuse.ts +2 -1
  179. package/src/cli/resolve-harness.ts +4 -5
  180. package/src/cli/run.ts +36 -399
  181. package/src/cli/session-json.ts +1 -6
  182. package/src/cli/session.ts +51 -44
  183. package/src/cli/skills-root.ts +1 -4
  184. package/src/execution/decode.ts +10 -36
  185. package/src/execution/events.ts +4 -1
  186. package/src/execution/failure.ts +18 -11
  187. package/src/execution/open-session.ts +154 -228
  188. package/src/execution/stream-turn.ts +90 -193
  189. package/src/execution/supervisor.ts +199 -0
  190. package/src/interpretation/argv.ts +65 -34
  191. package/src/interpretation/content.ts +30 -13
  192. package/src/interpretation/hints.ts +2 -12
  193. package/src/interpretation/index.ts +0 -1
  194. package/src/interpretation/isolation.ts +39 -0
  195. package/src/interpretation/limits.ts +14 -75
  196. package/src/interpretation/question.ts +46 -0
  197. package/src/interpretation/refusal.ts +28 -18
  198. package/src/interpretation/resolve-options.ts +98 -33
  199. package/src/interpretation/session-input.ts +88 -5
  200. package/src/interpretation/shape.ts +10 -0
  201. package/src/interpretation/skills-selection.ts +70 -74
  202. package/src/interpretation/support.ts +21 -6
  203. package/src/interpretation/tool-selection.ts +67 -122
  204. package/src/interpretation/tool-vocabulary.ts +48 -53
  205. package/src/interpretation/turn-options.ts +125 -220
  206. package/src/interpretation/vocabulary.ts +15 -10
  207. package/src/knowledge/claude-code.ts +17 -4
  208. package/src/knowledge/codex.ts +31 -7
  209. package/src/knowledge/descriptor.ts +106 -49
  210. package/src/knowledge/matchers.ts +35 -0
  211. package/src/knowledge/muse.ts +13 -6
  212. package/src/knowledge/overrides.ts +17 -32
  213. package/src/knowledge/pi.ts +8 -6
  214. package/src/knowledge/profile.ts +1 -0
  215. package/dist/interpretation/dimensions.d.ts +0 -11
  216. package/dist/interpretation/dimensions.d.ts.map +0 -1
  217. package/dist/interpretation/dimensions.js +0 -4
  218. package/dist/interpretation/dimensions.js.map +0 -1
  219. 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 { KILL_GRACE_MS, redactArgv, streamTurn } from "../execution/stream-turn.js";
4
- import { buildLaunchArgv, buildResumeArgv } from "../interpretation/argv.js";
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 { FloorExceededError, resolveEffectiveOptions } from "../interpretation/resolve-options.js";
8
- import { recognizeNativeSpelling, supportedBy } from "../interpretation/support.js";
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
- const { parseCommonFlags, detectPositionalPromptInjection, splitPassthrough } = await import(
26
- "./args.js"
27
- );
28
- const { normalized, passthrough } = splitPassthrough(rawArgs);
29
- // Decided before any refusal can fire: a refused --json run still owes
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 values = parsed.values as Record<string, unknown>;
106
- const positionals = parsed.positionals as string[];
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
- if (fullOpts.resume !== undefined && h.resume.onMissing === "create") {
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: extra.cwd ?? process.cwd(),
378
- sessionId: fullOpts.resume,
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 ${fullOpts.resume} found at ${path}`,
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
- // Delete HERDR_ENV before spawn
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. Precedence arg > project > user (no
397
- // profile entry by ratification). 0 = explicit disable.
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
- const _currentProc: { signal: (sig: "SIGTERM" | "SIGKILL") => void } | null = null;
410
- // We'll need to track the spawned process via deps.signal; but streamTurn owns process handle.
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: import("../execution/deps.js").SpawnedProcess, sig: "SIGTERM" | "SIGKILL") => {
75
+ signal: (proc: SpawnedProcess, sig: "SIGTERM" | "SIGKILL") => {
419
76
  lastProc = proc;
420
77
  originalSignal(proc, sig);
421
78
  },
422
- spawn: (argv: readonly string[], opts: import("../execution/deps.js").SpawnOptions) => {
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
- // streamTurn handles both launch and resume via TurnRunOptions
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
- if (event.kind === "done") {
471
- if (event.cause === "clean" || event.cause === "awaiting-input") exitCode = 0;
472
- else exitCode = 1;
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
- process.stderr.write(`${err.message}\n`);
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 = 1;
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 = 1;
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 ? 1 : 0;
169
+ process.exitCode = interrupted ? EXIT_FAILURE : 0;
533
170
  }
534
171
 
535
- if (interrupted && process.exitCode === 0) process.exitCode = 1;
172
+ if (interrupted && process.exitCode === 0) process.exitCode = EXIT_FAILURE;
536
173
  };
@@ -16,7 +16,7 @@ import {
16
16
  type SessionHandle,
17
17
  type SessionSendResult,
18
18
  } from "../execution/open-session.js";
19
- import type { QuestionMode } from "../interpretation/question.js";
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));