@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
@@ -8,21 +8,29 @@
8
8
  * it must refuse - is a distinct responsibility with its own vocabulary.
9
9
  * Folding it into argv.ts would make the largest interpretation file the
10
10
  * place two unrelated questions are answered.
11
+ *
12
+ * The render-to-tokens rule itself has one owner, `tokensFor` in the
13
+ * knowledge layer (RFC-02 change 1): every arm here validates its value,
14
+ * resolves the render for the phase, and calls it.
11
15
  */
12
- import type { HarnessDescriptor, OptionRender, SpecBase } from "../knowledge/descriptor.js";
13
- import { DISCOVERY_FACETS, resolveRender, TURN_OPTION_KEYS } from "../knowledge/descriptor.js";
16
+ import type { AccessValue, HarnessDescriptor, SpecBase } from "../knowledge/descriptor.js";
17
+ import {
18
+ ACCESS_VALUES,
19
+ DISCOVERY_FACETS,
20
+ resolveRender,
21
+ TURN_OPTION_KEYS,
22
+ tokensFor,
23
+ } from "../knowledge/descriptor.js";
24
+ import { defaultDescriptors } from "../knowledge/overrides.js";
14
25
  import type { DiscoveryOptions, TurnOptions } from "./argv.js";
15
26
  import { hintFor } from "./hints.js";
27
+ import { assertIsolationCombination } from "./isolation.js";
16
28
  import { ArgvRefusalError } from "./refusal.js";
29
+ import { supportedBy } from "./support.js";
17
30
  import { renderToolSelection } from "./tool-selection.js";
18
- import { READ_PRESET } from "./tool-vocabulary.js";
31
+ import { canonicalTable, hasCounterpart, READ_PRESET } from "./tool-vocabulary.js";
19
32
  import { CLEAN_SELECTOR, resolveModel, validateAccess, validateEffort } from "./vocabulary.js";
20
33
 
21
- // TOML-quoted value for config-kv: JSON.stringify is sufficient for the
22
- // closed vocabularies that may use it (enum, effort) - no value contains a
23
- // quote that would need escaping beyond JSON's.
24
- const tomlQuote = (value: string): string => JSON.stringify(value);
25
-
26
34
  const isPlainObject = (v: unknown): v is Record<string, unknown> =>
27
35
  typeof v === "object" && v !== null && !Array.isArray(v);
28
36
 
