@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
@@ -8,21 +8,28 @@
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";
16
27
  import { ArgvRefusalError } from "./refusal.js";
28
+ import { supportedBy } from "./support.js";
17
29
  import { renderToolSelection } from "./tool-selection.js";
18
- import { READ_PRESET } from "./tool-vocabulary.js";
30
+ import { canonicalTable, hasCounterpart, READ_PRESET } from "./tool-vocabulary.js";
19
31
  import { CLEAN_SELECTOR, resolveModel, validateAccess, validateEffort } from "./vocabulary.js";
20
32
 
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
33
  const isPlainObject = (v: unknown): v is Record<string, unknown> =>
27
34
  typeof v === "object" && v !== null && !Array.isArray(v);
28
35
 
@@ -35,20 +42,10 @@ export const renderTurnOptions = (
35
42
  phase: "launch" | "resume",
36
43
  ): string[] => {
37
44
  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
- }
45
+ // The turn option the access preset displaces when set (codex: sandbox),
46
+ // read from the descriptor so no arm below branches on a harness name.
47
+ const accessSpec = h.turnOptions.access;
48
+ const claimed = accessSpec?.kind === "access" ? accessSpec.claims : undefined;
52
49
 
53
50
  for (const key of TURN_OPTION_KEYS) {
54
51
  const spec = h.turnOptions[key];
@@ -109,16 +106,11 @@ export const renderTurnOptions = (
109
106
  for (const facet of DISCOVERY_FACETS) {
110
107
  const val = discObj[facet];
111
108
  if (val === undefined) continue;
112
- // Polarity handling: disables emits on false, enables emits on true
113
109
  const facetSpec = spec.facets[facet];
114
110
  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
111
+ // A facet the descriptor does not declare cannot be expressed.
112
+ // Only a value that would have emitted refuses: every current
113
+ // facet has "disables" polarity, so false would emit.
122
114
  if (val === false) {
123
115
  const supportedFacets = Object.keys(spec.facets);
124
116
  throw new ArgvRefusalError({
@@ -132,26 +124,15 @@ export const renderTurnOptions = (
132
124
  }
133
125
  continue;
134
126
  }
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
- }
127
+ // Polarity: disables emits on false, enables emits on true.
128
+ const shouldEmit =
129
+ (facetSpec.polarity === "disables" && val === false) ||
130
+ (facetSpec.polarity === "enables" && val === true);
143
131
  if (!shouldEmit) continue;
144
- // Check resumeRender refusal
145
132
  const effective = resolveRender(facetSpec, phase);
146
133
  if (effective === null) {
147
134
  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
- )
135
+ .filter(([, v]) => resolveRender(v as SpecBase, "resume") !== null)
155
136
  .map(([k]) => k);
156
137
  throw new ArgvRefusalError({
157
138
  issue: "unsupported-on-resume",
@@ -161,8 +142,8 @@ export const renderTurnOptions = (
161
142
  supported: resumeSupported.length ? resumeSupported : ["(none resume)"],
162
143
  });
163
144
  }
164
- // Reject config-kv for discovery (only closed vocab, discovery is not)
165
- if ((effective as OptionRender).kind === "config-kv") {
145
+ // Discovery is not a closed vocabulary, so config-kv cannot carry it.
146
+ if (effective.kind === "config-kv") {
166
147
  throw new ArgvRefusalError({
167
148
  issue: "invalid-option-value",
168
149
  harness: h.name,
@@ -171,21 +152,17 @@ export const renderTurnOptions = (
171
152
  supported: ["flag-list only for discovery"],
172
153
  });
173
154
  }
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);
155
+ sequences.push([...tokensFor(effective)]);
181
156
  }
182
157
  continue;
183
158
  }
184
159
 
185
- // Access is opt-in-only, no profile default; dispatch to tool-selection for the preset.
160
+ // Access is opt-in-only, no profile default. One arm for every harness:
161
+ // the descriptor says whether a value renders through the tool list, a
162
+ // phase-aware render, or nothing at all.
186
163
  if (key === "access") {
187
164
  if (raw === undefined) continue;
188
- if (spec === undefined) {
165
+ if (spec === undefined || spec.kind !== "access") {
189
166
  throw new ArgvRefusalError({
190
167
  issue: "unsupported-option",
191
168
  harness: h.name,
@@ -195,93 +172,63 @@ export const renderTurnOptions = (
195
172
  hint: hintFor(h.name, key),
196
173
  });
197
174
  }
198
- const v = validateAccess(raw as string);
175
+ // The resume path builds from unresolved options, so the value is
176
+ // checked here as well as in option resolution - one rule, two gates.
177
+ const v = validateAccess(String(raw));
199
178
  if (!v.ok) {
200
179
  throw new ArgvRefusalError({
201
180
  issue: "invalid-option-value",
202
181
  harness: h.name,
203
182
  option: key,
204
- supported: ["read", "write"],
183
+ supported: [...ACCESS_VALUES],
205
184
  detail: String(raw),
206
185
  });
207
186
  }
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;
187
+ const value = v.id as AccessValue;
188
+ const target = spec.renders[value];
189
+ if (target === null) continue;
190
+ if (target === "tool-preset") {
191
+ // The read preset rides the tool list. When discovery.tools is off,
192
+ // it must not switch tools back on; write is no restriction.
193
+ if (value !== "read" || opts.discovery?.tools === false) continue;
194
+ const table = canonicalTable(defaultDescriptors());
195
+ const filtered = (READ_PRESET as readonly string[]).filter((c) =>
196
+ hasCounterpart(table, c, h.name, opts.toolMap),
197
+ );
198
+ if (filtered.length === 0) continue;
199
+ const rendered = renderToolSelection(h, { include: filtered, toolMap: opts.toolMap });
200
+ if (rendered.tokens.length > 0) sequences.push([...rendered.tokens]);
201
+ continue;
211
202
  }
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
- }
203
+ const render = resolveRender(target, phase);
204
+ if (render === null) {
205
+ throw new ArgvRefusalError({
206
+ issue: "unsupported-on-resume",
207
+ harness: h.name,
208
+ option: key,
209
+ supported: ["re-launch with --access, or resume without it"],
210
+ });
254
211
  }
212
+ sequences.push([...tokensFor(render, target.value)]);
255
213
  continue;
256
214
  }
257
215
 
258
216
  // 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.
217
+ // Handle enum default on launch. A set access preset displaces the
218
+ // option it claims (codex: sandbox), so that option's default must not
219
+ // emit a second flag beside the preset's.
262
220
  if (raw === undefined) {
263
- if (key === "sandbox" && opts.access !== undefined) continue;
221
+ if (key === claimed && opts.access !== undefined) continue;
264
222
  if (
265
223
  spec !== undefined &&
266
224
  spec.kind === "enum" &&
267
225
  spec.default !== undefined &&
268
226
  phase === "launch"
269
227
  ) {
270
- const effective = resolveRender(spec as unknown as SpecBase, phase);
228
+ const effective = resolveRender(spec, phase);
271
229
  if (effective === null) continue; // should not happen for launch
272
230
  // 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);
231
+ sequences.push([...tokensFor(effective, spec.default)]);
285
232
  }
286
233
  continue;
287
234
  }
@@ -293,59 +240,59 @@ export const renderTurnOptions = (
293
240
  harness: h.name,
294
241
  option: key,
295
242
  supported: Object.keys(h.turnOptions).length ? Object.keys(h.turnOptions) : ["(none)"],
243
+ supportedBy: supportedBy(defaultDescriptors(), key),
296
244
  detail: String(raw),
297
245
  hint: hintFor(h.name, key),
298
246
  });
299
247
  }
300
248
 
249
+ // From here every spec carries a render: the discovery table and the
250
+ // access spec were handled above and left the loop with `continue`.
251
+ if (spec.kind === "discovery" || spec.kind === "access") continue;
252
+
301
253
  // 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
- }
254
+ const render = resolveRender(spec, phase);
255
+ if (render === null) {
256
+ const resumeSupported = TURN_OPTION_KEYS.filter((k) => {
257
+ const s = h.turnOptions[k];
258
+ if (!s) return false;
259
+ if (s.kind === "discovery") return Object.keys(s.facets).length > 0;
260
+ if (s.kind === "access") {
261
+ const read = s.renders.read;
262
+ return (
263
+ read === "tool-preset" || (read !== null && resolveRender(read, "resume") !== null)
264
+ );
265
+ }
266
+ return resolveRender(s, "resume") !== null;
267
+ }) as string[];
268
+ throw new ArgvRefusalError({
269
+ issue: "unsupported-on-resume",
270
+ harness: h.name,
271
+ option: key,
272
+ supported: resumeSupported.length ? resumeSupported : ["(none)"],
273
+ });
274
+ }
275
+ // Reject config-kv for open vocabularies. Exception (issue #48):
276
+ // prompt-text rides config-kv VERBATIM on codex - both a literal and
277
+ // a path are accepted (live-verified 0.146.1); no quoting, because
278
+ // codex's k=v split takes the rest of the token raw and both probed
279
+ // forms passed unquoted.
280
+ if (
281
+ spec.render.kind === "config-kv" &&
282
+ spec.kind !== "enum" &&
283
+ spec.kind !== "effort" &&
284
+ spec.kind !== "integer" &&
285
+ spec.kind !== "prompt-text"
286
+ ) {
287
+ throw new ArgvRefusalError({
288
+ issue: "invalid-option-value",
289
+ harness: h.name,
290
+ option: key,
291
+ supported: ["config-kv only for enum, effort, integer, and prompt-text"],
292
+ });
340
293
  }
341
294
 
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
-
295
+ // Per-kind validation; token generation is tokensFor's alone.
349
296
  switch (spec.kind) {
350
297
  case "effort": {
351
298
  if (typeof raw !== "string") {
@@ -380,13 +327,7 @@ export const renderTurnOptions = (
380
327
  detail: raw,
381
328
  });
382
329
  }
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);
330
+ sequences.push([...tokensFor(render, raw)]);
390
331
  break;
391
332
  }
