@dungle-scrubs/harness-cli-normalizer 0.3.1 → 0.4.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.
Files changed (112) hide show
  1. package/README.md +64 -86
  2. package/dist/cli/args.d.ts +12 -0
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +44 -5
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/config.d.ts +25 -0
  7. package/dist/cli/config.d.ts.map +1 -0
  8. package/dist/cli/config.js +147 -0
  9. package/dist/cli/config.js.map +1 -0
  10. package/dist/cli/help.d.ts +1 -1
  11. package/dist/cli/help.d.ts.map +1 -1
  12. package/dist/cli/help.js +17 -1
  13. package/dist/cli/help.js.map +1 -1
  14. package/dist/cli/inspect.d.ts.map +1 -1
  15. package/dist/cli/inspect.js +35 -1
  16. package/dist/cli/inspect.js.map +1 -1
  17. package/dist/cli/render.d.ts.map +1 -1
  18. package/dist/cli/render.js +10 -1
  19. package/dist/cli/render.js.map +1 -1
  20. package/dist/cli/run.d.ts.map +1 -1
  21. package/dist/cli/run.js +175 -5
  22. package/dist/cli/run.js.map +1 -1
  23. package/dist/cli/skills-root.d.ts +4 -0
  24. package/dist/cli/skills-root.d.ts.map +1 -0
  25. package/dist/cli/skills-root.js +45 -0
  26. package/dist/cli/skills-root.js.map +1 -0
  27. package/dist/execution/failure.d.ts +24 -4
  28. package/dist/execution/failure.d.ts.map +1 -1
  29. package/dist/execution/failure.js +39 -3
  30. package/dist/execution/failure.js.map +1 -1
  31. package/dist/execution/stream-turn.d.ts +4 -0
  32. package/dist/execution/stream-turn.d.ts.map +1 -1
  33. package/dist/execution/stream-turn.js +43 -6
  34. package/dist/execution/stream-turn.js.map +1 -1
  35. package/dist/interpretation/argv.d.ts +6 -0
  36. package/dist/interpretation/argv.d.ts.map +1 -1
  37. package/dist/interpretation/argv.js +17 -19
  38. package/dist/interpretation/argv.js.map +1 -1
  39. package/dist/interpretation/hints.d.ts +22 -0
  40. package/dist/interpretation/hints.d.ts.map +1 -0
  41. package/dist/interpretation/hints.js +59 -0
  42. package/dist/interpretation/hints.js.map +1 -0
  43. package/dist/interpretation/refusal.d.ts +25 -4
  44. package/dist/interpretation/refusal.d.ts.map +1 -1
  45. package/dist/interpretation/refusal.js +16 -0
  46. package/dist/interpretation/refusal.js.map +1 -1
  47. package/dist/interpretation/resolve-options.d.ts +42 -0
  48. package/dist/interpretation/resolve-options.d.ts.map +1 -0
  49. package/dist/interpretation/resolve-options.js +162 -0
  50. package/dist/interpretation/resolve-options.js.map +1 -0
  51. package/dist/interpretation/skills-selection.d.ts +22 -0
  52. package/dist/interpretation/skills-selection.d.ts.map +1 -0
  53. package/dist/interpretation/skills-selection.js +49 -0
  54. package/dist/interpretation/skills-selection.js.map +1 -0
  55. package/dist/interpretation/support.d.ts +32 -0
  56. package/dist/interpretation/support.d.ts.map +1 -0
  57. package/dist/interpretation/support.js +107 -0
  58. package/dist/interpretation/support.js.map +1 -0
  59. package/dist/interpretation/tool-selection.d.ts +29 -0
  60. package/dist/interpretation/tool-selection.d.ts.map +1 -0
  61. package/dist/interpretation/tool-selection.js +122 -0
  62. package/dist/interpretation/tool-selection.js.map +1 -0
  63. package/dist/interpretation/turn-options.d.ts.map +1 -1
  64. package/dist/interpretation/turn-options.js +4 -0
  65. package/dist/interpretation/turn-options.js.map +1 -1
  66. package/dist/knowledge/claude-code.d.ts.map +1 -1
  67. package/dist/knowledge/claude-code.js +35 -1
  68. package/dist/knowledge/claude-code.js.map +1 -1
  69. package/dist/knowledge/codex.d.ts.map +1 -1
  70. package/dist/knowledge/codex.js +20 -0
  71. package/dist/knowledge/codex.js.map +1 -1
  72. package/dist/knowledge/descriptor.d.ts +39 -0
  73. package/dist/knowledge/descriptor.d.ts.map +1 -1
  74. package/dist/knowledge/muse.d.ts.map +1 -1
  75. package/dist/knowledge/muse.js +20 -0
  76. package/dist/knowledge/muse.js.map +1 -1
  77. package/dist/knowledge/pi.d.ts.map +1 -1
  78. package/dist/knowledge/pi.js +22 -0
  79. package/dist/knowledge/pi.js.map +1 -1
  80. package/dist/knowledge/profile.d.ts +16 -0
  81. package/dist/knowledge/profile.d.ts.map +1 -0
  82. package/dist/knowledge/profile.js +51 -0
  83. package/dist/knowledge/profile.js.map +1 -0
  84. package/package.json +2 -23
  85. package/src/cli/args.ts +64 -3
  86. package/src/cli/config.ts +176 -0
  87. package/src/cli/help.ts +17 -1
  88. package/src/cli/inspect.ts +34 -1
  89. package/src/cli/render.ts +10 -1
  90. package/src/cli/run.ts +203 -6
  91. package/src/cli/skills-root.ts +47 -0
  92. package/src/execution/failure.ts +58 -5
  93. package/src/execution/stream-turn.ts +50 -5
  94. package/src/interpretation/argv.ts +23 -19
  95. package/src/interpretation/hints.ts +99 -0
  96. package/src/interpretation/refusal.ts +33 -3
  97. package/src/interpretation/resolve-options.ts +214 -0
  98. package/src/interpretation/skills-selection.ts +73 -0
  99. package/src/interpretation/support.ts +119 -0
  100. package/src/interpretation/tool-selection.ts +161 -0
  101. package/src/interpretation/turn-options.ts +4 -0
  102. package/src/knowledge/claude-code.ts +35 -1
  103. package/src/knowledge/codex.ts +20 -0
  104. package/src/knowledge/descriptor.ts +39 -0
  105. package/src/knowledge/muse.ts +20 -0
  106. package/src/knowledge/pi.ts +22 -0
  107. package/src/knowledge/profile.ts +53 -0
  108. package/dist/index.d.ts +0 -8
  109. package/dist/index.d.ts.map +0 -1
  110. package/dist/index.js +0 -8
  111. package/dist/index.js.map +0 -1
  112. package/src/index.ts +0 -7
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dungle-scrubs/harness-cli-normalizer",
3
- "version": "0.3.1",
4
- "description": "One stable interface to four coding-agent CLIs.",
3
+ "version": "0.4.1",
4
+ "description": "One stable CLI to four coding-agent harnesses: normalized flags, defaults, and events.",
5
5
  "license": "MIT",
