@dungle-scrubs/harness-cli-normalizer 0.1.2 → 0.3.0
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.
- package/README.md +144 -7
- package/dist/cli/args.d.ts +74 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +339 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/check.d.ts +2 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +110 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +7 -0
- package/dist/cli/exit-codes.d.ts.map +1 -0
- package/dist/cli/exit-codes.js +11 -0
- package/dist/cli/exit-codes.js.map +1 -0
- package/dist/cli/help.d.ts +7 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +110 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +170 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/inspect.d.ts +3 -0
- package/dist/cli/inspect.d.ts.map +1 -0
- package/dist/cli/inspect.js +170 -0
- package/dist/cli/inspect.js.map +1 -0
- package/dist/cli/ls.d.ts +2 -0
- package/dist/cli/ls.d.ts.map +1 -0
- package/dist/cli/ls.js +10 -0
- package/dist/cli/ls.js.map +1 -0
- package/dist/cli/render.d.ts +8 -0
- package/dist/cli/render.d.ts.map +1 -0
- package/dist/cli/render.js +55 -0
- package/dist/cli/render.js.map +1 -0
- package/dist/cli/resolve-harness.d.ts +4 -0
- package/dist/cli/resolve-harness.d.ts.map +1 -0
- package/dist/cli/resolve-harness.js +31 -0
- package/dist/cli/resolve-harness.js.map +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +280 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/session.d.ts +2 -0
- package/dist/cli/session.d.ts.map +1 -0
- package/dist/cli/session.js +128 -0
- package/dist/cli/session.js.map +1 -0
- package/dist/cli/version.d.ts +2 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/cli/version.js +34 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/cli.js +2 -0
- package/dist/execution/decode.d.ts.map +1 -1
- package/dist/execution/decode.js +32 -0
- package/dist/execution/decode.js.map +1 -1
- package/dist/execution/deps.d.ts +7 -1
- package/dist/execution/deps.d.ts.map +1 -1
- package/dist/execution/events.d.ts +6 -2
- package/dist/execution/events.d.ts.map +1 -1
- package/dist/execution/events.js.map +1 -1
- package/dist/execution/failure.d.ts +45 -0
- package/dist/execution/failure.d.ts.map +1 -0
- package/dist/execution/failure.js +121 -0
- package/dist/execution/failure.js.map +1 -0
- package/dist/execution/node-deps.d.ts.map +1 -1
- package/dist/execution/node-deps.js +19 -0
- package/dist/execution/node-deps.js.map +1 -1
- package/dist/execution/stream-turn.d.ts +2 -0
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +181 -16
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/interpretation/argv.d.ts +16 -4
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +68 -19
- package/dist/interpretation/argv.js.map +1 -1
- package/dist/interpretation/dimensions.d.ts +0 -2
- package/dist/interpretation/dimensions.d.ts.map +1 -1
- package/dist/interpretation/dimensions.js +0 -2
- package/dist/interpretation/dimensions.js.map +1 -1
- package/dist/interpretation/limits.d.ts +10 -1
- package/dist/interpretation/limits.d.ts.map +1 -1
- package/dist/interpretation/limits.js +84 -5
- package/dist/interpretation/limits.js.map +1 -1
- package/dist/interpretation/refusal.d.ts +31 -0
- package/dist/interpretation/refusal.d.ts.map +1 -0
- package/dist/interpretation/refusal.js +87 -0
- package/dist/interpretation/refusal.js.map +1 -0
- package/dist/interpretation/turn-options.d.ts +18 -0
- package/dist/interpretation/turn-options.d.ts.map +1 -0
- package/dist/interpretation/turn-options.js +417 -0
- package/dist/interpretation/turn-options.js.map +1 -0
- package/dist/interpretation/vocabulary.d.ts +5 -0
- package/dist/interpretation/vocabulary.d.ts.map +1 -1
- package/dist/interpretation/vocabulary.js +1 -1
- package/dist/interpretation/vocabulary.js.map +1 -1
- package/dist/knowledge/claude-code.d.ts +13 -1
- package/dist/knowledge/claude-code.d.ts.map +1 -1
- package/dist/knowledge/claude-code.js +35 -13
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +25 -14
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +118 -14
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/descriptor.js +58 -0
- package/dist/knowledge/descriptor.js.map +1 -1
- package/dist/knowledge/matchers.d.ts +3 -3
- package/dist/knowledge/matchers.d.ts.map +1 -1
- package/dist/knowledge/matchers.js +19 -6
- package/dist/knowledge/matchers.js.map +1 -1
- package/dist/knowledge/muse.d.ts +2 -2
- package/dist/knowledge/muse.d.ts.map +1 -1
- package/dist/knowledge/muse.js +22 -6
- package/dist/knowledge/muse.js.map +1 -1
- package/dist/knowledge/overrides.d.ts +7 -0
- package/dist/knowledge/overrides.d.ts.map +1 -1
- package/dist/knowledge/overrides.js +76 -0
- package/dist/knowledge/overrides.js.map +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +25 -3
- package/dist/knowledge/pi.js.map +1 -1
- package/package.json +5 -2
- package/src/cli/args.ts +381 -0
- package/src/cli/check.ts +136 -0
- package/src/cli/exit-codes.ts +13 -0
- package/src/cli/help.ts +114 -0
- package/src/cli/index.ts +177 -0
- package/src/cli/inspect.ts +174 -0
- package/src/cli/ls.ts +14 -0
- package/src/cli/render.ts +62 -0
- package/src/cli/resolve-harness.ts +33 -0
- package/src/cli/run.ts +280 -0
- package/src/cli/session.ts +137 -0
- package/src/cli/version.ts +32 -0
- package/src/execution/decode.ts +32 -0
- package/src/execution/deps.ts +7 -1
- package/src/execution/events.ts +9 -2
- package/src/execution/failure.ts +159 -0
- package/src/execution/node-deps.ts +17 -0
- package/src/execution/stream-turn.ts +189 -17
- package/src/interpretation/argv.ts +85 -27
- package/src/interpretation/dimensions.ts +0 -6
- package/src/interpretation/limits.ts +97 -6
- package/src/interpretation/refusal.ts +116 -0
- package/src/interpretation/turn-options.ts +424 -0
- package/src/interpretation/vocabulary.ts +1 -1
- package/src/knowledge/claude-code.ts +35 -13
- package/src/knowledge/codex.ts +25 -14
- package/src/knowledge/descriptor.ts +138 -12
- package/src/knowledge/matchers.ts +22 -9
- package/src/knowledge/muse.ts +22 -6
- package/src/knowledge/overrides.ts +97 -0
- package/src/knowledge/pi.ts +25 -3
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turn-options: rendering and validation of per-call turn options into argv.
|
|
3
|
+
*
|
|
4
|
+
* This lives separately from argv.ts because argv.ts owns argv *assembly and
|
|
5
|
+
* ordering* (where the prompt goes, that tools is last, that resume never
|
|
6
|
+
* inherits launch flags). Turn-option *rendering and validation* - which
|
|
7
|
+
* flag spells a dimension, how its value is validated and quoted, and when
|
|
8
|
+
* it must refuse - is a distinct responsibility with its own vocabulary.
|
|
9
|
+
* Folding it into argv.ts would make the largest interpretation file the
|
|
10
|
+
* place two unrelated questions are answered.
|
|
11
|
+
*/
|
|
12
|
+
import type { HarnessDescriptor, OptionRender, SpecBase } from "../knowledge/descriptor.js";
|
|
13
|
+
import { DISCOVERY_FACETS, resolveRender, TURN_OPTION_KEYS } from "../knowledge/descriptor.js";
|
|
14
|
+
import type { DiscoveryOptions, TurnOptions } from "./argv.js";
|
|
15
|
+
import { ArgvRefusalError } from "./refusal.js";
|
|
16
|
+
import { CLEAN_SELECTOR, resolveModel, validateEffort } from "./vocabulary.js";
|
|
17
|
+
|
|
18
|
+
// TOML-quoted value for config-kv: JSON.stringify is sufficient for the
|
|
19
|
+
// closed vocabularies that may use it (enum, effort) - no value contains a
|
|
20
|
+
// quote that would need escaping beyond JSON's.
|
|
21
|
+
const tomlQuote = (value: string): string => JSON.stringify(value);
|
|
22
|
+
|
|
23
|
+
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
|
24
|
+
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
25
|
+
|
|
26
|
+
/** Render turn options for a given phase, in TURN_OPTION_KEYS order and with
|
|
27
|
+
* discovery facets in DISCOVERY_FACETS order, de-duplicating by exact token
|
|
28
|
+
* sequence (first wins) so claude's two facets sharing one flag emit once. */
|
|
29
|
+
export const renderTurnOptions = (
|
|
30
|
+
h: HarnessDescriptor,
|
|
31
|
+
opts: TurnOptions,
|
|
32
|
+
phase: "launch" | "resume",
|
|
33
|
+
): string[] => {
|
|
34
|
+
const sequences: string[][] = [];
|
|
35
|
+
|
|
36
|
+
for (const key of TURN_OPTION_KEYS) {
|
|
37
|
+
const spec = h.turnOptions[key];
|
|
38
|
+
const raw = (opts as unknown as Record<string, unknown>)[key];
|
|
39
|
+
|
|
40
|
+
// Discovery is a table of facets, handled separately.
|
|
41
|
+
if (key === "discovery") {
|
|
42
|
+
const discovery = raw as DiscoveryOptions | undefined;
|
|
43
|
+
// omitted, null, or {} => no-op
|
|
44
|
+
if (discovery === undefined || discovery === null) continue;
|
|
45
|
+
if (!isPlainObject(discovery as unknown)) {
|
|
46
|
+
throw new ArgvRefusalError({
|
|
47
|
+
issue: "invalid-option-value",
|
|
48
|
+
harness: h.name,
|
|
49
|
+
option: "discovery",
|
|
50
|
+
supported: ["discovery must be an object"],
|
|
51
|
+
detail: String(raw),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const discObj = discovery as Record<string, unknown>;
|
|
55
|
+
// If spec absent, any facet set to false must refuse.
|
|
56
|
+
if (spec === undefined) {
|
|
57
|
+
const requested = DISCOVERY_FACETS.filter((f) => discObj[f] === false);
|
|
58
|
+
if (requested.length === 0) continue;
|
|
59
|
+
throw new ArgvRefusalError({
|
|
60
|
+
issue: "unsupported-option",
|
|
61
|
+
harness: h.name,
|
|
62
|
+
option: "discovery",
|
|
63
|
+
supported: Object.keys(h.turnOptions).length ? Object.keys(h.turnOptions) : ["(none)"],
|
|
64
|
+
detail: String(requested[0]),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (spec.kind !== "discovery") {
|
|
68
|
+
throw new ArgvRefusalError({
|
|
69
|
+
issue: "invalid-option-value",
|
|
70
|
+
harness: h.name,
|
|
71
|
+
option: "discovery",
|
|
72
|
+
supported: ["discovery spec malformed"],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// Validate each facet value is boolean if present
|
|
76
|
+
for (const facet of DISCOVERY_FACETS) {
|
|
77
|
+
if (!(facet in discObj)) continue;
|
|
78
|
+
const v = discObj[facet];
|
|
79
|
+
if (v !== undefined && typeof v !== "boolean") {
|
|
80
|
+
throw new ArgvRefusalError({
|
|
81
|
+
issue: "invalid-option-value",
|
|
82
|
+
harness: h.name,
|
|
83
|
+
option: "discovery",
|
|
84
|
+
facet,
|
|
85
|
+
supported: ["true or false"],
|
|
86
|
+
detail: String(v),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Render facets in DISCOVERY_FACETS order
|
|
91
|
+
for (const facet of DISCOVERY_FACETS) {
|
|
92
|
+
const val = discObj[facet];
|
|
93
|
+
if (val === undefined) continue;
|
|
94
|
+
// Polarity handling: disables emits on false, enables emits on true
|
|
95
|
+
const facetSpec = spec.facets[facet];
|
|
96
|
+
if (facetSpec === undefined) {
|
|
97
|
+
// If caller asked to disable (false for disables, true for enables) but facet not declared, refuse
|
|
98
|
+
// But if caller passed true for a disables facet, it's a no-op, not a refusal
|
|
99
|
+
// So only refuse when the caller's value would have emitted
|
|
100
|
+
// For all current descriptors polarity is disables, so false would emit -> refuse
|
|
101
|
+
// For safety, check wouldEmit logic
|
|
102
|
+
// If val would not emit anyway, skip
|
|
103
|
+
// Determine wouldEmit: if facetSpec undefined, we cannot know polarity, so treat false as intended disable -> refuse
|
|
104
|
+
if (val === false) {
|
|
105
|
+
const supportedFacets = Object.keys(spec.facets);
|
|
106
|
+
throw new ArgvRefusalError({
|
|
107
|
+
issue: "unsupported-option-facet",
|
|
108
|
+
harness: h.name,
|
|
109
|
+
option: "discovery",
|
|
110
|
+
facet,
|
|
111
|
+
supported: supportedFacets.length ? supportedFacets : ["(none)"],
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// Determine if this value should emit
|
|
117
|
+
let shouldEmit = false;
|
|
118
|
+
if (facetSpec.polarity === "disables" && val === false) shouldEmit = true;
|
|
119
|
+
else if (facetSpec.polarity === "enables" && val === true) shouldEmit = true;
|
|
120
|
+
else {
|
|
121
|
+
// true for disables or false for enables => no-op
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!shouldEmit) continue;
|
|
125
|
+
// Check resumeRender refusal
|
|
126
|
+
const effective = resolveRender(facetSpec, phase);
|
|
127
|
+
if (effective === null) {
|
|
128
|
+
const resumeSupported = Object.entries(spec.facets)
|
|
129
|
+
.filter(
|
|
130
|
+
([, v]) =>
|
|
131
|
+
resolveRender(
|
|
132
|
+
v as unknown as import("../knowledge/descriptor.js").SpecBase,
|
|
133
|
+
"resume",
|
|
134
|
+
) !== null,
|
|
135
|
+
)
|
|
136
|
+
.map(([k]) => k);
|
|
137
|
+
throw new ArgvRefusalError({
|
|
138
|
+
issue: "unsupported-on-resume",
|
|
139
|
+
harness: h.name,
|
|
140
|
+
option: "discovery",
|
|
141
|
+
facet,
|
|
142
|
+
supported: resumeSupported.length ? resumeSupported : ["(none resume)"],
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
// Reject config-kv for discovery (only closed vocab, discovery is not)
|
|
146
|
+
if ((effective as OptionRender).kind === "config-kv") {
|
|
147
|
+
throw new ArgvRefusalError({
|
|
148
|
+
issue: "invalid-option-value",
|
|
149
|
+
harness: h.name,
|
|
150
|
+
option: "discovery",
|
|
151
|
+
facet,
|
|
152
|
+
supported: ["flag-list only for discovery"],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
let seq: string[];
|
|
156
|
+
if ((effective as OptionRender).kind === "flag-list")
|
|
157
|
+
seq = [...(effective as Extract<OptionRender, { kind: "flag-list" }>).flags];
|
|
158
|
+
else if ((effective as OptionRender).kind === "flag-value")
|
|
159
|
+
seq = [(effective as Extract<OptionRender, { kind: "flag-value" }>).flag, ""];
|
|
160
|
+
else seq = [];
|
|
161
|
+
sequences.push(seq);
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Non-discovery keys
|
|
167
|
+
// Handle enum default on launch
|
|
168
|
+
if (raw === undefined) {
|
|
169
|
+
if (
|
|
170
|
+
spec !== undefined &&
|
|
171
|
+
spec.kind === "enum" &&
|
|
172
|
+
spec.default !== undefined &&
|
|
173
|
+
phase === "launch"
|
|
174
|
+
) {
|
|
175
|
+
const effective = resolveRender(spec as unknown as SpecBase, phase);
|
|
176
|
+
if (effective === null) continue; // should not happen for launch
|
|
177
|
+
// config-kv is allowed for enum (closed vocab) - no rejection needed here
|
|
178
|
+
let seq: string[];
|
|
179
|
+
if ((effective as OptionRender).kind === "flag-value")
|
|
180
|
+
seq = [(effective as Extract<OptionRender, { kind: "flag-value" }>).flag, spec.default];
|
|
181
|
+
else if ((effective as OptionRender).kind === "config-kv")
|
|
182
|
+
seq = [
|
|
183
|
+
(effective as Extract<OptionRender, { kind: "config-kv" }>).flag,
|
|
184
|
+
`${(effective as Extract<OptionRender, { kind: "config-kv" }>).key}=${tomlQuote(spec.default)}`,
|
|
185
|
+
];
|
|
186
|
+
else if ((effective as OptionRender).kind === "flag-list")
|
|
187
|
+
seq = [...(effective as Extract<OptionRender, { kind: "flag-list" }>).flags];
|
|
188
|
+
else seq = [];
|
|
189
|
+
sequences.push(seq);
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Raw is defined but spec is absent => unsupported-option
|
|
195
|
+
if (spec === undefined) {
|
|
196
|
+
throw new ArgvRefusalError({
|
|
197
|
+
issue: "unsupported-option",
|
|
198
|
+
harness: h.name,
|
|
199
|
+
option: key,
|
|
200
|
+
supported: Object.keys(h.turnOptions).length ? Object.keys(h.turnOptions) : ["(none)"],
|
|
201
|
+
detail: String(raw),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check resumeRender null => unsupported-on-resume
|
|
206
|
+
// For discovery we handled per-facet; for others check spec
|
|
207
|
+
if ((spec as { kind: string }).kind !== "discovery") {
|
|
208
|
+
const effective = resolveRender(spec as unknown as SpecBase, phase);
|
|
209
|
+
if (effective === null) {
|
|
210
|
+
const resumeSupported = TURN_OPTION_KEYS.filter((k) => {
|
|
211
|
+
const s = h.turnOptions[k];
|
|
212
|
+
if (!s) return false;
|
|
213
|
+
if ((s as { kind: string }).kind === "discovery") {
|
|
214
|
+
const facets = (s as unknown as { facets: Record<string, unknown> }).facets;
|
|
215
|
+
return Object.keys(facets).length > 0;
|
|
216
|
+
}
|
|
217
|
+
return resolveRender(s as unknown as SpecBase, "resume") !== null;
|
|
218
|
+
}) as string[];
|
|
219
|
+
throw new ArgvRefusalError({
|
|
220
|
+
issue: "unsupported-on-resume",
|
|
221
|
+
harness: h.name,
|
|
222
|
+
option: key,
|
|
223
|
+
supported: resumeSupported.length ? resumeSupported : ["(none)"],
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
// Reject config-kv for non-closed vocabulary
|
|
227
|
+
if (
|
|
228
|
+
(spec as unknown as SpecBase).render.kind === "config-kv" &&
|
|
229
|
+
(spec as { kind: string }).kind !== "enum" &&
|
|
230
|
+
(spec as { kind: string }).kind !== "effort"
|
|
231
|
+
) {
|
|
232
|
+
throw new ArgvRefusalError({
|
|
233
|
+
issue: "invalid-option-value",
|
|
234
|
+
harness: h.name,
|
|
235
|
+
option: key,
|
|
236
|
+
supported: ["config-kv only for enum and effort (closed vocabularies)"],
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Per-kind validation and token generation
|
|
242
|
+
const effectiveRender =
|
|
243
|
+
(spec as { kind: string }).kind === "discovery"
|
|
244
|
+
? null
|
|
245
|
+
: resolveRender(spec as unknown as SpecBase, phase);
|
|
246
|
+
// effectiveRender already checked for null
|
|
247
|
+
|
|
248
|
+
switch (spec.kind) {
|
|
249
|
+
case "effort": {
|
|
250
|
+
if (typeof raw !== "string") {
|
|
251
|
+
throw new ArgvRefusalError({
|
|
252
|
+
issue: "invalid-option-value",
|
|
253
|
+
harness: h.name,
|
|
254
|
+
option: key,
|
|
255
|
+
supported: [...h.vocabulary.efforts],
|
|
256
|
+
detail: String(raw),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
const validated = validateEffort(
|
|
260
|
+
h,
|
|
261
|
+
raw,
|
|
262
|
+
(opts as unknown as Record<string, unknown>).model as string | undefined,
|
|
263
|
+
);
|
|
264
|
+
if (!validated.ok) {
|
|
265
|
+
let ladder = h.vocabulary.efforts;
|
|
266
|
+
const model = (opts as unknown as Record<string, unknown>).model as string | undefined;
|
|
267
|
+
if (model !== undefined && h.vocabulary.effortsByModel !== undefined) {
|
|
268
|
+
const { id } = resolveModel(h, model);
|
|
269
|
+
const perModel = Object.hasOwn(h.vocabulary.effortsByModel, id)
|
|
270
|
+
? h.vocabulary.effortsByModel[id]
|
|
271
|
+
: undefined;
|
|
272
|
+
if (perModel !== undefined) ladder = perModel;
|
|
273
|
+
}
|
|
274
|
+
throw new ArgvRefusalError({
|
|
275
|
+
issue: "unknown-effort",
|
|
276
|
+
harness: h.name,
|
|
277
|
+
option: key,
|
|
278
|
+
supported: [...ladder],
|
|
279
|
+
detail: raw,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const r = effectiveRender!;
|
|
283
|
+
let seq: string[];
|
|
284
|
+
if (r.kind === "flag-value") seq = [r.flag, raw];
|
|
285
|
+
else if (r.kind === "config-kv") seq = [r.flag, `${r.key}=${tomlQuote(raw)}`];
|
|
286
|
+
else if (r.kind === "flag-list") seq = [...r.flags];
|
|
287
|
+
else seq = [];
|
|
288
|
+
sequences.push(seq);
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
case "enum": {
|
|
292
|
+
if (typeof raw !== "string") {
|
|
293
|
+
throw new ArgvRefusalError({
|
|
294
|
+
issue: "invalid-option-value",
|
|
295
|
+
harness: h.name,
|
|
296
|
+
option: key,
|
|
297
|
+
supported: [...spec.values],
|
|
298
|
+
detail: String(raw),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (!spec.values.includes(raw)) {
|
|
302
|
+
throw new ArgvRefusalError({
|
|
303
|
+
issue: "invalid-option-value",
|
|
304
|
+
harness: h.name,
|
|
305
|
+
option: key,
|
|
306
|
+
supported: [...spec.values],
|
|
307
|
+
detail: raw,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const r = effectiveRender!;
|
|
311
|
+
let seq: string[];
|
|
312
|
+
if (r.kind === "flag-value") seq = [r.flag, raw];
|
|
313
|
+
else if (r.kind === "config-kv") seq = [r.flag, `${r.key}=${tomlQuote(raw)}`];
|
|
314
|
+
else if (r.kind === "flag-list") seq = [...r.flags];
|
|
315
|
+
else seq = [];
|
|
316
|
+
sequences.push(seq);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
case "selector": {
|
|
320
|
+
if (typeof raw !== "string") {
|
|
321
|
+
throw new ArgvRefusalError({
|
|
322
|
+
issue: "invalid-option-value",
|
|
323
|
+
harness: h.name,
|
|
324
|
+
option: key,
|
|
325
|
+
supported: ["CLEAN_SELECTOR"],
|
|
326
|
+
detail: String(raw),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (!CLEAN_SELECTOR.test(raw)) {
|
|
330
|
+
throw new ArgvRefusalError({
|
|
331
|
+
issue: "invalid-option-value",
|
|
332
|
+
harness: h.name,
|
|
333
|
+
option: key,
|
|
334
|
+
supported: ["must match CLEAN_SELECTOR /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,127}$/"],
|
|
335
|
+
detail: raw,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
const r = effectiveRender!;
|
|
339
|
+
let seq: string[];
|
|
340
|
+
if (r.kind === "flag-value") seq = [r.flag, raw];
|
|
341
|
+
else if (r.kind === "config-kv") seq = [r.flag, `${r.key}=${tomlQuote(raw)}`];
|
|
342
|
+
else if (r.kind === "flag-list") seq = [...r.flags];
|
|
343
|
+
else seq = [];
|
|
344
|
+
sequences.push(seq);
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
case "toggle": {
|
|
348
|
+
if (typeof raw !== "boolean") {
|
|
349
|
+
throw new ArgvRefusalError({
|
|
350
|
+
issue: "invalid-option-value",
|
|
351
|
+
harness: h.name,
|
|
352
|
+
option: key,
|
|
353
|
+
supported: ["true or false"],
|
|
354
|
+
detail: String(raw),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
let shouldEmit = false;
|
|
358
|
+
if (spec.polarity === "disables" && raw === false) shouldEmit = true;
|
|
359
|
+
else if (spec.polarity === "enables" && raw === true) shouldEmit = true;
|
|
360
|
+
if (!shouldEmit) break;
|
|
361
|
+
const r = effectiveRender!;
|
|
362
|
+
let seq: string[];
|
|
363
|
+
if (r.kind === "flag-list") seq = [...r.flags];
|
|
364
|
+
else if (r.kind === "flag-value") seq = [r.flag, String(raw)];
|
|
365
|
+
else if (r.kind === "config-kv") seq = [r.flag, `${r.key}=${tomlQuote(String(raw))}`];
|
|
366
|
+
else seq = [];
|
|
367
|
+
sequences.push(seq);
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
case "integer": {
|
|
371
|
+
if (typeof raw !== "number") {
|
|
372
|
+
throw new ArgvRefusalError({
|
|
373
|
+
issue: "invalid-option-value",
|
|
374
|
+
harness: h.name,
|
|
375
|
+
option: key,
|
|
376
|
+
supported: [`[${spec.min}, ${spec.max}] integer`],
|
|
377
|
+
detail: String(raw),
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
if (!Number.isFinite(raw) || !Number.isInteger(raw) || raw < spec.min || raw > spec.max) {
|
|
381
|
+
throw new ArgvRefusalError({
|
|
382
|
+
issue: "invalid-option-value",
|
|
383
|
+
harness: h.name,
|
|
384
|
+
option: key,
|
|
385
|
+
supported: [`[${spec.min}, ${spec.max}] integer`],
|
|
386
|
+
detail: String(raw),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
const r = effectiveRender!;
|
|
390
|
+
let seq: string[];
|
|
391
|
+
if (r.kind === "flag-value") seq = [r.flag, String(raw)];
|
|
392
|
+
else if (r.kind === "config-kv") seq = [r.flag, `${r.key}=${tomlQuote(String(raw))}`];
|
|
393
|
+
else if (r.kind === "flag-list") seq = [...r.flags];
|
|
394
|
+
else seq = [];
|
|
395
|
+
sequences.push(seq);
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
case "discovery":
|
|
399
|
+
// already handled
|
|
400
|
+
break;
|
|
401
|
+
default: {
|
|
402
|
+
const _exhaustive: never = spec as never;
|
|
403
|
+
throw new ArgvRefusalError({
|
|
404
|
+
issue: "invalid-option-value",
|
|
405
|
+
harness: h.name,
|
|
406
|
+
option: key,
|
|
407
|
+
supported: [],
|
|
408
|
+
detail: String(_exhaustive),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// De-duplication by exact token sequence, first occurrence winning
|
|
415
|
+
const seen = new Set<string>();
|
|
416
|
+
const deduped: string[][] = [];
|
|
417
|
+
for (const seq of sequences) {
|
|
418
|
+
const k = seq.join("\0");
|
|
419
|
+
if (seen.has(k)) continue;
|
|
420
|
+
seen.add(k);
|
|
421
|
+
deduped.push(seq);
|
|
422
|
+
}
|
|
423
|
+
return deduped.flat();
|
|
424
|
+
};
|
|
@@ -18,7 +18,7 @@ export interface ValidationRefusal {
|
|
|
18
18
|
* models as provider/id[:thinking], so word characters plus the few real
|
|
19
19
|
* separators - never whitespace, shell metacharacters, or control/format
|
|
20
20
|
* characters, and bounded like session ids. */
|
|
21
|
-
const CLEAN_SELECTOR = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,127}$/;
|
|
21
|
+
export const CLEAN_SELECTOR = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,127}$/;
|
|
22
22
|
|
|
23
23
|
export interface ResolvedModel {
|
|
24
24
|
readonly id: string;
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The claude-code descriptor: facts about the `claude` CLI as data, verified
|
|
3
|
-
* against claude 2.1.
|
|
3
|
+
* against claude 2.1.229 and the 00-chat-substrate spike evidence (A-001,
|
|
4
4
|
* A-002, A-005). No process logic lives here.
|
|
5
|
+
*
|
|
6
|
+
* Discovery: claude 2.1.229 has no isolated instruction-file toggle.
|
|
7
|
+
* `--bare` would stop `CLAUDE.md` discovery, but it also skips hooks, LSP,
|
|
8
|
+
* plugin sync, auto-memory and keychain reads, and forces auth to
|
|
9
|
+
* `ANTHROPIC_API_KEY` / `apiKeyHelper` (OAuth never read) - so a caller
|
|
10
|
+
* authenticated via OAuth would break. The descriptor therefore offers no
|
|
11
|
+
* `instructionFiles` facet and a call passing it must refuse. Likewise
|
|
12
|
+
* `tools` has no discovery flag on claude and refuses.
|
|
13
|
+
*
|
|
14
|
+
* Effort: A-002 showed `--effort bogus` warns on stderr and runs at DEFAULT
|
|
15
|
+
* effort, exit 0, with nothing echoed in the stream. The library-side
|
|
16
|
+
* `validateEffort` refusal is therefore load-bearing, not cosmetic.
|
|
5
17
|
*/
|
|
6
18
|
import { deepFreeze, type HarnessDescriptor, UUID_SHAPE } from "./descriptor.js";
|
|
7
19
|
import { SHARED_AUTH_MATCHERS, SHARED_LIMIT_MATCHERS } from "./matchers.js";
|
|
@@ -9,7 +21,7 @@ import { SHARED_AUTH_MATCHERS, SHARED_LIMIT_MATCHERS } from "./matchers.js";
|
|
|
9
21
|
export const claudeCode: HarnessDescriptor = deepFreeze({
|
|
10
22
|
name: "claude",
|
|
11
23
|
bin: "claude",
|
|
12
|
-
verifiedAgainst: "2.1.
|
|
24
|
+
verifiedAgainst: "2.1.229",
|
|
13
25
|
versionSource: { kind: "npm", package: "@anthropic-ai/claude-code" },
|
|
14
26
|
launch: {
|
|
15
27
|
baseFlags: ["-p"],
|
|
@@ -69,17 +81,17 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
|
|
|
69
81
|
limitMatchers: [
|
|
70
82
|
// Observed phrasings from the live CLI (ported from lucid v1's limits.ts):
|
|
71
83
|
// "You've hit your session limit · resets 6:30pm"
|
|
72
|
-
|
|
84
|
+
{ pattern: "you'?ve hit your session limit", flags: "i", code: "session-limit" },
|
|
73
85
|
// "You've hit your weekly limit · resets 2am (Asia/Bangkok)"
|
|
74
|
-
|
|
86
|
+
{ pattern: "you'?ve hit your weekly limit", flags: "i", code: "weekly-limit" },
|
|
75
87
|
...SHARED_LIMIT_MATCHERS,
|
|
76
88
|
],
|
|
77
89
|
authMatchers: [
|
|
78
90
|
// A detached process cannot read Keychain creds, and misreading that as
|
|
79
91
|
// "not logged in" sends the human to redo a login that was never broken.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
{ pattern: "oauth session expired|could not be refreshed", flags: "i", kind: "expired" },
|
|
93
|
+
{ pattern: "failed to authenticate", flags: "i", kind: "expired" },
|
|
94
|
+
{ pattern: "not logged in|please run \\/login", flags: "i", kind: "not-logged-in" },
|
|
83
95
|
...SHARED_AUTH_MATCHERS,
|
|
84
96
|
],
|
|
85
97
|
autonomy: { flag: "--dangerously-skip-permissions" },
|
|
@@ -93,8 +105,6 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
|
|
|
93
105
|
haiku: "claude-haiku-4-5-20251001",
|
|
94
106
|
},
|
|
95
107
|
efforts: ["low", "medium", "high", "xhigh", "max"],
|
|
96
|
-
// Effort is an in-session command for claude, not a launch flag.
|
|
97
|
-
effortFlag: null,
|
|
98
108
|
extensible: false,
|
|
99
109
|
},
|
|
100
110
|
store: {
|
|
@@ -111,11 +121,7 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
|
|
|
111
121
|
usedPctField: "used_percentage",
|
|
112
122
|
},
|
|
113
123
|
resumeLast: null,
|
|
114
|
-
provider: null,
|
|
115
124
|
stdin: "inherit",
|
|
116
|
-
// D-025: project-only setting sources is claude's discovery-isolation
|
|
117
|
-
// spelling; the child loads no user-level hooks or skills.
|
|
118
|
-
discoveryDisableFlags: ["--setting-sources", "project"],
|
|
119
125
|
presence: {
|
|
120
126
|
headlessMarkers: ["-p", "--print"],
|
|
121
127
|
},
|
|
@@ -129,4 +135,20 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
|
|
|
129
135
|
},
|
|
130
136
|
session: true,
|
|
131
137
|
},
|
|
138
|
+
turnOptions: {
|
|
139
|
+
effort: { kind: "effort", render: { kind: "flag-value", flag: "--effort" } },
|
|
140
|
+
discovery: {
|
|
141
|
+
kind: "discovery",
|
|
142
|
+
facets: {
|
|
143
|
+
extensions: {
|
|
144
|
+
polarity: "disables",
|
|
145
|
+
render: { kind: "flag-list", flags: ["--setting-sources", "project"] },
|
|
146
|
+
},
|
|
147
|
+
skills: {
|
|
148
|
+
polarity: "disables",
|
|
149
|
+
render: { kind: "flag-list", flags: ["--setting-sources", "project"] },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
132
154
|
});
|
package/src/knowledge/codex.ts
CHANGED
|
@@ -14,13 +14,13 @@ export const codexCli: HarnessDescriptor = deepFreeze({
|
|
|
14
14
|
versionSource: { kind: "npm", package: "@openai/codex" },
|
|
15
15
|
launch: {
|
|
16
16
|
// exec --json emits structured item events; without --json, identity
|
|
17
|
-
// discovery is blind (v1: requiredArgument "--json").
|
|
18
|
-
// is v1's proven spawn shape - codex's built-in default is read-only,
|
|
19
|
-
// under which a non-autonomy turn cannot write files.
|
|
17
|
+
// discovery is blind (v1: requiredArgument "--json").
|
|
20
18
|
// --skip-git-repo-check: codex exec refuses to run outside a trusted
|
|
21
19
|
// git dir without it (verified 0.147.0). cwd targeting is the spawner's
|
|
22
20
|
// job (spawn opts.cwd), not descriptor data.
|
|
23
|
-
|
|
21
|
+
// Sandbox is now per-call via turnOptions.sandbox with default
|
|
22
|
+
// workspace-write, not a hardcoded base flag.
|
|
23
|
+
baseFlags: ["exec", "--json", "--skip-git-repo-check"],
|
|
24
24
|
subcommands: ["exec"],
|
|
25
25
|
promptStyle: "positional",
|
|
26
26
|
toolsFlag: null,
|
|
@@ -54,24 +54,24 @@ export const codexCli: HarnessDescriptor = deepFreeze({
|
|
|
54
54
|
announce: { match: { type: "thread.started" }, idField: "thread_id" },
|
|
55
55
|
},
|
|
56
56
|
limitMatchers: [...SHARED_LIMIT_MATCHERS],
|
|
57
|
-
authMatchers: [
|
|
57
|
+
authMatchers: [
|
|
58
|
+
{ pattern: "run codex login", flags: "i", kind: "not-logged-in" },
|
|
59
|
+
...SHARED_AUTH_MATCHERS,
|
|
60
|
+
],
|
|
58
61
|
// Accepted by codex 0.147.0 as a hidden alias (not in --help).
|
|
59
62
|
autonomy: { flag: "--yolo" },
|
|
60
63
|
vocabulary: {
|
|
61
64
|
modelFlag: "--model",
|
|
62
65
|
models: ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"],
|
|
63
66
|
aliases: {},
|
|
64
|
-
efforts: ["minimal", "low", "medium", "high", "xhigh", "max"
|
|
67
|
+
efforts: ["minimal", "low", "medium", "high", "xhigh", "max"],
|
|
65
68
|
// Codex constrains ladders per model generation (v1 registry).
|
|
66
69
|
effortsByModel: {
|
|
67
70
|
"gpt-5.5": ["minimal", "low", "medium", "high"],
|
|
68
|
-
"gpt-5.6-sol": ["medium", "high", "xhigh", "max"
|
|
69
|
-
"gpt-5.6-terra": ["medium", "high", "xhigh", "max"
|
|
70
|
-
"gpt-5.6-luna": ["medium", "high", "xhigh", "max"
|
|
71
|
+
"gpt-5.6-sol": ["medium", "high", "xhigh", "max"],
|
|
72
|
+
"gpt-5.6-terra": ["medium", "high", "xhigh", "max"],
|
|
73
|
+
"gpt-5.6-luna": ["medium", "high", "xhigh", "max"],
|
|
71
74
|
},
|
|
72
|
-
// Reasoning effort is a config key (-c model_reasoning_effort=...), not
|
|
73
|
-
// a plain flag; argv-level insertion has no spelling to use.
|
|
74
|
-
effortFlag: null,
|
|
75
75
|
extensible: false,
|
|
76
76
|
},
|
|
77
77
|
store: {
|
|
@@ -84,12 +84,10 @@ export const codexCli: HarnessDescriptor = deepFreeze({
|
|
|
84
84
|
contextHook: null,
|
|
85
85
|
// Valid only in the `exec resume` context: `codex exec resume --last`.
|
|
86
86
|
resumeLast: { flag: "--last" },
|
|
87
|
-
provider: null,
|
|
88
87
|
// codex exec appends piped stdin as a <stdin> block and can block on an
|
|
89
88
|
// open stdin - close it (verified 0.147.0: "Reading additional input
|
|
90
89
|
// from stdin...").
|
|
91
90
|
stdin: "close-required",
|
|
92
|
-
discoveryDisableFlags: [],
|
|
93
91
|
presence: {
|
|
94
92
|
headlessMarkers: ["exec"],
|
|
95
93
|
},
|
|
@@ -103,4 +101,17 @@ export const codexCli: HarnessDescriptor = deepFreeze({
|
|
|
103
101
|
},
|
|
104
102
|
session: false,
|
|
105
103
|
},
|
|
104
|
+
turnOptions: {
|
|
105
|
+
effort: {
|
|
106
|
+
kind: "effort",
|
|
107
|
+
render: { kind: "config-kv", flag: "-c", key: "model_reasoning_effort" },
|
|
108
|
+
},
|
|
109
|
+
sandbox: {
|
|
110
|
+
kind: "enum",
|
|
111
|
+
values: ["read-only", "workspace-write", "danger-full-access"],
|
|
112
|
+
default: "workspace-write",
|
|
113
|
+
render: { kind: "flag-value", flag: "--sandbox" },
|
|
114
|
+
resumeRender: null,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
106
117
|
});
|