392
333
  case "enum": {
@@ -408,13 +349,7 @@ export const renderTurnOptions = (
408
349
  detail: raw,
409
350
  });
410
351
  }
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);
352
+ sequences.push([...tokensFor(render, raw)]);
418
353
  break;
419
354
  }
420
355
  case "prompt-text": {
@@ -431,13 +366,7 @@ export const renderTurnOptions = (
431
366
  detail: typeof raw === "string" ? "(empty)" : String(raw),
432
367
  });
433
368
  }
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);
369
+ sequences.push([...tokensFor(render, raw, "verbatim")]);
441
370
  break;
442
371
  }
443
372
  case "selector": {
@@ -459,13 +388,7 @@ export const renderTurnOptions = (
459
388
  detail: raw,
460
389
  });
461
390
  }
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);
391
+ sequences.push([...tokensFor(render, raw)]);
469
392
  break;
470
393
  }
471
394
  case "toggle": {
@@ -478,17 +401,11 @@ export const renderTurnOptions = (
478
401
  detail: String(raw),
479
402
  });
480
403
  }
481
- let shouldEmit = false;
482
- if (spec.polarity === "disables" && raw === false) shouldEmit = true;
483
- else if (spec.polarity === "enables" && raw === true) shouldEmit = true;
404
+ const shouldEmit =
405
+ (spec.polarity === "disables" && raw === false) ||
406
+ (spec.polarity === "enables" && raw === true);
484
407
  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);