6
6
  "author": "Kevin Frilot",
7
7
  "repository": {
@@ -13,27 +13,6 @@
13
13
  "bin": {
14
14
  "hcn": "./dist/cli.js"
15
15
  },
16
- "main": "./dist/index.js",
17
- "types": "./dist/index.d.ts",
18
- "exports": {
19
- ".": {
20
- "types": "./dist/index.d.ts",
21
- "import": "./dist/index.js"
22
- },
23
- "./knowledge": {
24
- "types": "./dist/knowledge/index.d.ts",
25
- "import": "./dist/knowledge/index.js"
26
- },
27
- "./interpretation": {
28
- "types": "./dist/interpretation/index.d.ts",
29
- "import": "./dist/interpretation/index.js"
30
- },
31
- "./execution": {
32
- "types": "./dist/execution/index.d.ts",
33
- "import": "./dist/execution/index.js"
34
- },
35
- "./package.json": "./package.json"
36
- },
37
16
  "files": [
38
17
  "dist",
39
18
  "src"
package/src/cli/args.ts CHANGED
@@ -162,6 +162,15 @@ export const parseTurnOptions = (values: Record<string, unknown>): TurnOptions =
162
162
  const raw = String(values.tools);
163
163
  opts.tools = raw.length === 0 ? [] : raw.split(",").map((s) => s.trim());
164
164
  }
165
+ if (values["exclude-tools"] !== undefined) {
166
+ const raw = String(values["exclude-tools"]);
167
+ opts.excludeTools = raw.length === 0 ? [] : raw.split(",").map((s) => s.trim());
168
+ }
169
+ if (values.skills !== undefined) {
170
+ const raw = String(values.skills);
171
+ (opts as Record<string, unknown>).skills =
172
+ raw.length === 0 ? [] : raw.split(",").map((s) => s.trim());
173
+ }
165
174
  if (values.autonomy === true) opts.autonomy = true;
166
175
  else if (values["no-autonomy"] === true) opts.autonomy = false;
167
176
  else if (values.autonomy === false) opts.autonomy = false; // for completeness
@@ -212,8 +221,31 @@ export const parseTurnOptions = (values: Record<string, unknown>): TurnOptions =
212
221
 
213
222
  export const parseRunExtra = (
214
223
  values: Record<string, unknown>,
215
- ): { cwd?: string; env?: Record<string, string>; resume?: string } => {
216
- const extra: { cwd?: string; env?: Record<string, string>; resume?: string } = {};
224
+ ): {
225
+ cwd?: string;
226
+ env?: Record<string, string>;
227
+ resume?: string;
228
+ timeoutSeconds?: number;
229
+ } => {
230
+ const extra: {
231
+ cwd?: string;
232
+ env?: Record<string, string>;
233
+ resume?: string;
234
+ timeoutSeconds?: number;
235
+ } = {};
236
+ if (values.timeout !== undefined) {
237
+ const n = Number(values.timeout);
238
+ if (!Number.isFinite(n) || n < 0 || !Number.isInteger(n)) {
239
+ throw new ArgvRefusalError({
240
+ issue: "invalid-option-value",
241
+ harness: "claude",
242
+ option: "maxSteps",
243
+ supported: ["whole seconds, >= 0 (0 disables)"],
244
+ detail: String(values.timeout),
245
+ });
246
+ }
247
+ extra.timeoutSeconds = n;
248
+ }
217
249
  if (values.cwd !== undefined) extra.cwd = String(values.cwd);
218
250
  if (values.resume !== undefined) extra.resume = String(values.resume);
219
251
  if (values["session-id"] !== undefined) extra.resume = String(values["session-id"]);
@@ -238,6 +270,7 @@ const KNOWN_FLAGS = new Set([
238
270
  "--sandbox",
239
271
  "--provider",
240
272
  "--tools",
273
+ "--exclude-tools",
241
274
  "--autonomy",
242
275
  "--no-autonomy",
243
276
  "--write",
@@ -269,11 +302,13 @@ const FLAGS_WITH_VALUE = new Set([
269
302
  "--sandbox",
270
303
  "--provider",
271
304
  "--tools",
305
+ "--exclude-tools",
272
306
  "--max-steps",
273
307
  "--cwd",
274
308
  "--env",
275
309
  "--resume",
276
310
  "--session-id",
311
+ "--timeout",
277
312
  ]);
278
313
 
279
314
  export const detectPositionalPromptInjection = (argv: string[]): string | null => {
@@ -339,11 +374,34 @@ const preprocessPromptArgs = (argv: string[]): string[] => {
339
374
  * Wrapper around node:util parseArgs for the common flag table.
340
375
  * Throws with exit code 2 on unknown flag.
341
376
  */
377
+ export interface SplitPassthrough {
378
+ /** Tokens before the separator - hcn's normalized surface. */
379
+ readonly normalized: string[];
380
+ /** Tokens after the separator - verbatim passthrough to the harness,
381
+ * empty when no separator was present. */
382
+ readonly passthrough: readonly string[];
383
+ }
384
+
385
+ /** D6: split argv at the first bare `--`. Everything after it belongs to
386
+ * the harness, not to hcn - wrong-harness flags there fail in the harness
387
+ * itself and surface as native errors, never as hcn refusals. */
388
+ export const splitPassthrough = (argv: readonly string[]): SplitPassthrough => {
389
+ const idx = argv.indexOf("--");
390
+ if (idx === -1) return { normalized: [...argv], passthrough: [] };
391
+ return {
392
+ normalized: argv.slice(0, idx),
393
+ passthrough: argv.slice(idx + 1),
394
+ };
395
+ };
396
+
342
397
  export const parseCommonFlags = (
343
398
  argv: string[],
344
399
  opts: { strict?: boolean } = {},
345
400
  ): ReturnType<typeof parseArgs> => {
346
- const normalized = preprocessPromptArgs(argv);
401
+ // The separator itself never reaches parseArgs: passthrough tokens may
402
+ // be unknown to hcn by design (that is their purpose).
403
+ const { normalized: withoutPassthrough } = splitPassthrough(argv);
404
+ const normalized = preprocessPromptArgs(withoutPassthrough);
347
405
  const config = {
348
406
  allowPositionals: true,
349
407
  strict: opts.strict ?? true,
@@ -355,6 +413,8 @@ export const parseCommonFlags = (
355
413
  sandbox: { type: "string" as const },
356
414
  provider: { type: "string" as const },
357
415
  tools: { type: "string" as const },
416
+ "exclude-tools": { type: "string" as const },
417
+ skills: { type: "string" as const },
358
418
  autonomy: { type: "boolean" as const },
359
419
  "no-autonomy": { type: "boolean" as const },
360
420
  write: { type: "boolean" as const },
@@ -370,6 +430,7 @@ export const parseCommonFlags = (
370
430
  env: { type: "string" as const, multiple: true },
371
431
  resume: { type: "string" as const },
372
432
  "session-id": { type: "string" as const },
433
+ timeout: { type: "string" as const },
373
434
  json: { type: "boolean" as const },
374
435
  argv: { type: "boolean" as const },
375
436
  help: { type: "boolean" as const },
@@ -0,0 +1,176 @@
1
+ /**
2
+ * User-level config: ~/.config/hcn/config.json (or $HCN_CONFIG_DIR for
3
+ * tests). Hard-fail discipline: unknown keys, malformed JSON, and schema
4
+ * version mismatches exit 2 naming the offender - a config that silently
5
+ * no-ops is a safety hole in a tool whose config can tighten a sandbox.
6
+ *
7
+ * Schema is intentionally minimal and mirrors the normalized option
8
+ * surface; it grows as dimensions ratify. The parser validates SHAPE
9
+ * (key known, value type plausible); value legality (effort in ladder,
10
+ * model in vocabulary) is enforced by the same renderers as args, so
11
+ * config and CLI can never disagree about what is legal.
12
+ */
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { dirname, join, resolve } from "node:path";
16
+ import type { TurnOptions } from "../interpretation/argv.js";
17
+
18
+ const SCHEMA_VERSION = 1;
19
+
20
+ export class ConfigError extends Error {
21
+ constructor(message: string) {
22
+ super(message);
23
+ this.name = "ConfigError";
24
+ }
25
+ }
26
+
27
+ export const userConfigPath = (): string =>
28
+ join(
29
+ process.env.HCN_CONFIG_DIR ??
30
+ join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "hcn"),
31
+ "config.json",
32
+ );
33
+
34
+ /** Project config: `.hcn/config.json` at the git root (P4 ratified as A -
35
+ * auto-discover, no trust gate; the config is code-reviewed like AGENTS.md
36
+ * and every project-tier provenance line names its path). Null outside a
37
+ * repo or when the repo declares none. */
38
+ export const projectConfigPath = (startDir: string = process.cwd()): string | null => {
39
+ let dir = resolve(startDir);
40
+ for (;;) {
41
+ if (existsSync(join(dir, ".hcn", "config.json"))) return join(dir, ".hcn", "config.json");
42
+ const parent = dirname(dir);
43
+ if (parent === dir) return null;
44
+ // stop at the git root: above it is not this project
45
+ if (existsSync(join(dir, ".git")) && !existsSync(join(parent, ".git"))) {
46
+ return null;
47
+ }
48
+ dir = parent;
49
+ }
50
+ };
51
+
52
+ type MutableTurnOptions = {
53
+ [K in keyof TurnOptions]?: TurnOptions[K];
54
+ };
55
+
56
+ const KNOWN_KEYS = new Set([
57
+ "effort",
58
+ "model",
59
+ "provider",
60
+ "sandbox",
61
+ "tools",
62
+ "excludeTools",
63
+ "autonomy",
64
+ "write",
65
+ "shell",
66
+ "maxSteps",
67
+ "toolsets",
68
+ "timeout",
69
+ ]);
70
+
71
+ const LIST_KEYS = new Set(["tools", "excludeTools"]);
72
+ const BOOL_KEYS = new Set(["autonomy", "write", "shell"]);
73
+
74
+ /** Parse + validate config text. Throws ConfigError with the offending key
75
+ * named on any violation - never warns and continues. */
76
+ export const parseUserConfig = (text: string): Partial<TurnOptions> => {
77
+ let raw: unknown;
78
+ try {
79
+ raw = JSON.parse(text);
80
+ } catch (e) {
81
+ throw new ConfigError(`config is not valid JSON: ${(e as Error).message}`);
82
+ }
83
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
84
+ throw new ConfigError("config root must be a JSON object");
85
+ }
86
+ const obj = raw as Record<string, unknown>;
87
+
88
+ if (obj.version === undefined) {
89
+ throw new ConfigError("config missing required field: version");
90
+ }
91
+ if (obj.version !== SCHEMA_VERSION) {
92
+ throw new ConfigError(
93
+ `config version ${JSON.stringify(obj.version)} not supported (expected ${SCHEMA_VERSION})`,
94
+ );
95
+ }
96
+ delete obj.version;
97
+
98
+ const out: MutableTurnOptions = {};
99
+ for (const [key, value] of Object.entries(obj)) {
100
+ if (!KNOWN_KEYS.has(key)) {
101
+ throw new ConfigError(`unknown config key: ${JSON.stringify(key)}`);
102
+ }
103
+ if (key === "toolsets") {
104
+ if (
105
+ typeof value !== "object" ||
106
+ value === null ||
107
+ Array.isArray(value) ||
108
+ Object.entries(value).some(
109
+ ([, v]) => !Array.isArray(v) || v.some((t) => typeof t !== "string" || t.trim() === ""),
110
+ )
111
+ ) {
112
+ throw new ConfigError(
113
+ 'config key "toolsets" must be an object of name -> non-empty string array',
114
+ );
115
+ }
116
+ (out as Record<string, unknown>)[key] = value;
117
+ continue;
118
+ }
119
+ if (key === "timeout") {
120
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
121
+ throw new ConfigError(
122
+ 'config key "timeout" must be a whole number of seconds, >= 0 (0 disables)',
123
+ );
124
+ }
125
+ (out as Record<string, unknown>)[key] = value;
126
+ continue;
127
+ }
128
+ if (LIST_KEYS.has(key)) {
129
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
130
+ throw new ConfigError(`config key ${JSON.stringify(key)} must be an array of strings`);
131
+ }
132
+ (out as Record<string, unknown>)[key] = value;
133
+ continue;
134
+ }
135
+ if (BOOL_KEYS.has(key)) {
136
+ if (typeof value !== "boolean") {
137
+ throw new ConfigError(`config key ${JSON.stringify(key)} must be a boolean`);
138
+ }
139
+ (out as Record<string, unknown>)[key] = value;
140
+ continue;
141
+ }
142
+ if (typeof value !== "string" && typeof value !== "number") {
143
+ throw new ConfigError(
144
+ `config key ${JSON.stringify(key)} must be a string or number, got ${typeof value}`,
145
+ );
146
+ }
147
+ (out as Record<string, unknown>)[key] = value;
148
+ }
149
+ return out as Partial<TurnOptions>;
150
+ };
151
+
152
+ /** Load the user config if the file exists; absent file is an empty config
153
+ * (no tiers engaged), unreadable or invalid file is a hard error. */
154
+ export const loadUserConfig = (): { config: Partial<TurnOptions>; path: string } | null =>
155
+ loadConfigAt(userConfigPath());
156
+
157
+ /** Load the project config (git-root auto-discovery, ratified A). */
158
+ export const loadProjectConfig = (
159
+ startDir?: string,
160
+ ): { config: Partial<TurnOptions>; path: string } | null => {
161
+ const path = projectConfigPath(startDir);
162
+ if (path === null) return null;
163
+ return loadConfigAt(path);
164
+ };
165
+
166
+ const loadConfigAt = (path: string): { config: Partial<TurnOptions>; path: string } | null => {
167
+ let text: string;
168
+ try {
169
+ text = readFileSync(path, "utf8");
170
+ } catch (e) {
171
+ const code = (e as NodeJS.ErrnoException).code;
172
+ if (code === "ENOENT") return null;
173
+ throw new ConfigError(`cannot read config at ${path}: ${(e as Error).message}`);
174
+ }
175
+ return { config: parseUserConfig(text), path };
176
+ };
package/src/cli/help.ts CHANGED
@@ -32,7 +32,10 @@ Options:
32
32
  --effort <value> Effort level (validated per harness/model)
33
33
  --sandbox <value> Sandbox mode (codex only)
34
34
  --provider <value> Provider (pi only)
35
- --tools <a,b> Tool grants (claude only, comma-separated)
35
+ --tools <a,b> Tool grant allowlist (claude, pi; a bare name
36
+ matching a configured toolset expands to it)
37
+ --exclude-tools <a,b> Complement over known tools (claude, pi;
38
+ mutually exclusive with --tools)
36
39
  --autonomy Enable autonomy flag (claude/codex/muse)
37
40
  --no-autonomy Disable autonomy
38
41
  --write Enable write (muse)
@@ -40,6 +43,8 @@ Options:
40
43
  --shell Enable shell (muse)
41
44
  --no-shell Disable shell
42
45
  --max-steps <n> Max steps (muse, 1-10000)
46
+ --timeout <seconds> Wall-clock budget for the run (all harnesses,
47
+ hcn-enforced; 0 disables; no default)
43
48
  --no-tools Disable tools discovery facet
44
49
  --no-instruction-files Disable instructionFiles discovery facet
45
50
  --no-extensions Disable extensions discovery facet
@@ -47,9 +52,20 @@ Options:
47
52
  --cwd <path> Working directory for spawn
48
53
  --env KEY=VAL Environment (repeatable; KEY= deletes)
49
54
  --resume <uuid> Resume session id (UUID)
55
+ -- Passthrough: native harness args verbatim
56
+ (failures surface as labeled native errors)
50
57
  --json NDJSON HarnessEvent to stdout
51
58
  -h, --help Show help
52
59
  -V, --version Show version
60
+
61
+ Defaults with no flags:
62
+ Every launch resolves args > project config (.hcn/config.json at the
63
+ git root) > user config (~/.config/hcn/config.json) > built-in profile
64
+ > harness default. The profile pins: effort medium, sandbox
65
+ workspace-write (codex only; other harnesses report divergence),
66
+ discovery on, autonomy off, write/shell on. timeout and max-steps have
67
+ no default. Provenance prints to stderr on every run; see
68
+ 'hcn inspect <harness>' for the resolved argv of a bare run.
53
69
  `;
54
70
 
55
71
  export const SESSION_HELP = `hcn session - Interactive session (Claude-only)
@@ -1,6 +1,7 @@
1
1
  import { redactArgv } from "../execution/stream-turn.js";
2
2
  import { buildLaunchArgv } from "../interpretation/argv.js";
3
3
  import { ArgvRefusalError } from "../interpretation/refusal.js";
4
+ import { FloorExceededError, resolveEffectiveOptions } from "../interpretation/resolve-options.js";
4
5
  import { parseTurnOptions, resolvePromptAsync } from "./args.js";
5
6
  import { resolveHarness } from "./resolve-harness.js";
6
7
 
@@ -123,8 +124,40 @@ export const inspect = async (harnessName: string, rawArgs: string[]): Promise<v
123
124
  throw err;
124
125
  }
125
126
 
127
+ // Inspect resolves exactly as a launch would: profile + user config,
128
+ // launch-only semantics, so --argv previews the truth of a bare run.
129
+ const { loadUserConfig, loadProjectConfig, ConfigError } = await import("./config.js");
130
+ const tiers: {
131
+ user?: Partial<ReturnType<typeof parseTurnOptions>>;
132
+ project?: Partial<ReturnType<typeof parseTurnOptions>>;
133
+ } = {};
134
+ try {
135
+ const loaded = loadUserConfig();
136
+ if (loaded !== null) tiers.user = loaded.config;
137
+ const proj = loadProjectConfig();
138
+ if (proj !== null) tiers.project = proj.config;
139
+ } catch (configErr) {
140
+ if (configErr instanceof ConfigError) {
141
+ process.stderr.write(`config error: ${(configErr as Error).message}\n`);
142
+ process.exitCode = 2;
143
+ return;
144
+ }
145
+ throw configErr;
146
+ }
147
+ let resolved: ReturnType<typeof resolveEffectiveOptions>;
148
+ try {
149
+ resolved = resolveEffectiveOptions(h, { ...turnOpts, prompt } as never, tiers);
150
+ } catch (resErr) {
151
+ if (resErr instanceof FloorExceededError) {
152
+ process.stderr.write(`${(resErr as Error).message}\n`);
153
+ process.exitCode = 2;
154
+ return;
155
+ }
156
+ throw resErr;
157
+ }
158
+ const { prompt: _p, ...effectiveRest } = resolved.options as { prompt: string };
126
159
  const fullOpts = {
127
- ...turnOpts,
160
+ ...(effectiveRest as object),
128
161
  prompt,
129
162
  ...(promptSource !== "positional" ? { __explicitPrompt: true as const } : {}),
130
163
  } as Parameters<typeof buildLaunchArgv>[1];
package/src/cli/render.ts CHANGED
@@ -41,7 +41,16 @@ export const renderEvent = (event: HarnessEvent, state: RenderState): void => {
41
41
  break;
42
42
  case "failure": {
43
43
  const detail = event.message ?? `${event.class}`;
44
- process.stdout.write(red(`\n ✗ failure ${event.class}: ${detail}`));
44
+ // D6: native failures get an unmistakable prefix and the native exit
45
+ // code shown as data, so a human never reads a harness error as an
46
+ // hcn error.
47
+ if (event.class === "native") {
48
+ const nat =
49
+ event.nativeExitCode !== undefined ? ` [native exit ${event.nativeExitCode}]` : "";
50
+ process.stdout.write(red(`\n ✗ NATIVE${nat} ${detail}`));
51
+ } else {
52
+ process.stdout.write(red(`\n ✗ failure ${event.class}: ${detail}`));
53
+ }
45
54
  break;
46
55
  }
47
56
  case "done": {