@dungle-scrubs/harness-cli-normalizer 0.6.0 → 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.
Files changed (212) hide show
  1. package/README.md +18 -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 +31 -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 +6 -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 +6 -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 +266 -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 +25 -44
  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 +38 -12
  75. package/dist/interpretation/argv.d.ts.map +1 -1
  76. package/dist/interpretation/argv.js +26 -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/limits.d.ts +2 -5
  91. package/dist/interpretation/limits.d.ts.map +1 -1
  92. package/dist/interpretation/limits.js +8 -62
  93. package/dist/interpretation/limits.js.map +1 -1
  94. package/dist/interpretation/question.d.ts +27 -0
  95. package/dist/interpretation/question.d.ts.map +1 -1
  96. package/dist/interpretation/question.js +28 -0
  97. package/dist/interpretation/question.js.map +1 -1
  98. package/dist/interpretation/refusal.d.ts +10 -5
  99. package/dist/interpretation/refusal.d.ts.map +1 -1
  100. package/dist/interpretation/refusal.js +24 -15
  101. package/dist/interpretation/refusal.js.map +1 -1
  102. package/dist/interpretation/resolve-options.d.ts +36 -2
  103. package/dist/interpretation/resolve-options.d.ts.map +1 -1
  104. package/dist/interpretation/resolve-options.js +38 -21
  105. package/dist/interpretation/resolve-options.js.map +1 -1
  106. package/dist/interpretation/session-input.d.ts +44 -1
  107. package/dist/interpretation/session-input.d.ts.map +1 -1
  108. package/dist/interpretation/session-input.js +59 -4
  109. package/dist/interpretation/session-input.js.map +1 -1
  110. package/dist/interpretation/shape.d.ts +1 -0
  111. package/dist/interpretation/shape.d.ts.map +1 -1
  112. package/dist/interpretation/shape.js +10 -0
  113. package/dist/interpretation/shape.js.map +1 -1
  114. package/dist/interpretation/skills-selection.d.ts +25 -33
  115. package/dist/interpretation/skills-selection.d.ts.map +1 -1
  116. package/dist/interpretation/skills-selection.js +41 -48
  117. package/dist/interpretation/skills-selection.js.map +1 -1
  118. package/dist/interpretation/support.d.ts.map +1 -1
  119. package/dist/interpretation/support.js +23 -8
  120. package/dist/interpretation/support.js.map +1 -1
  121. package/dist/interpretation/tool-selection.d.ts +7 -6
  122. package/dist/interpretation/tool-selection.d.ts.map +1 -1
  123. package/dist/interpretation/tool-selection.js +54 -116
  124. package/dist/interpretation/tool-selection.js.map +1 -1
  125. package/dist/interpretation/tool-vocabulary.d.ts +31 -12
  126. package/dist/interpretation/tool-vocabulary.d.ts.map +1 -1
  127. package/dist/interpretation/tool-vocabulary.js +19 -41
  128. package/dist/interpretation/tool-vocabulary.js.map +1 -1
  129. package/dist/interpretation/turn-options.d.ts +4 -0
  130. package/dist/interpretation/turn-options.d.ts.map +1 -1
  131. package/dist/interpretation/turn-options.js +105 -241
  132. package/dist/interpretation/turn-options.js.map +1 -1
  133. package/dist/interpretation/vocabulary.d.ts +8 -6
  134. package/dist/interpretation/vocabulary.d.ts.map +1 -1
  135. package/dist/interpretation/vocabulary.js +22 -9
  136. package/dist/interpretation/vocabulary.js.map +1 -1
  137. package/dist/knowledge/claude-code.js +3 -3
  138. package/dist/knowledge/claude-code.js.map +1 -1
  139. package/dist/knowledge/codex.d.ts.map +1 -1
  140. package/dist/knowledge/codex.js +31 -7
  141. package/dist/knowledge/codex.js.map +1 -1
  142. package/dist/knowledge/descriptor.d.ts +67 -36
  143. package/dist/knowledge/descriptor.d.ts.map +1 -1
  144. package/dist/knowledge/descriptor.js +49 -15
  145. package/dist/knowledge/descriptor.js.map +1 -1
  146. package/dist/knowledge/matchers.d.ts +12 -0
  147. package/dist/knowledge/matchers.d.ts.map +1 -1
  148. package/dist/knowledge/matchers.js +34 -0
  149. package/dist/knowledge/matchers.js.map +1 -1
  150. package/dist/knowledge/muse.d.ts.map +1 -1
  151. package/dist/knowledge/muse.js +7 -5
  152. package/dist/knowledge/muse.js.map +1 -1
  153. package/dist/knowledge/overrides.d.ts.map +1 -1
  154. package/dist/knowledge/overrides.js +9 -20
  155. package/dist/knowledge/overrides.js.map +1 -1
  156. package/dist/knowledge/pi.d.ts.map +1 -1
  157. package/dist/knowledge/pi.js +8 -6
  158. package/dist/knowledge/pi.js.map +1 -1
  159. package/dist/knowledge/profile.d.ts +1 -0
  160. package/dist/knowledge/profile.d.ts.map +1 -1
  161. package/dist/knowledge/profile.js +1 -0
  162. package/dist/knowledge/profile.js.map +1 -1
  163. package/package.json +1 -1
  164. package/src/cli/args.ts +35 -27
  165. package/src/cli/check.ts +10 -3
  166. package/src/cli/config.ts +12 -11
  167. package/src/cli/exit-codes.ts +0 -2
  168. package/src/cli/help.ts +6 -2
  169. package/src/cli/index.ts +3 -1
  170. package/src/cli/inspect.ts +88 -269
  171. package/src/cli/plan-turn.ts +346 -0
  172. package/src/cli/refuse.ts +2 -1
  173. package/src/cli/resolve-harness.ts +4 -5
  174. package/src/cli/run.ts +36 -399
  175. package/src/cli/session-json.ts +1 -6
  176. package/src/cli/session.ts +34 -44
  177. package/src/cli/skills-root.ts +1 -4
  178. package/src/execution/decode.ts +10 -36
  179. package/src/execution/events.ts +4 -1
  180. package/src/execution/failure.ts +18 -11
  181. package/src/execution/open-session.ts +154 -228
  182. package/src/execution/stream-turn.ts +90 -193
  183. package/src/execution/supervisor.ts +199 -0
  184. package/src/interpretation/argv.ts +62 -34
  185. package/src/interpretation/content.ts +30 -13
  186. package/src/interpretation/hints.ts +2 -12
  187. package/src/interpretation/index.ts +0 -1
  188. package/src/interpretation/limits.ts +14 -75
  189. package/src/interpretation/question.ts +46 -0
  190. package/src/interpretation/refusal.ts +28 -18
  191. package/src/interpretation/resolve-options.ts +85 -32
  192. package/src/interpretation/session-input.ts +88 -5
  193. package/src/interpretation/shape.ts +10 -0
  194. package/src/interpretation/skills-selection.ts +70 -74
  195. package/src/interpretation/support.ts +19 -6
  196. package/src/interpretation/tool-selection.ts +67 -122
  197. package/src/interpretation/tool-vocabulary.ts +48 -53
  198. package/src/interpretation/turn-options.ts +123 -220
  199. package/src/interpretation/vocabulary.ts +15 -10
  200. package/src/knowledge/claude-code.ts +3 -3
  201. package/src/knowledge/codex.ts +31 -7
  202. package/src/knowledge/descriptor.ts +105 -49
  203. package/src/knowledge/matchers.ts +35 -0
  204. package/src/knowledge/muse.ts +7 -5
  205. package/src/knowledge/overrides.ts +17 -32
  206. package/src/knowledge/pi.ts +8 -6
  207. package/src/knowledge/profile.ts +1 -0
  208. package/dist/interpretation/dimensions.d.ts +0 -11
  209. package/dist/interpretation/dimensions.d.ts.map +0 -1
  210. package/dist/interpretation/dimensions.js +0 -4
  211. package/dist/interpretation/dimensions.js.map +0 -1
  212. package/src/interpretation/dimensions.ts +0 -14