408
+ sequences.push([...tokensFor(render, String(raw))]);
492
409
  break;
493
410
  }
494
411
  case "integer": {
@@ -510,25 +427,11 @@ export const renderTurnOptions = (
510
427
  detail: String(raw),
511
428
  });
512
429
  }
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);
430
+ sequences.push([...tokensFor(render, String(raw), "verbatim")]);
520
431
  break;
521
432
  }
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
433
  default: {
531
- const _exhaustive: never = spec as never;
434
+ const _exhaustive: never = spec;
532
435
  throw new ArgvRefusalError({
533
436
  issue: "invalid-option-value",
534
437
  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) {
@@ -27,7 +27,6 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
27
27
  baseFlags: ["-p"],
28
28
  subcommands: [],
29
29
  promptStyle: "positional",
30
- toolsFlag: "--allowedTools",
31
30
  // A headless turn launches with the full stream-json output set so the
32
31
  // runner can decode identity/limits and stream token deltas; bare -p
33
32
  // (granularity none) is a degraded invocation this builder never emits.
@@ -198,7 +197,9 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
198
197
  },
199
198
  },
200
199
  },
201
- access: { kind: "tool-preset", render: { kind: "flag-value", flag: "--allowedTools" } },
200
+ // read renders the read preset through the tool list (grant plus deny
201
+ // complement); write is the harness default and emits nothing.
202
+ access: { kind: "access", renders: { read: "tool-preset", write: null } },
202
203
  },
203
204
  // Phase 0 fixtures: claude-tool-interplay.md. include is a permission
204
205
  // grant (Bash, Edit stay visible under --allowedTools Read); only the
@@ -211,7 +212,6 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
211
212
  includeFlag: "--allowedTools",
212
213
  excludeFlag: "--disallowedTools",