@@ -34,21 +42,12 @@ export const renderTurnOptions = (
34
42
  opts: TurnOptions,
35
43
  phase: "launch" | "resume",
36
44
  ): string[] => {
45
+ assertIsolationCombination(h, opts);
37
46
  const sequences: string[][] = [];
38
-
39
- const accessRaw = opts.access;
40
- if (accessRaw !== undefined) {
41
- const v = validateAccess(accessRaw);
42
- if (!v.ok) {
43
- throw new ArgvRefusalError({
44
- issue: "invalid-option-value",
45
- harness: h.name,
46
- option: "access",
47
- supported: ["read", "write"],
48
- detail: String(accessRaw),
49
- });
50
- }
51
- }
47
+ // The turn option the access preset displaces when set (codex: sandbox),
48
+ // read from the descriptor so no arm below branches on a harness name.
49
+ const accessSpec = h.turnOptions.access;
50
+ const claimed = accessSpec?.kind === "access" ? accessSpec.claims : undefined;
52
51
 
53
52
  for (const key of TURN_OPTION_KEYS) {
54
53
  const spec = h.turnOptions[key];
@@ -109,16 +108,11 @@ export const renderTurnOptions = (
109
108
  for (const facet of DISCOVERY_FACETS) {
110
109
  const val = discObj[facet];
111
110
  if (val === undefined) continue;
112
- // Polarity handling: disables emits on false, enables emits on true
113
111
  const facetSpec = spec.facets[facet];
114
112
  if (facetSpec === undefined) {
115
- // If caller asked to disable (false for disables, true for enables) but facet not declared, refuse
116
- // But if caller passed true for a disables facet, it's a no-op, not a refusal
117
- // So only refuse when the caller's value would have emitted
118
- // For all current descriptors polarity is disables, so false would emit -> refuse
119
- // For safety, check wouldEmit logic
120
- // If val would not emit anyway, skip
121
- // Determine wouldEmit: if facetSpec undefined, we cannot know polarity, so treat false as intended disable -> refuse
113
+ // A facet the descriptor does not declare cannot be expressed.
114
+ // Only a value that would have emitted refuses: every current
115
+ // facet has "disables" polarity, so false would emit.
122
116
  if (val === false) {
123
117
  const supportedFacets = Object.keys(spec.facets);
124
118
  throw new ArgvRefusalError({
@@ -132,26 +126,15 @@ export const renderTurnOptions = (
132
126
  }
133
127
  continue;
134
128
  }
135
- // Determine if this value should emit
136
- let shouldEmit = false;
137
- if (facetSpec.polarity === "disables" && val === false) shouldEmit = true;
138
- else if (facetSpec.polarity === "enables" && val === true) shouldEmit = true;
139
- else {
140
- // true for disables or false for enables => no-op
141
- continue;
142
- }
129
+ // Polarity: disables emits on false, enables emits on true.
130
+ const shouldEmit =
131
+ (facetSpec.polarity === "disables" && val === false) ||
132
+ (facetSpec.polarity === "enables" && val === true);
143
133
  if (!shouldEmit) continue;
144
- // Check resumeRender refusal
145
134
  const effective = resolveRender(facetSpec, phase);
146
135
  if (effective === null) {
147
136
  const resumeSupported = Object.entries(spec.facets)
148
- .filter(
149
- ([, v]) =>
150
- resolveRender(
151
- v as unknown as import("../knowledge/descriptor.js").SpecBase,
152
- "resume",
153
- ) !== null,
154
- )
137
+ .filter(([, v]) => resolveRender(v as SpecBase, "resume") !== null)
155
138
  .map(([k]) => k);
156
139
  throw new ArgvRefusalError({
157
140
  issue: "unsupported-on-resume",
@@ -161,8 +144,8 @@ export const renderTurnOptions = (
161
144
  supported: resumeSupported.length ? resumeSupported : ["(none resume)"],
162
145
  });
163
146
  }
164
- // Reject config-kv for discovery (only closed vocab, discovery is not)
165
- if ((effective as OptionRender).kind === "config-kv") {
147
+ // Discovery is not a closed vocabulary, so config-kv cannot carry it.
148
+ if (effective.kind === "config-kv") {
166
149
  throw new ArgvRefusalError({
167
150
  issue: "invalid-option-value",
168
151
  harness: h.name,
@@ -171,21 +154,17 @@ export const renderTurnOptions = (
171
154
  supported: ["flag-list only for discovery"],
172
155
  });
173
156
  }
174
- let seq: string[];
175
- if ((effective as OptionRender).kind === "flag-list")
176
- seq = [...(effective as Extract<OptionRender, { kind: "flag-list" }>).flags];
177
- else if ((effective as OptionRender).kind === "flag-value")
178
- seq = [(effective as Extract<OptionRender, { kind: "flag-value" }>).flag, ""];
179
- else seq = [];
180
- sequences.push(seq);
157
+ sequences.push([...tokensFor(effective)]);
181
158
  }
182
159
  continue;
183
160
  }
184
161
 
185
- // Access is opt-in-only, no profile default; dispatch to tool-selection for the preset.
162
+ // Access is opt-in-only, no profile default. One arm for every harness:
163
+ // the descriptor says whether a value renders through the tool list, a
164
+ // phase-aware render, or nothing at all.
186
165
  if (key === "access") {
187
166
  if (raw === undefined) continue;
188
- if (spec === undefined) {
167
+ if (spec === undefined || spec.kind !== "access") {
189
168
  throw new ArgvRefusalError({
190
169
  issue: "unsupported-option",
191
170
  harness: h.name,
@@ -195,93 +174,63 @@ export const renderTurnOptions = (
195
174
  hint: hintFor(h.name, key),
196
175
  });
197
176
  }
198
- const v = validateAccess(raw as string);
177
+ // The resume path builds from unresolved options, so the value is
178
+ // checked here as well as in option resolution - one rule, two gates.
179
+ const v = validateAccess(String(raw));
199
180
  if (!v.ok) {
200
181
  throw new ArgvRefusalError({
201
182
  issue: "invalid-option-value",
202
183
  harness: h.name,
203
184
  option: key,
204
- supported: ["read", "write"],
185
+ supported: [...ACCESS_VALUES],
205
186
  detail: String(raw),
206
187
  });
207
188
  }
208
- // When discovery.tools is off, the read preset must not re-enable tools.
209
- if (raw === "read" && opts.discovery?.tools === false) {
210
- if (spec.kind === "tool-preset") continue;
189
+ const value = v.id as AccessValue;
190
+ const target = spec.renders[value];
191
+ if (target === null) continue;
192
+ if (target === "tool-preset") {
193
+ // The read preset rides the tool list. When discovery.tools is off,
194
+ // it must not switch tools back on; write is no restriction.
195
+ if (value !== "read" || opts.discovery?.tools === false) continue;
196
+ const table = canonicalTable(defaultDescriptors());
197
+ const filtered = (READ_PRESET as readonly string[]).filter((c) =>
198
+ hasCounterpart(table, c, h.name, opts.toolMap),
199
+ );
200
+ if (filtered.length === 0) continue;
201
+ const rendered = renderToolSelection(h, { include: filtered, toolMap: opts.toolMap });
202
+ if (rendered.tokens.length > 0) sequences.push([...rendered.tokens]);
203
+ continue;
211
204
  }
212
- switch (spec.kind) {
213
- case "tool-preset": {
214
- if (raw === "write") break;
215
- const toolMapForHarness = (opts as { toolMap?: Record<string, Record<string, string>> })
216
- .toolMap?.[h.name];
217
- const filtered = (READ_PRESET as readonly string[]).filter((c) => {
218
- if (toolMapForHarness?.[c] !== undefined) return true;
219
- if (h.tools.builtins.some((b) => b.canonical === c)) return true;
220
- if (h.tools.categories.some((cat) => (cat.canonical as readonly string[]).includes(c)))
221
- return true;
222
- return false;
223
- });
224
- if (filtered.length === 0) break;
225
- const rendered = renderToolSelection(h, {
226
- include: filtered as unknown as string[],
227
- toolMap: toolMapForHarness,
228
- });
229
- if (rendered.tokens.length > 0) sequences.push([...rendered.tokens]);
230
- break;
231
- }
232
- case "flag-value": {
233
- const fv = spec as Extract<typeof spec, { kind: "flag-value" }>;
234
- const mapped = fv.values[raw as string];
235
- if (mapped !== undefined) sequences.push([fv.flag, mapped]);
236
- break;
237
- }
238
- case "flag-list-by-value": {
239
- const fl = spec as Extract<typeof spec, { kind: "flag-list-by-value" }>;
240
- const list = fl.flags[raw as string] ?? [];
241
- if (list.length > 0) sequences.push([...list]);
242
- break;
243
- }
244
- default: {
245
- const _exhaustive: never = spec as never;
246
- throw new ArgvRefusalError({
247
- issue: "invalid-option-value",
248
- harness: h.name,
249
- option: key,
250
- supported: [],
251
- detail: String(_exhaustive),
252
- });
253
- }
205
+ const render = resolveRender(target, phase);
206
+ if (render === null) {
207
+ throw new ArgvRefusalError({
208
+ issue: "unsupported-on-resume",
209
+ harness: h.name,
210
+ option: key,
211
+ supported: ["re-launch with --access, or resume without it"],
212
+ });
254
213
  }
214
+ sequences.push([...tokensFor(render, target.value)]);
255
215
  continue;
256
216
  }
257
217
 
258
218
  // Non-discovery keys
259
- // Handle enum default on launch. An access preset already claimed the
260
- // sandbox flag on harnesses that express access through it (codex);
261
- // the enum default must not emit a second --sandbox.
219
+ // Handle enum default on launch. A set access preset displaces the
220
+ // option it claims (codex: sandbox), so that option's default must not
221
+ // emit a second flag beside the preset's.
262
222
  if (raw === undefined) {
263
- if (key === "sandbox" && opts.access !== undefined) continue;
223
+ if (key === claimed && opts.access !== undefined) continue;
264
224
  if (
265
225
  spec !== undefined &&
266
226
  spec.kind === "enum" &&
267
227
  spec.default !== undefined &&
268
228
  phase === "launch"
269
229
  ) {
270
- const effective = resolveRender(spec as unknown as SpecBase, phase);
230
+ const effective = resolveRender(spec, phase);
271
231
  if (effective === null) continue; // should not happen for launch
272
232
  // config-kv is allowed for enum (closed vocab) - no rejection needed here
273
- let seq: string[];
274
- if ((effective as OptionRender).kind === "flag-value")
275
- seq = [(effective as Extract<OptionRender, { kind: "flag-value" }>).flag, spec.default];
276
- else if ((effective as OptionRender).kind === "config-kv")
277
- seq = [
278
- (effective as Extract<OptionRender, { kind: "config-kv" }>).flag,
279
- `${(effective as Extract<OptionRender, { kind: "config-kv" }>).key}=${tomlQuote(spec.default)}`,
280
- ];
281
- else if ((effective as OptionRender).kind === "flag-list")
282
- seq = [...(effective as Extract<OptionRender, { kind: "flag-list" }>).flags];
283
- else seq = [];
284
- sequences.push(seq);
233
+ sequences.push([...tokensFor(effective, spec.default)]);
285
234
  }
286
235
  continue;
287
236
  }
@@ -293,59 +242,59 @@ export const renderTurnOptions = (
293
242
  harness: h.name,
294
243
  option: key,
295
244
  supported: Object.keys(h.turnOptions).length ? Object.keys(h.turnOptions) : ["(none)"],
245
+ supportedBy: supportedBy(defaultDescriptors(), key),
296
246
  detail: String(raw),
297
247
  hint: hintFor(h.name, key),
298
248
  });
299
249
  }
300
250
 
251
+ // From here every spec carries a render: the discovery table and the
252
+ // access spec were handled above and left the loop with `continue`.
253
+ if (spec.kind === "discovery" || spec.kind === "access") continue;
254
+
301
255
  // Check resumeRender null => unsupported-on-resume
302
- // For discovery we handled per-facet; for others check spec
303
- if ((spec as { kind: string }).kind !== "discovery") {
304
- const effective = resolveRender(spec as unknown as SpecBase, phase);
305
- if (effective === null) {
306
- const resumeSupported = TURN_OPTION_KEYS.filter((k) => {
307
- const s = h.turnOptions[k];
308
- if (!s) return false;
309
- if ((s as { kind: string }).kind === "discovery") {
310
- const facets = (s as unknown as { facets: Record<string, unknown> }).facets;
311
- return Object.keys(facets).length > 0;
312
- }
313
- return resolveRender(s as unknown as SpecBase, "resume") !== null;
314
- }) as string[];
315
- throw new ArgvRefusalError({
316
- issue: "unsupported-on-resume",
317
- harness: h.name,
318
- option: key,
319
- supported: resumeSupported.length ? resumeSupported : ["(none)"],
320
- });
321
- }
322
- // Reject config-kv for open vocabularies. Exception (issue #48):
323
- // prompt-text rides config-kv VERBATIM on codex - both a literal and
324
- // a path are accepted (live-verified 0.146.1); no quoting, because
325
- // codex's k=v split takes the rest of the token raw and both probed
326
- // forms passed unquoted.
327
- if (
328
- (spec as unknown as SpecBase).render.kind === "config-kv" &&
329
- (spec as { kind: string }).kind !== "enum" &&
330
- (spec as { kind: string }).kind !== "effort" &&
331
- (spec as { kind: string }).kind !== "prompt-text"
332
- ) {
333
- throw new ArgvRefusalError({
334
- issue: "invalid-option-value",
335
- harness: h.name,
336
- option: key,
337
- supported: ["config-kv only for enum, effort, and prompt-text (issue #48)"],
338
- });
339
- }
256
+ const render = resolveRender(spec, phase);
257
+ if (render === null) {
258
+ const resumeSupported = TURN_OPTION_KEYS.filter((k) => {
259
+ const s = h.turnOptions[k];
260
+ if (!s) return false;
261
+ if (s.kind === "discovery") return Object.keys(s.facets).length > 0;
262
+ if (s.kind === "access") {
263
+ const read = s.renders.read;
264
+ return (
265
+ read === "tool-preset" || (read !== null && resolveRender(read, "resume") !== null)
266
+ );
267
+ }
268
+ return resolveRender(s, "resume") !== null;
269
+ }) as string[];
270
+ throw new ArgvRefusalError({
271
+ issue: "unsupported-on-resume",
272
+ harness: h.name,
273
+ option: key,
274
+ supported: resumeSupported.length ? resumeSupported : ["(none)"],
275
+ });
276
+ }
277
+ // Reject config-kv for open vocabularies. Exception (issue #48):
278
+ // prompt-text rides config-kv VERBATIM on codex - both a literal and
279
+ // a path are accepted (live-verified 0.146.1); no quoting, because
280
+ // codex's k=v split takes the rest of the token raw and both probed
281
+ // forms passed unquoted.
282
+ if (
283
+ spec.render.kind === "config-kv" &&
284
+ spec.kind !== "enum" &&
285
+ spec.kind !== "effort" &&
286
+ spec.kind !== "integer" &&
287
+ spec.kind !== "prompt-text"
288
+ ) {
289
+ throw new ArgvRefusalError({
290
+ issue: "invalid-option-value",
291
+ harness: h.name,
292
+ option: key,
293
+ supported: ["config-kv only for enum, effort, integer, and prompt-text"],
294
+ });
340
295
  }
341
296
 
342
- // Per-kind validation and token generation
343
- const effectiveRender =
344
- (spec as { kind: string }).kind === "discovery"
345
- ? null
346
- : resolveRender(spec as unknown as SpecBase, phase);
347
- // effectiveRender already checked for null
348
-
297
+ // Per-kind validation; token generation is tokensFor's alone.
349
298
  switch (spec.kind) {
350
299
  case "effort": {
351
300
  if (typeof raw !== "string") {
@@ -380,13 +329,7 @@ export const renderTurnOptions = (
380
329
  detail: raw,
381
330
  });
382
331
  }
383
- const r = effectiveRender!;
384
- let seq: string[];
385
- if (r.kind === "flag-value") seq = [r.flag, raw];
386
- else if (r.kind === "config-kv") seq = [r.flag, `${r.key}=${tomlQuote(raw)}`];
387
- else if (r.kind === "flag-list") seq = [...r.flags];
388
- else seq = [];
389
- sequences.push(seq);
332
+ sequences.push([...tokensFor(render, raw)]);
390
333
  break;
391
334
  }
392
335
  case "enum": {
@@ -408,13 +351,7 @@ export const renderTurnOptions = (
408
351
  detail: raw,
409
352
  });
410
353
  }
411
- const r = effectiveRender!;
412
- let seq: string[];
413
- if (r.kind === "flag-value") seq = [r.flag, raw];
414
- else if (r.kind === "config-kv") seq = [r.flag, `${r.key}=${tomlQuote(raw)}`];
415
- else if (r.kind === "flag-list") seq = [...r.flags];
416
- else seq = [];
417
- sequences.push(seq);
354
+ sequences.push([...tokensFor(render, raw)]);
418
355
  break;
419
356
  }
420
357
  case "prompt-text": {
@@ -431,13 +368,7 @@ export const renderTurnOptions = (
431
368
  detail: typeof raw === "string" ? "(empty)" : String(raw),
432
369
  });
433
370
  }
434
- const r = effectiveRender!;
435
- let seq: string[];
436
- if (r.kind === "flag-value") seq = [...(r.extraFlags ?? []), r.flag, raw];
437
- else if (r.kind === "config-kv") seq = [r.flag, `${r.key}=${raw}`];
438
- else if (r.kind === "flag-list") seq = [...r.flags];
439
- else seq = [];
440
- sequences.push(seq);
371
+ sequences.push([...tokensFor(render, raw, "verbatim")]);
441
372
  break;
442
373
  }
443
374
  case "selector": {
@@ -459,13 +390,7 @@ export const renderTurnOptions = (
459
390
  detail: raw,
460
391
  });
461
392
  }
462
- const r = effectiveRender!;
463
- let seq: string[];
464
- if (r.kind === "flag-value") seq = [r.flag, raw];
465
- else if (r.kind === "config-kv") seq = [r.flag, `${r.key}=${tomlQuote(raw)}`];
466
- else if (r.kind === "flag-list") seq = [...r.flags];
467
- else seq = [];
468
- sequences.push(seq);
393
+ sequences.push([...tokensFor(render, raw)]);
469
394
  break;
470
395
  }
471
396
  case "toggle": {
@@ -478,17 +403,11 @@ export const renderTurnOptions = (
478
403
  detail: String(raw),
479
404
  });
480
405
  }
481
- let shouldEmit = false;
482
- if (spec.polarity === "disables" && raw === false) shouldEmit = true;
483
- else if (spec.polarity === "enables" && raw === true) shouldEmit = true;
406
+ const shouldEmit =
407
+ (spec.polarity === "disables" && raw === false) ||
408
+ (spec.polarity === "enables" && raw === true);
484
409
  if (!shouldEmit) break;
485
- const r = effectiveRender!;
486
- let seq: string[];
487
- if (r.kind === "flag-list") seq = [...r.flags];
488
- else if (r.kind === "flag-value") seq = [r.flag, String(raw)];
489
- else if (r.kind === "config-kv") seq = [r.flag, `${r.key}=${tomlQuote(String(raw))}`];
490
- else seq = [];
491
- sequences.push(seq);
410
+ sequences.push([...tokensFor(render, String(raw))]);
492
411
  break;
493
412
  }
494
413
  case "integer": {
@@ -510,25 +429,11 @@ export const renderTurnOptions = (
510
429
  detail: String(raw),
511
430
  });
512
431
  }
513
- const r = effectiveRender!;
514
- let seq: string[];
515
- if (r.kind === "flag-value") seq = [r.flag, String(raw)];
516
- else if (r.kind === "config-kv") seq = [r.flag, `${r.key}=${tomlQuote(String(raw))}`];
517
- else if (r.kind === "flag-list") seq = [...r.flags];
518
- else seq = [];
519
- sequences.push(seq);
432
+ sequences.push([...tokensFor(render, String(raw), "verbatim")]);
520
433
  break;
521
434
  }
522
- case "discovery":
523
- // already handled
524
- break;
525
- case "tool-preset":
526
- case "flag-value":
527
- case "flag-list-by-value":
528
- // access-only kinds - handled above for key === "access"
529
- break;
530
435
  default: {
531
- const _exhaustive: never = spec as never;
436
+ const _exhaustive: never = spec;
532
437
  throw new ArgvRefusalError({
533
438
  issue: "invalid-option-value",
534
439
  harness: h.name,
@@ -5,7 +5,7 @@
5
5
  * actionable without reading the descriptor. Resolution lives in ONE place
6
6
  * (resolveModel) so validation and capability claims cannot drift.
7
7
  */
8
- import type { HarnessDescriptor } from "../knowledge/descriptor.js";
8
+ import { ACCESS_VALUES, type HarnessDescriptor } from "../knowledge/descriptor.js";
9
9
 
10
10
  export type Validated = { readonly ok: true; readonly id: string } | ValidationRefusal;
11
11
 
@@ -14,10 +14,11 @@ export interface ValidationRefusal {
14
14
  readonly reason: string;
15
15
  }
16
16
 
17
- /** The selector grammar an extensible registry still demands: pi documents
18
- * models as provider/id[:thinking], so word characters plus the few real
19
- * separators - never whitespace, shell metacharacters, or control/format
20
- * characters, and bounded like session ids. */
17
+ /** The one selector grammar for model ids, tool names, and toolMap keys
18
+ * (RFC-02 change 9): pi documents models as provider/id[:thinking], so
19
+ * word characters plus the few real separators - never whitespace, shell
20
+ * metacharacters, or control/format characters, and bounded like session
21
+ * ids. */
21
22
  export const CLEAN_SELECTOR = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,127}$/;
22
23
 
23
24
  export interface ResolvedModel {
@@ -54,14 +55,18 @@ export const validateModel = (h: HarnessDescriptor, model: string): Validated =>
54
55
  };
55
56
  };
56
57
 
57
- /** Validate an effort against the ladder that applies to the pick: the
58
- * model's own ladder where the harness constrains per model (codex), else
59
- * the harness-wide ladder. */
58
+ /** The access preset takes one of the closed ACCESS_VALUES. */
60
59
  export const validateAccess = (value: string): Validated => {
61
- if (value === "read" || value === "write") return { ok: true, id: value };
62
- return { ok: false, reason: `unknown access ${JSON.stringify(value)}; accepted: read, write` };
60
+ if ((ACCESS_VALUES as readonly string[]).includes(value)) return { ok: true, id: value };
61
+ return {
62
+ ok: false,
63
+ reason: `unknown access ${JSON.stringify(value)}; accepted: ${ACCESS_VALUES.join(", ")}`,
64
+ };
63
65
  };
64
66
 
67
+ /** Validate an effort against the ladder that applies to the pick: the
68
+ * model's own ladder where the harness constrains per model (codex), else
69
+ * the harness-wide ladder. */
65
70
  export const validateEffort = (h: HarnessDescriptor, effort: string, model?: string): Validated => {
66
71
  let ladder = h.vocabulary.efforts;
67
72
  if (model !== undefined && h.vocabulary.effortsByModel !== undefined) {
@@ -9,7 +9,9 @@
9
9
  * `ANTHROPIC_API_KEY` / `apiKeyHelper` (OAuth never read) - so a caller
10
10
  * authenticated via OAuth would break. The descriptor therefore offers no
11
11
  * `instructionFiles` facet and a call passing it must refuse. Likewise
12
- * `tools` has no discovery flag on claude and refuses.
12
+ * `tools` has no discovery flag on claude and refuses. The explicit
13
+ * tool-free isolation composite below accepts the bare-mode auth cost and
14
+ * disables native tools too. It is separate from a granular discovery facet.
13
15
  *
14
16
  * Effort: A-002 showed `--effort bogus` warns on stderr and runs at DEFAULT
15
17
  * effort, exit 0, with nothing echoed in the stream. The library-side
@@ -27,7 +29,6 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
27
29
  baseFlags: ["-p"],
28
30
  subcommands: [],
29
31
  promptStyle: "positional",
30
- toolsFlag: "--allowedTools",
31
32
  // A headless turn launches with the full stream-json output set so the
32
33
  // runner can decode identity/limits and stream token deltas; bare -p
33
34
  // (granularity none) is a degraded invocation this builder never emits.
@@ -162,6 +163,17 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
162
163
  observedOn: { harness: "claude", model: "", version: "2.1.235", date: "2026-08-19" },
163
164
  },
164
165
  turnOptions: {
166
+ // Native CLI reference: bare removes discovery; the empty built-in list and
167
+ // MCP deny remove tool access. Resume and native overrides are refused.
168
+ isolation: {
169
+ kind: "enum",
170
+ values: ["tool-free"],
171
+ resumeRender: null,
172
+ render: {
173
+ kind: "flag-list",
174
+ flags: ["--bare", "--tools", "", "--disallowedTools", "mcp__*", "--strict-mcp-config"],
175
+ },
176
+ },
165
177
  effort: { kind: "effort", render: { kind: "flag-value", flag: "--effort" } },
166
178
  // issue #48, live-verified 2.1.235: --system-prompt replaces the built-in
167
179
  // prompt; --exclude-dynamic-system-prompt-sections strips the dynamic
@@ -198,7 +210,9 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
198
210
  },
199
211
  },
200
212
  },
201
- access: { kind: "tool-preset", render: { kind: "flag-value", flag: "--allowedTools" } },
213
+ // read renders the read preset through the tool list (grant plus deny
214
+ // complement); write is the harness default and emits nothing.
215
+ access: { kind: "access", renders: { read: "tool-preset", write: null } },
202
216
  },
203
217
  // Phase 0 fixtures: claude-tool-interplay.md. include is a permission
204
218
  // grant (Bash, Edit stay visible under --allowedTools Read); only the
@@ -211,7 +225,6 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
211
225
  includeFlag: "--allowedTools",
212
226
  excludeFlag: "--disallowedTools",
213
227
  includeIsStrictAllowlist: false,
214
- composable: true,
215
228
  builtins: [
216
229
  { name: "Bash", defaultEnabled: true, canonical: "shell" },
217
230
  { name: "Edit", defaultEnabled: true, canonical: "edit" },