@@ -1,5 +1,8 @@
1
1
  /**
2
- * Tool-selection rendering with canonical vocabulary.
2
+ * Tool-selection rendering with canonical vocabulary. The descriptor's
3
+ * denySemantics says which shape a harness has; the toolMap helpers in
4
+ * tool-vocabulary.ts own the counterpart and native-name rules (RFC-02
5
+ * changes 7 and 8) - nothing here restates them.
3
6
  */
4
7
  import type { HarnessDescriptor } from "../knowledge/descriptor.js";
5
8
  import { defaultDescriptors } from "../knowledge/overrides.js";
@@ -10,19 +13,17 @@ import {
10
13
  allCanonicalNames,
11
14
  canonicalTable,
12
15
  hasCounterpart,
13
- NATIVE_PREFIX,
14
16
  nativeFor,
15
17
  parseToolSelector,
16
- READ_PRESET,
17
- validateCanonicalList,
18
+ type ToolMap,
18
19
  } from "./tool-vocabulary.js";
19
-
20
- export const TOOL_SELECTOR = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,127}$/;
20
+ import { CLEAN_SELECTOR } from "./vocabulary.js";
21
21
 
22
22
  export interface ToolSelection {
23
23
  readonly include?: readonly string[];
24
24
  readonly exclude?: readonly string[];
25
- readonly toolMap?: Readonly<Record<string, string>>;
25
+ /** The merged toolMap, every harness; the renderer reads its own entry. */
26
+ readonly toolMap?: ToolMap;
26
27
  }