213
214
  includeIsStrictAllowlist: false,
214
- composable: true,
215
215
  builtins: [
216
216
  { name: "Bash", defaultEnabled: true, canonical: "shell" },
217
217
  { name: "Edit", defaultEnabled: true, canonical: "edit" },
@@ -23,7 +23,6 @@ export const codexCli: HarnessDescriptor = deepFreeze({
23
23
  baseFlags: ["exec", "--json", "--skip-git-repo-check"],
24
24
  subcommands: ["exec"],
25
25
  promptStyle: "positional",
26
- toolsFlag: null,
27
26
  streamFlags: [],
28
27
  // Codex mints its own thread id; there is nothing to assign at launch.
29
28
  idFlag: null,
@@ -62,11 +61,13 @@ export const codexCli: HarnessDescriptor = deepFreeze({
62
61
  autonomy: { flag: "--yolo" },
63
62
  vocabulary: {
64
63
  modelFlag: "--model",
65
- models: ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"],
64
+ models: ["gpt-6-astra", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"],
66
65
  aliases: {},
67
66
  efforts: ["minimal", "low", "medium", "high", "xhigh", "max"],
68
67
  // Codex constrains ladders per model generation (v1 registry).
69
68
  effortsByModel: {
69
+ // https://developers.openai.com/api/docs/models/gpt-6-astra (2026-09-06).
70
+ "gpt-6-astra": ["low", "medium", "high", "xhigh", "max"],
70
71
  "gpt-5.5": ["minimal", "low", "medium", "high"],
71
72
  "gpt-5.6-sol": ["medium", "high", "xhigh", "max"],
72
73
  "gpt-5.6-terra": ["medium", "high", "xhigh", "max"],
@@ -109,6 +110,14 @@ export const codexCli: HarnessDescriptor = deepFreeze({
109
110
  observedOn: { harness: "codex", model: "", version: "0.146.1", date: "2026-08-19" },
110
111
  },
111
112
  turnOptions: {
113
+ // Codex config reference; accepted as an integer on CLI 0.153.4.
114
+ // Native compaction uses this window; hcn does not count request tokens.
115
+ contextWindow: {
116
+ kind: "integer",
117
+ min: 1,
118
+ max: 272000,
119
+ render: { kind: "config-kv", flag: "-c", key: "model_context_window" },
120
+ },
112
121
  effort: {
113
122
  kind: "effort",
114
123
  render: { kind: "config-kv", flag: "-c", key: "model_reasoning_effort" },
@@ -138,11 +147,27 @@ export const codexCli: HarnessDescriptor = deepFreeze({
138
147
  // caller asked.
139
148
  resumeRender: { kind: "config-kv", flag: "-c", key: "sandbox_mode" },
140
149
  },
150
+ // The preset maps onto the sandbox dimension and therefore CLAIMS it:
151
+ // an explicit --sandbox alongside --access refuses, and the profile's
152
+ // sandbox default yields. Data here, not a harness-name branch.
153
+ // On resume the preset rides the same config-kv spelling the sandbox
154
+ // dimension uses (issue #72 evidence above): `codex exec resume`
155
+ // rejects --sandbox, and -c sandbox_mode is enforced there.
141
156
  access: {
142
- kind: "flag-value",
143
- flag: "--sandbox",
144
- values: { read: "read-only", write: "workspace-write" },
145
- render: { kind: "flag-value", flag: "--sandbox" },
157
+ kind: "access",
158
+ claims: "sandbox",
159
+ renders: {
160
+ read: {
161
+ render: { kind: "flag-value", flag: "--sandbox" },
162
+ resumeRender: { kind: "config-kv", flag: "-c", key: "sandbox_mode" },
163
+ value: "read-only",
164
+ },
165
+ write: {
166
+ render: { kind: "flag-value", flag: "--sandbox" },
167
+ resumeRender: { kind: "config-kv", flag: "-c", key: "sandbox_mode" },
168
+ value: "workspace-write",
169
+ },
170
+ },
146
171
  },
147
172
  },
148
173
  // Tools: no built-in name lists; control is feature booleans
@@ -158,7 +183,6 @@ export const codexCli: HarnessDescriptor = deepFreeze({
158
183
  includeFlag: null,
159
184
  excludeFlag: null,
160
185
  includeIsStrictAllowlist: false,
161
- composable: false,
162
186
  builtins: [],
163
187
  categories: [
164
188
  { key: "shell", disableFlag: null, configKey: "features.shell_tool", canonical: [] },