27
28
 
28
29
  export interface RenderedToolSelection {
@@ -39,11 +40,11 @@ const validateInnerName = (h: HarnessDescriptor, name: string, raw: readonly str
39
40
  detail: `tools=${JSON.stringify(raw)}`,
40
41
  });
41
42
  }
42
- if (!TOOL_SELECTOR.test(name)) {
43
+ if (!CLEAN_SELECTOR.test(name)) {
43
44
  throw new ArgvRefusalError({
44
45
  issue: "invalid-tool-grant",
45
46
  harness: h.name,
46
- supported: [`must match ${TOOL_SELECTOR.source}`],
47
+ supported: [`must match ${CLEAN_SELECTOR.source}`],
47
48
  detail: name,
48
49
  });
49
50
  }
@@ -62,30 +63,6 @@ const categoriesFor = (
62
63
  return cats;
63
64
  };
64
65
 
65
- export const renderAccessPreset = (
66
- h: HarnessDescriptor,
67
- access: "read" | "write",
68
- opts: { toolMap?: Readonly<Record<string, string>> } = {},
69
- ): string[] => {
70
- if (access === "write") return [];
71
- // read preset
72
- const filtered = (READ_PRESET as readonly string[]).filter((c) => {
73
- if (opts.toolMap?.[c] !== undefined) return true;
74
- const table = canonicalTable(defaultDescriptors());
75
- return table[c]?.[h.name] !== undefined;
76
- });
77
- if (filtered.length === 0) return [];
78
- // For tool-preset harnesses, render via tool-selection include
79
- const spec = h.turnOptions.access;
80
- if (spec && spec.kind === "tool-preset") {
81
- const rendered = renderToolSelection(h, { include: filtered, toolMap: opts.toolMap });
82
- return [...rendered.tokens];
83
- }
84
- // For flag-value / flag-list-by-value harnesses, caller should handle via spec mapping;
85
- // we return empty here so turn-options can map via spec values.
86
- return [];
87
- };
88
-
89
66
  export const renderToolSelection = (
90
67
  h: HarnessDescriptor,
91
68
  selection: ToolSelection,
@@ -106,10 +83,11 @@ export const renderToolSelection = (
106
83
 
107
84
  const set = defaultDescriptors();
108
85
  const table = canonicalTable(set);
109
- const toolMap = selection.toolMap ?? {};
110
- const allCanonical = allCanonicalNames(set, {
111
- [h.name]: toolMap as unknown as Record<string, string>,
112
- } as unknown as import("./tool-vocabulary.js").ToolMap);
86
+ const toolMap = selection.toolMap;
87
+ // Names this harness's own toolMap entry adds count as canonical here;
88
+ // another harness's entries do not make a name expressible on this one.
89
+ const own = toolMap?.[h.name];
90
+ const allCanonical = allCanonicalNames(set, own === undefined ? undefined : { [h.name]: own });
113
91
 
114
92
  const splitSelection = (
115
93
  names: readonly string[],
@@ -140,10 +118,31 @@ export const renderToolSelection = (
140
118
  return { canonical, passthrough };
141
119
  };
142
120
 
143
- const isCodexShape = h.tools.includeFlag === null && h.tools.categories.length === 0;
144
- const isCategoryShape = h.tools.includeFlag === null && h.tools.categories.length > 0;
121
+ /** Refuse the first canonical this harness cannot express. */
122
+ const requireCounterparts = (
123
+ canonical: readonly string[],
124
+ option: "tools" | "excludeTools",
125
+ hint: (c: string) => string | undefined,
126
+ ): void => {
127
+ for (const c of canonical) {
128
+ if (hasCounterpart(table, c, h.name, toolMap)) continue;
129
+ throw new ArgvRefusalError({
130
+ issue: "unsupported-option",
131
+ harness: h.name,
132
+ option,
133
+ supported: ["per-tool name lists"],
134
+ supportedBy: supportedByCanonical(set, c),
135
+ hint: hint(c),
136
+ });
137
+ }
138
+ };
139
+
140
+ // The descriptor says how a deny lands (RFC-02 change 7): no lists at
141
+ // all (codex), a policy gate over category switches (muse), or removal
142
+ // from the name list (claude, pi). Nothing here re-derives that shape.
143
+ const semantics = h.tools.denySemantics;
145
144
 
146
- if (isCodexShape) {
145
+ if (semantics === "no-lists") {
147
146
  throw new ArgvRefusalError({
148
147
  issue: "unsupported-option",
149
148
  harness: h.name,
@@ -155,25 +154,12 @@ export const renderToolSelection = (
155
154
  "nearest control on codex: category switches via config keys (features.shell_tool, web_search) or sandbox modes - see `hcn inspect codex`",
156
155
  });
157
156
  }
158
- if (isCategoryShape) {
157
+ if (semantics === "policy-gate") {
159
158
  const names = hasInclude ? selection.include! : selection.exclude!;
160
159
  const { canonical, passthrough } = splitSelection(names);
161
160
 
162
161
  if (hasExclude) {
163
- // one helper replaces the three no-counterpart loops
164
- for (const c of canonical) {
165
- const has = toolMap[c] !== undefined || table[c]?.[h.name] !== undefined;
166
- if (!has) {
167
- throw new ArgvRefusalError({
168
- issue: "unsupported-option",
169
- harness: h.name,
170
- option: "excludeTools",
171
- supported: ["per-tool name lists"],
172
- supportedBy: supportedByCanonical(set, c),
173
- hint: hintFor(h.name, "excludeTools"),
174
- });
175
- }
176
- }
162
+ requireCounterparts(canonical, "excludeTools", () => hintFor(h.name, "excludeTools"));
177
163
  const cats = categoriesFor(h, canonical, table);
178
164
  const tokens: string[] = [];
179
165
  for (const cat of h.tools.categories) {
@@ -182,19 +168,7 @@ export const renderToolSelection = (
182
168
  return { tokens, passthrough };
183
169
  }
184
170
 
185
- for (const c of canonical) {
186
- const has = toolMap[c] !== undefined || table[c]?.[h.name] !== undefined;
187
- if (!has) {
188
- throw new ArgvRefusalError({
189
- issue: "unsupported-option",
190
- harness: h.name,
191
- option: "tools",
192
- supported: ["per-tool name lists"],
193
- supportedBy: supportedByCanonical(set, c),
194
- hint: hintFor(h.name, "tools"),
195
- });
196
- }
197
- }
171
+ requireCounterparts(canonical, "tools", () => hintFor(h.name, "tools"));
198
172
  if (canonical.length === 0) {
199
173
  const tokens = h.tools.categories.map((c) => c.disableFlag).filter((f): f is string => !!f);
200
174
  return { tokens, passthrough };
@@ -207,7 +181,14 @@ export const renderToolSelection = (
207
181
  return { tokens, passthrough };
208
182
  }
209
183
 
210
- // From here: h has list flags (claude, pi)
184
+ // From here: remove-from-set, the harness has name-list flags (claude, pi)
185
+ const toolMapHint = (c: string): string =>
186
+ `add toolMap.${h.name}.${c} to ~/.config/hcn/config.json or pass native:${c}`;
187
+ const nativeNames = (canonical: readonly string[]): string[] =>
188
+ canonical
189
+ .map((c) => nativeFor(table, c, h.name, toolMap))
190
+ .filter((n): n is string => n !== null);
191
+
211
192
  if (hasInclude) {
212
193
  const names = selection.include!;
213
194
  if (names.length === 0) {
@@ -224,41 +205,23 @@ export const renderToolSelection = (
224
205
  return { tokens: [h.tools.excludeFlag!, known.join(",")], passthrough: [] };
225
206
  }
226
207
  const { canonical, passthrough } = splitSelection(names);
227
- for (const c of canonical) {
228
- const has = toolMap[c] !== undefined || table[c]?.[h.name] !== undefined;
229
- if (!has) {
230
- throw new ArgvRefusalError({
231
- issue: "unsupported-option",
232
- harness: h.name,
233
- option: "tools",
234
- supported: ["per-tool name lists"],
235
- supportedBy: supportedByCanonical(set, c),
236
- hint: `add toolMap.${h.name}.${c} to ~/.config/hcn/config.json or pass native:${c}`,
237
- });
238
- }
239
- }
240
- const mapped: string[] = [];
241
- for (const c of canonical) {
242
- let n: string | null = toolMap[c] ?? null;
243
- if (n === null) {
244
- const entry = table[c]?.[h.name];
245
- if (entry?.kind === "builtin") n = entry.native;
246
- }
247
- if (n) mapped.push(n);
248
- }
208
+ requireCounterparts(canonical, "tools", toolMapHint);
209
+ const mapped = nativeNames(canonical);
249
210
  if (!h.tools.includeIsStrictAllowlist) {
250
- // claude's include flag pre-approves without restricting, so an exact allowlist renders as the deny complement
211
+ // claude's include flag pre-approves without restricting, so an exact
212
+ // allowlist renders as the deny complement - always, even when the
213
+ // complement is empty.
251
214
  const known = h.tools.builtins.map((t) => t.name);
252
215
  const excluded = known.filter((n) => !mapped.includes(n));
253
- const tokens: string[] = [h.tools.includeFlag!, [...mapped, ...passthrough].join(",")];
254
- // only add exclude if needed? original pushed exclude even when? Keep original behavior: push excludeFlag excluded
255
- if (excluded.length > 0 || passthrough.length > 0) {
256
- tokens.push(h.tools.excludeFlag!, excluded.join(","));
257
- } else {
258
- // when all tools included, exclude nothing? but keep token? original pushed exclude always
259
- tokens.push(h.tools.excludeFlag!, excluded.join(","));
260
- }
261
- return { tokens, passthrough };
216
+ return {
217
+ tokens: [
218
+ h.tools.includeFlag!,
219
+ [...mapped, ...passthrough].join(","),
220
+ h.tools.excludeFlag!,
221
+ excluded.join(","),
222
+ ],
223
+ passthrough,
224
+ };
262
225
  }
263
226
  return {
264
227
  tokens: [h.tools.includeFlag!, [...mapped, ...passthrough].join(",")],
@@ -269,24 +232,10 @@ export const renderToolSelection = (
269
232
  // exclude
270
233
  const names = selection.exclude!;
271
234
  const { canonical, passthrough: excludedPassthrough } = splitSelection(names);
272
- for (const c of canonical) {
273
- const has = toolMap[c] !== undefined || table[c]?.[h.name] !== undefined;
274
- if (!has) {
275
- throw new ArgvRefusalError({
276
- issue: "unsupported-option",
277
- harness: h.name,
278
- option: "excludeTools",
279
- supported: ["per-tool name lists"],
280
- supportedBy: supportedByCanonical(set, c),
281
- hint: `add toolMap.${h.name}.${c} to ~/.config/hcn/config.json or pass native:${c}`,
282
- });
283
- }
284
- }
235
+ requireCounterparts(canonical, "excludeTools", toolMapHint);
236
+ const mapped = nativeNames(canonical);
285
237
  if (excludedPassthrough.length > 0) {
286
238
  if (!h.tools.includeIsStrictAllowlist) {
287
- const mapped = canonical.map(
288
- (c) => toolMap[c] ?? (table[c]?.[h.name] as { native: string } | undefined)?.native ?? c,
289
- );
290
239
  const tokens = [h.tools.excludeFlag!, [...mapped, ...excludedPassthrough].join(",")];
291
240
  return { tokens, passthrough: excludedPassthrough };
292
241
  }
@@ -298,11 +247,7 @@ export const renderToolSelection = (
298
247
  detail: `cannot exclude unknown name(s) ${excludedPassthrough.join(", ")}: the complement cannot be computed`,
299
248
  });
300
249
  }
301
- const mapped = canonical
302
- .map((c) => toolMap[c] ?? (table[c]?.[h.name] as { native: string } | undefined)?.native ?? "")
303
- .filter(Boolean);
304
- const known = h.tools.builtins.filter((t) => !mapped.includes(t.name));
305
- const kept = known.map((t) => t.name);
250
+ const kept = h.tools.builtins.filter((t) => !mapped.includes(t.name)).map((t) => t.name);
306
251
  if (!h.tools.includeIsStrictAllowlist) {
307
252
  return { tokens: [h.tools.excludeFlag!, mapped.join(",")], passthrough: [] };
308
253
  }
@@ -1,3 +1,13 @@
1
+ /**
2
+ * The canonical tool vocabulary and the toolMap that extends it.
3
+ *
4
+ * One toolMap shape (RFC-02 change 8): a config file carries the raw
5
+ * shape, harness -> canonical -> native, and `mergeToolMaps` folds the
6
+ * tiers into the one shape everything downstream reads, harness ->
7
+ * canonical -> the native name plus the tier that set it. Tool selection
8
+ * and the access preset call `hasCounterpart` and `nativeFor` instead of
9
+ * restating them.
10
+ */
1
11
  import type { DescriptorSet } from "../knowledge/overrides.js";
2
12
  import { ArgvRefusalError } from "./refusal.js";
3
13
 
@@ -16,20 +26,27 @@ export const parseToolSelector = (raw: string): ParsedSelector =>
16
26
  ? { kind: "native", name: raw.slice(NATIVE_PREFIX.length) }
17
27
  : { kind: "canonical", name: raw };
18
28
 
19
- export type ToolMap = Readonly<Record<string, Readonly<Record<string, string>>>>;
29
+ /** The raw shape a config file carries: harness -> canonical -> native. */
30
+ export type ToolMapConfig = Readonly<Record<string, Readonly<Record<string, string>>>>;
20
31
 
21
32
  export type ToolMapTier = "user-config" | "project-config";
22
33
 
23
- export type MergedToolMap = Readonly<
24
- Record<string, Readonly<Record<string, { native: string; tier: ToolMapTier }>>>
25
- >;
34
+ export interface ToolMapEntry {
35
+ readonly native: string;
36
+ readonly tier: ToolMapTier;
37
+ }
38
+
39
+ /** The one shape past the merge: harness -> canonical -> native plus the
40
+ * tier that set it. Carried on the turn options and read by every
41
+ * consumer. */
42
+ export type ToolMap = Readonly<Record<string, Readonly<Record<string, ToolMapEntry>>>>;
26
43
 
27
44
  /** Pure merge per harness per canonical: project > user. */
28
45
  export const mergeToolMaps = (tiers: {
29
- readonly user?: ToolMap;
30
- readonly project?: ToolMap;
31
- }): MergedToolMap => {
32
- const merged: Record<string, Record<string, { native: string; tier: ToolMapTier }>> = {};
46
+ readonly user?: ToolMapConfig;
47
+ readonly project?: ToolMapConfig;
48
+ }): ToolMap => {
49
+ const merged: Record<string, Record<string, ToolMapEntry>> = {};
33
50
  const harnesses = new Set<string>([
34
51
  ...Object.keys(tiers.user ?? {}),
35
52
  ...Object.keys(tiers.project ?? {}),
@@ -41,12 +58,14 @@ export const mergeToolMaps = (tiers: {
41
58
  ...Object.keys(userEntries),
42
59
  ...Object.keys(projectEntries),
43
60
  ]);
44
- const perHarness: Record<string, { native: string; tier: ToolMapTier }> = {};
61
+ const perHarness: Record<string, ToolMapEntry> = {};
45
62
  for (const c of canonicals) {
46
- if (projectEntries[c] !== undefined) {
47
- perHarness[c] = { native: projectEntries[c] as string, tier: "project-config" };
48
- } else if (userEntries[c] !== undefined) {
49
- perHarness[c] = { native: userEntries[c] as string, tier: "user-config" };
63
+ const project = projectEntries[c];
64
+ const user = userEntries[c];
65
+ if (project !== undefined) {
66
+ perHarness[c] = { native: project, tier: "project-config" };
67
+ } else if (user !== undefined) {
68
+ perHarness[c] = { native: user, tier: "user-config" };
50
69
  }
51
70
  }
52
71
  if (Object.keys(perHarness).length > 0) {
@@ -99,65 +118,41 @@ export const canonicalTable = (set: DescriptorSet): CanonicalTable => {
99
118
  return frozen;
100
119
  };
101
120
 
102
- // Backwards compat alias
103
- export const canonicalToolTable = canonicalTable;
104
-
105
121
  export const canonicalNames = (set: DescriptorSet): readonly string[] => {
106
122
  const table = canonicalTable(set);
107
123
  return Object.keys(table).sort();
108
124
  };
109
125
 
110
- export const allCanonicalNames = (
111
- set: DescriptorSet,
112
- toolMap?: ToolMap | MergedToolMap,
113
- ): readonly string[] => {
126
+ /** Every canonical name: the descriptors' plus every name a toolMap adds. */
127
+ export const allCanonicalNames = (set: DescriptorSet, toolMap?: ToolMap): readonly string[] => {
114
128
  const base = canonicalNames(set);
115
- const extra = toolMap
116
- ? Object.values(toolMap).flatMap((m) => Object.keys(m as Record<string, unknown>))
117
- : [];
129
+ const extra = toolMap ? Object.values(toolMap).flatMap((m) => Object.keys(m)) : [];
118
130
  return [...new Set([...base, ...extra])].sort();
119
131
  };
120
132
 
133
+ /** Whether `harnessName` can express `canonical`: through its toolMap
134
+ * entry, a built-in, or a category. */
121
135
  export const hasCounterpart = (
122
136
  table: CanonicalTable,
123
137
  canonical: string,
124
138
  harnessName: string,
125
- toolMap?: MergedToolMap | ToolMap,
126
- ): boolean => {
127
- if (toolMap) {
128
- const hm = (toolMap as MergedToolMap)[harnessName];
129
- if (hm && (hm as Record<string, unknown>)[canonical] !== undefined) {
130
- const v = (hm as Record<string, unknown>)[canonical] as unknown;
131
- if (typeof v === "object" && v !== null && "native" in (v as Record<string, unknown>))
132
- return true;
133
- if (typeof v === "string") return true;
134
- }
135
- }
136
- const entry = table[canonical];
137
- if (!entry) return false;
138
- return entry[harnessName] !== undefined;
139
- };
139
+ toolMap?: ToolMap,
140
+ ): boolean =>
141
+ toolMap?.[harnessName]?.[canonical] !== undefined ||
142
+ table[canonical]?.[harnessName] !== undefined;
140
143
 
144
+ /** The native name `harnessName` lists `canonical` under: the toolMap
145
+ * entry wins, then the built-in; a category has no name-list spelling. */
141
146
  export const nativeFor = (
142
147
  table: CanonicalTable,
143
148
  canonical: string,
144
149
  harnessName: string,
145
- toolMap?: MergedToolMap | ToolMap,
150
+ toolMap?: ToolMap,
146
151
  ): string | null => {
147
- if (toolMap) {
148
- const hm = (toolMap as Record<string, Record<string, unknown>>)[harnessName];
149
- const val = hm?.[canonical];
150
- if (val !== undefined) {
151
- if (typeof val === "object" && val !== null && "native" in (val as Record<string, unknown>)) {
152
- return (val as { native: string }).native;
153
- }
154
- if (typeof val === "string") return val as string;
155
- }
156
- }
157
- const entry = table[canonical];
158
- const v = entry?.[harnessName];
159
- if (v?.kind === "builtin") return v.native;
160
- return null;
152
+ const mapped = toolMap?.[harnessName]?.[canonical];
153
+ if (mapped !== undefined) return mapped.native;
154
+ const entry = table[canonical]?.[harnessName];
155
+ return entry?.kind === "builtin" ? entry.native : null;
161
156
  };
162
157
 
163
158
  export const validateCanonicalList = (