@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
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Descriptor types: the shape of what is KNOWN about a harness CLI, as pure
|
|
3
3
|
* data. Interpretation functions consume these; nothing here executes.
|
|
4
|
+
*
|
|
5
|
+
* Turn option keys and discovery facets use a closed vocabulary for the same
|
|
6
|
+
* reason `LimitCode` does: a descriptor must not invent an option a consumer
|
|
7
|
+
* has no field for. Every `TurnOptionKey` maps to a field on `TurnOptions`
|
|
8
|
+
* and every `DiscoveryFacet` maps to a field on `DiscoveryOptions`; adding
|
|
9
|
+
* a key without a consumer arm would be dead data that can only drift.
|
|
4
10
|
*/
|
|
5
11
|
|
|
6
12
|
export const HARNESS_NAMES = ["claude", "codex", "pi", "muse"] as const;
|
|
@@ -34,13 +40,136 @@ export interface SessionInputContract {
|
|
|
34
40
|
/** Consumers branch on these (session-limit: wait for reset; weekly-limit:
|
|
35
41
|
* route elsewhere), so the vocabulary is closed - a descriptor cannot invent
|
|
36
42
|
* a code a consumer has no arm for. */
|
|
37
|
-
export type LimitCode =
|
|
43
|
+
export type LimitCode =
|
|
44
|
+
| "usage-limit"
|
|
45
|
+
| "session-limit"
|
|
46
|
+
| "weekly-limit"
|
|
47
|
+
| "credits"
|
|
48
|
+
| "quota"
|
|
49
|
+
| "rate-limit";
|
|
38
50
|
|
|
39
51
|
/** Auth walls are separate from usage limits because the remedy is entirely
|
|
40
52
|
* different (re-auth vs wait), and a detached process misreading one as the
|
|
41
53
|
* other sends the human to fix the wrong thing. */
|
|
42
54
|
export type AuthFailureKind = "not-logged-in" | "expired" | "invalid-key";
|
|
43
55
|
|
|
56
|
+
/** Serializable wall matcher: pattern and flags are data, not a RegExp
|
|
57
|
+
* literal, so an override file can extend them. `compileMatchers` turns
|
|
58
|
+
* these into RegExps with bounds (pattern length, count, allowed flags)
|
|
59
|
+
* and a WeakMap cache so the same array instance reuses compiled RegExps.
|
|
60
|
+
* The input window (first 4096 chars of a line), not pattern analysis, is
|
|
61
|
+
* the backtracking bound. */
|
|
62
|
+
export interface LimitMatcher {
|
|
63
|
+
readonly pattern: string;
|
|
64
|
+
readonly flags?: string;
|
|
65
|
+
readonly code: LimitCode;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface AuthMatcher {
|
|
69
|
+
readonly pattern: string;
|
|
70
|
+
readonly flags?: string;
|
|
71
|
+
readonly kind: AuthFailureKind;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The turn-option vocabulary is closed for the same reason `LimitCode` is:
|
|
75
|
+
* a descriptor must not invent an option a consumer has no field for. Every
|
|
76
|
+
* key here maps to a field on `TurnOptions`; adding a key without a consumer
|
|
77
|
+
* arm would be dead data that can only drift. Render order is the tuple
|
|
78
|
+
* order, so argv is deterministic regardless of caller field order. */
|
|
79
|
+
export const TURN_OPTION_KEYS = deepFreeze([
|
|
80
|
+
"effort",
|
|
81
|
+
"sandbox",
|
|
82
|
+
"provider",
|
|
83
|
+
"discovery",
|
|
84
|
+
"write",
|
|
85
|
+
"shell",
|
|
86
|
+
"maxSteps",
|
|
87
|
+
] as const);
|
|
88
|
+
export type TurnOptionKey = (typeof TURN_OPTION_KEYS)[number];
|
|
89
|
+
|
|
90
|
+
/** Discovery facets are closed for the same reason as `TurnOptionKey`: each
|
|
91
|
+
* maps to a field on `DiscoveryOptions` and a concrete flag spelling per
|
|
92
|
+
* harness. A harness that cannot express a facet simply omits it from its
|
|
93
|
+
* `turnOptions.discovery.facets` table; a call passing that facet must
|
|
94
|
+
* refuse. */
|
|
95
|
+
export const DISCOVERY_FACETS = deepFreeze([
|
|
96
|
+
"tools",
|
|
97
|
+
"instructionFiles",
|
|
98
|
+
"extensions",
|
|
99
|
+
"skills",
|
|
100
|
+
] as const);
|
|
101
|
+
export type DiscoveryFacet = (typeof DISCOVERY_FACETS)[number];
|
|
102
|
+
|
|
103
|
+
export type OptionRender =
|
|
104
|
+
/** `--flag <value>` */
|
|
105
|
+
| { readonly kind: "flag-value"; readonly flag: string }
|
|
106
|
+
/** `-c key=value` - codex's config-override grammar. Permitted only for
|
|
107
|
+
* closed-vocabulary specs, so no value can need escaping. */
|
|
108
|
+
| { readonly kind: "config-kv"; readonly flag: string; readonly key: string }
|
|
109
|
+
/** A fixed multi-token flag set emitted verbatim, value-less. */
|
|
110
|
+
| { readonly kind: "flag-list"; readonly flags: readonly string[] };
|
|
111
|
+
|
|
112
|
+
export interface SpecBase {
|
|
113
|
+
readonly render: OptionRender;
|
|
114
|
+
/** The spelling the RESUME grammar accepts. Omitted means "same as
|
|
115
|
+
* `render`"; an explicit `null` declares the option unexpressible on
|
|
116
|
+
* resume, and building a resume argv with it must refuse. */
|
|
117
|
+
readonly resumeRender?: OptionRender | null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type TurnOptionSpec =
|
|
121
|
+
/** Closed value vocabulary. `default` renders on LAUNCH ONLY. */
|
|
122
|
+
| (SpecBase & {
|
|
123
|
+
readonly kind: "enum";
|
|
124
|
+
readonly values: readonly string[];
|
|
125
|
+
readonly default?: string;
|
|
126
|
+
})
|
|
127
|
+
/** Ladder comes from vocabulary.efforts / effortsByModel, not from here. */
|
|
128
|
+
| (SpecBase & { readonly kind: "effort" })
|
|
129
|
+
/** Open selector, CLEAN_SELECTOR-validated. */
|
|
130
|
+
| (SpecBase & { readonly kind: "selector" })
|
|
131
|
+
/** `polarity: "disables"` emits the render when the caller asks for FALSE. */
|
|
132
|
+
| (SpecBase & { readonly kind: "toggle"; readonly polarity: "enables" | "disables" })
|
|
133
|
+
| (SpecBase & { readonly kind: "integer"; readonly min: number; readonly max: number })
|
|
134
|
+
/** Per-facet toggles; a facet absent from the table cannot be expressed. */
|
|
135
|
+
| {
|
|
136
|
+
readonly kind: "discovery";
|
|
137
|
+
readonly facets: Readonly<
|
|
138
|
+
Partial<Record<DiscoveryFacet, SpecBase & { readonly polarity: "enables" | "disables" }>>
|
|
139
|
+
>;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/** Resolve the effective render for a spec at a given phase.
|
|
143
|
+
* - `launch` always uses `render`.
|
|
144
|
+
* - `resume` with `resumeRender: null` is unexpressible (returns null).
|
|
145
|
+
* - `resume` with `resumeRender` omitted resolves to the same render as `render`.
|
|
146
|
+
* - `resume` with an explicit `resumeRender` uses that spelling.
|
|
147
|
+
* Discovery facets use the same rule per facet; this helper works for any
|
|
148
|
+
* `SpecBase` (top-level specs and per-facet specs alike). */
|
|
149
|
+
export const resolveRender = (spec: SpecBase, phase: "launch" | "resume"): OptionRender | null => {
|
|
150
|
+
if (phase === "launch") return spec.render;
|
|
151
|
+
if (spec.resumeRender === null) return null;
|
|
152
|
+
return spec.resumeRender ?? spec.render;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** Alias for `resolveRender` with the resume-only null semantics made
|
|
156
|
+
* explicit in the name; useful for tests asserting the "omitted => same as
|
|
157
|
+
* render, null => unexpressible" contract. */
|
|
158
|
+
export const resolveResumeRender = (spec: SpecBase): OptionRender | null =>
|
|
159
|
+
resolveRender(spec, "resume");
|
|
160
|
+
|
|
161
|
+
/** Like `resolveRender` but for a `TurnOptionSpec` that may be a `discovery`
|
|
162
|
+
* table. Returns null for an unexpressible resume, the spec's render for
|
|
163
|
+
* non-discovery specs, and for discovery returns the spec itself (facets are
|
|
164
|
+
* resolved per-facet via `resolveRender`). */
|
|
165
|
+
export const getOptionRender = (
|
|
166
|
+
spec: TurnOptionSpec,
|
|
167
|
+
phase: "launch" | "resume",
|
|
168
|
+
): OptionRender | null => {
|
|
169
|
+
if (spec.kind === "discovery") return null;
|
|
170
|
+
return resolveRender(spec, phase);
|
|
171
|
+
};
|
|
172
|
+
|
|
44
173
|
export interface HarnessDescriptor {
|
|
45
174
|
readonly name: HarnessName;
|
|
46
175
|
readonly bin: string;
|
|
@@ -147,17 +276,15 @@ export interface HarnessDescriptor {
|
|
|
147
276
|
};
|
|
148
277
|
/** "Stopped on a limit" vs crash vs clean exit: the harness's own wall
|
|
149
278
|
* phrasings. First match wins per line. */
|
|
150
|
-
readonly limitMatchers: ReadonlyArray<
|
|
279
|
+
readonly limitMatchers: ReadonlyArray<LimitMatcher>;
|
|
151
280
|
/** Auth-wall phrasings, same scan discipline as limitMatchers. */
|
|
152
|
-
readonly authMatchers: ReadonlyArray<
|
|
281
|
+
readonly authMatchers: ReadonlyArray<AuthMatcher>;
|
|
153
282
|
/** The "run unattended without stops" flag, or null when the harness has
|
|
154
283
|
* no such mode. */
|
|
155
284
|
readonly autonomy: { readonly flag: string } | null;
|
|
156
285
|
/** The harness's own model-id spellings, alias map, and effort ladder.
|
|
157
286
|
* Curated baseline - pi's registry is runtime-extensible, so validation
|
|
158
|
-
* against this vocabulary is a default, not a final word (D-008).
|
|
159
|
-
* `effortFlag` is null where effort is not a launch-time flag (claude:
|
|
160
|
-
* effort is an in-session command, not argv). */
|
|
287
|
+
* against this vocabulary is a default, not a final word (D-008). */
|
|
161
288
|
readonly vocabulary: {
|
|
162
289
|
readonly modelFlag: string;
|
|
163
290
|
readonly models: readonly string[];
|
|
@@ -167,7 +294,6 @@ export interface HarnessDescriptor {
|
|
|
167
294
|
* gpt-5.5 tops out at high; gpt-5.6-* starts at medium). Falls back to
|
|
168
295
|
* the harness-wide `efforts`. */
|
|
169
296
|
readonly effortsByModel?: Readonly<Record<string, readonly string[]>>;
|
|
170
|
-
readonly effortFlag: string | null;
|
|
171
297
|
/** D-008: an extensible vocabulary (pi) accepts clean unknown model
|
|
172
298
|
* selectors at argv time; capability claims for them degrade to
|
|
173
299
|
* unknown until runtime verification. */
|
|
@@ -190,14 +316,9 @@ export interface HarnessDescriptor {
|
|
|
190
316
|
/** Resume-most-recent support (codex --last), or null. The race it opens
|
|
191
317
|
* is owned by the corroboration ranking in interpretation. */
|
|
192
318
|
readonly resumeLast: { readonly flag: string } | null;
|
|
193
|
-
/** Provider/model-route flag where present (pi --provider). */
|
|
194
|
-
readonly provider: { readonly flag: string } | null;
|
|
195
319
|
/** Whether backgrounded headless calls must have stdin closed (pi hangs
|
|
196
320
|
* without `< /dev/null`). */
|
|
197
321
|
readonly stdin: "inherit" | "close-required";
|
|
198
|
-
/** Flags that disable instruction-file/skill/MCP auto-discovery, in the
|
|
199
|
-
* spelling the harness accepts. */
|
|
200
|
-
readonly discoveryDisableFlags: readonly string[];
|
|
201
322
|
/** Presence recognition: how an interactive process for a session id shows
|
|
202
323
|
* up in a process listing. `headlessMarkers` mark a process as headless
|
|
203
324
|
* (not interactive presence). Known blind spot, inherent to argv matching:
|
|
@@ -214,4 +335,9 @@ export interface HarnessDescriptor {
|
|
|
214
335
|
readonly streamingByMode: Readonly<Record<HarnessMode, StreamingGranularity>>;
|
|
215
336
|
readonly session: boolean;
|
|
216
337
|
};
|
|
338
|
+
/** Per-call turn options this harness can express, keyed by the closed
|
|
339
|
+
* `TurnOptionKey` vocabulary. Absent keys are unexpressible on this
|
|
340
|
+
* harness; a call passing them must refuse. Discovery is a table of
|
|
341
|
+
* per-facet specs rather than a single flag. */
|
|
342
|
+
readonly turnOptions: Readonly<Partial<Record<TurnOptionKey, TurnOptionSpec>>>;
|
|
217
343
|
}
|
|
@@ -4,16 +4,29 @@
|
|
|
4
4
|
* or a provider 401 must classify no matter which CLI printed it. Each
|
|
5
5
|
* descriptor spreads these after its harness-specific phrasings.
|
|
6
6
|
*/
|
|
7
|
-
import type {
|
|
7
|
+
import type { AuthMatcher, LimitMatcher } from "./descriptor.js";
|
|
8
8
|
|
|
9
|
-
export const SHARED_LIMIT_MATCHERS: ReadonlyArray<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
export const SHARED_LIMIT_MATCHERS: ReadonlyArray<LimitMatcher> = [
|
|
10
|
+
{ pattern: "you'?ve hit your usage limit", flags: "i", code: "usage-limit" },
|
|
11
|
+
{ pattern: "usage limit (?:reached|exceeded)", flags: "i", code: "usage-limit" },
|
|
12
|
+
{
|
|
13
|
+
pattern: "purchase more credits|insufficient credits|out of credits",
|
|
14
|
+
flags: "i",
|
|
15
|
+
code: "credits",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
pattern: "resource_exhausted|quota exceeded|exceeded your current quota",
|
|
19
|
+
flags: "i",
|
|
20
|
+
code: "quota",
|
|
21
|
+
},
|
|
22
|
+
// Rate-limit patterns are last so a line with both a usage wall and a 429 keeps usage-limit (first-match-wins, documented cost)
|
|
23
|
+
{ pattern: "429", flags: "i", code: "rate-limit" },
|
|
24
|
+
{ pattern: "Too Many Requests", flags: "i", code: "rate-limit" },
|
|
25
|
+
{ pattern: "rate limit(?:ed|ing)?", flags: "i", code: "rate-limit" },
|
|
26
|
+
{ pattern: "Retry-After", flags: "i", code: "rate-limit" },
|
|
14
27
|
];
|
|
15
28
|
|
|
16
|
-
export const SHARED_AUTH_MATCHERS: ReadonlyArray<
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
export const SHARED_AUTH_MATCHERS: ReadonlyArray<AuthMatcher> = [
|
|
30
|
+
{ pattern: "401 unauthorized", flags: "i", kind: "expired" },
|
|
31
|
+
{ pattern: "invalid api key", flags: "i", kind: "invalid-key" },
|
|
19
32
|
];
|
package/src/knowledge/muse.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* (D-003). The v1 scars this encodes: headless re-entry is `muse exec
|
|
5
5
|
* --session-id <id>` (the positional `muse resume <id>` is the INTERACTIVE
|
|
6
6
|
* picker - recognized when pasted, never built), and `muse exec` exits 0
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* on task failure but 1 on step exhaustion (verified 0.1.0 - see spike
|
|
8
|
+
* report A-003 for `budget` vs `task` split).
|
|
9
9
|
*/
|
|
10
10
|
import { deepFreeze, type HarnessDescriptor, UUID_SHAPE } from "./descriptor.js";
|
|
11
11
|
import { SHARED_AUTH_MATCHERS, SHARED_LIMIT_MATCHERS } from "./matchers.js";
|
|
@@ -59,8 +59,7 @@ export const museCode: HarnessDescriptor = deepFreeze({
|
|
|
59
59
|
modelFlag: "--model",
|
|
60
60
|
models: ["muse-spark-1.2-contributor", "muse-spark-1.2", "muse-spark-1.1"],
|
|
61
61
|
aliases: {},
|
|
62
|
-
efforts: ["none", "minimal", "low", "medium", "high", "xhigh"
|
|
63
|
-
effortFlag: "--reasoning-effort",
|
|
62
|
+
efforts: ["none", "minimal", "low", "medium", "high", "xhigh"],
|
|
64
63
|
extensible: false,
|
|
65
64
|
},
|
|
66
65
|
store: {
|
|
@@ -73,9 +72,7 @@ export const museCode: HarnessDescriptor = deepFreeze({
|
|
|
73
72
|
contextHook: null,
|
|
74
73
|
// `muse resume --last` exists (muse resume --help).
|
|
75
74
|
resumeLast: { flag: "--last" },
|
|
76
|
-
provider: null,
|
|
77
75
|
stdin: "inherit",
|
|
78
|
-
discoveryDisableFlags: [],
|
|
79
76
|
presence: {
|
|
80
77
|
headlessMarkers: ["exec"],
|
|
81
78
|
},
|
|
@@ -91,4 +88,23 @@ export const museCode: HarnessDescriptor = deepFreeze({
|
|
|
91
88
|
},
|
|
92
89
|
session: false,
|
|
93
90
|
},
|
|
91
|
+
turnOptions: {
|
|
92
|
+
effort: { kind: "effort", render: { kind: "flag-value", flag: "--reasoning-effort" } },
|
|
93
|
+
write: {
|
|
94
|
+
kind: "toggle",
|
|
95
|
+
polarity: "disables",
|
|
96
|
+
render: { kind: "flag-list", flags: ["--disable-write"] },
|
|
97
|
+
},
|
|
98
|
+
shell: {
|
|
99
|
+
kind: "toggle",
|
|
100
|
+
polarity: "disables",
|
|
101
|
+
render: { kind: "flag-list", flags: ["--disable-shell"] },
|
|
102
|
+
},
|
|
103
|
+
maxSteps: {
|
|
104
|
+
kind: "integer",
|
|
105
|
+
min: 1,
|
|
106
|
+
max: 10000,
|
|
107
|
+
render: { kind: "flag-value", flag: "--max-model-steps" },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
94
110
|
});
|
|
@@ -32,6 +32,11 @@ export const defaultDescriptors = (): DescriptorSet => ({
|
|
|
32
32
|
muse: museCode,
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
/** Tracks which descriptors were produced by an override file and what their
|
|
36
|
+
* matcher counts are, so the execution layer can emit `matcherOverrides` on
|
|
37
|
+
* the spawn boundary event without re-deriving it. */
|
|
38
|
+
export const matcherOverridesOf = new WeakMap<HarnessDescriptor, { limit: number; auth: number }>();
|
|
39
|
+
|
|
35
40
|
export class OverrideRefusalError extends Error {
|
|
36
41
|
constructor(
|
|
37
42
|
readonly path: string,
|
|
@@ -146,6 +151,80 @@ const mergeValue = (
|
|
|
146
151
|
return override;
|
|
147
152
|
};
|
|
148
153
|
|
|
154
|
+
const validateMatcherPattern = (
|
|
155
|
+
pattern: string,
|
|
156
|
+
flags: string | undefined,
|
|
157
|
+
path: string,
|
|
158
|
+
harness: string,
|
|
159
|
+
): void => {
|
|
160
|
+
const f = flags ?? "i";
|
|
161
|
+
if (pattern.length > 200) {
|
|
162
|
+
throw new OverrideRefusalError(
|
|
163
|
+
path,
|
|
164
|
+
`pattern over 200 characters: ${JSON.stringify(pattern.slice(0, 40))}`,
|
|
165
|
+
harness,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (pattern.length === 0) {
|
|
169
|
+
throw new OverrideRefusalError(path, `pattern must not be empty`, harness);
|
|
170
|
+
}
|
|
171
|
+
if (f.includes("g") || f.includes("y")) {
|
|
172
|
+
throw new OverrideRefusalError(
|
|
173
|
+
path,
|
|
174
|
+
`flags must not contain g or y (got ${JSON.stringify(f)})`,
|
|
175
|
+
harness,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
for (const ch of f) {
|
|
179
|
+
if (!"imsu".includes(ch))
|
|
180
|
+
throw new OverrideRefusalError(path, `flag ${JSON.stringify(ch)} outside imsu`, harness);
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
new RegExp(pattern, f);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
throw new OverrideRefusalError(
|
|
186
|
+
path,
|
|
187
|
+
`uncompilable pattern ${JSON.stringify(pattern)}: ${(e as Error).message}`,
|
|
188
|
+
harness,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const validateMatchers = (desc: HarnessDescriptor, path: string, harness: string): void => {
|
|
194
|
+
const limitMatchers = (desc as unknown as { limitMatchers: readonly unknown[] }).limitMatchers;
|
|
195
|
+
const authMatchers = (desc as unknown as { authMatchers: readonly unknown[] }).authMatchers;
|
|
196
|
+
if (Array.isArray(limitMatchers)) {
|
|
197
|
+
if (limitMatchers.length > 64) {
|
|
198
|
+
throw new OverrideRefusalError(path, `more than 64 limit matchers`, harness);
|
|
199
|
+
}
|
|
200
|
+
for (const m of limitMatchers) {
|
|
201
|
+
if (!isPlain(m as unknown as Record<string, unknown>)) {
|
|
202
|
+
throw new OverrideRefusalError(path, `limit matcher must be an object`, harness);
|
|
203
|
+
}
|
|
204
|
+
const obj = m as Record<string, unknown>;
|
|
205
|
+
if (typeof obj.pattern !== "string" || typeof obj.code !== "string") {
|
|
206
|
+
throw new OverrideRefusalError(path, `limit matcher must have pattern and code`, harness);
|
|
207
|
+
}
|
|
208
|
+
validateMatcherPattern(obj.pattern, obj.flags as string | undefined, path, harness);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(authMatchers)) {
|
|
212
|
+
if (authMatchers.length > 64) {
|
|
213
|
+
throw new OverrideRefusalError(path, `more than 64 auth matchers`, harness);
|
|
214
|
+
}
|
|
215
|
+
for (const m of authMatchers) {
|
|
216
|
+
if (!isPlain(m as unknown as Record<string, unknown>)) {
|
|
217
|
+
throw new OverrideRefusalError(path, `auth matcher must be an object`, harness);
|
|
218
|
+
}
|
|
219
|
+
const obj = m as Record<string, unknown>;
|
|
220
|
+
if (typeof obj.pattern !== "string" || typeof obj.kind !== "string") {
|
|
221
|
+
throw new OverrideRefusalError(path, `auth matcher must have pattern and kind`, harness);
|
|
222
|
+
}
|
|
223
|
+
validateMatcherPattern(obj.pattern, obj.flags as string | undefined, path, harness);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
149
228
|
/** Parse an override document and merge it over the code defaults. */
|
|
150
229
|
export const parseOverrides = (jsonText: string, path: string): DescriptorSet => {
|
|
151
230
|
let doc: unknown;
|
|
@@ -176,6 +255,24 @@ export const parseOverrides = (jsonText: string, path: string): DescriptorSet =>
|
|
|
176
255
|
if (next.store.template.includes("..")) {
|
|
177
256
|
refuse('store.template must not contain ".."');
|
|
178
257
|
}
|
|
258
|
+
// Validate matcher compilation at load time, not at first line
|
|
259
|
+
validateMatchers(next, path, name);
|
|
260
|
+
// Record matcher counts for the spawn boundary event when this descriptor was overridden
|
|
261
|
+
const defaultsForHarness = defaults[name as HarnessName] as HarnessDescriptor;
|
|
262
|
+
const limitChanged =
|
|
263
|
+
(next.limitMatchers as unknown as ReadonlyArray<unknown>).length !==
|
|
264
|
+
(defaultsForHarness.limitMatchers as unknown as ReadonlyArray<unknown>).length ||
|
|
265
|
+
JSON.stringify(next.limitMatchers) !== JSON.stringify(defaultsForHarness.limitMatchers);
|
|
266
|
+
const authChanged =
|
|
267
|
+
(next.authMatchers as unknown as ReadonlyArray<unknown>).length !==
|
|
268
|
+
(defaultsForHarness.authMatchers as unknown as ReadonlyArray<unknown>).length ||
|
|
269
|
+
JSON.stringify(next.authMatchers) !== JSON.stringify(defaultsForHarness.authMatchers);
|
|
270
|
+
if (limitChanged || authChanged) {
|
|
271
|
+
matcherOverridesOf.set(next, {
|
|
272
|
+
limit: (next.limitMatchers as unknown as ReadonlyArray<unknown>).length,
|
|
273
|
+
auth: (next.authMatchers as unknown as ReadonlyArray<unknown>).length,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
179
276
|
merged[name as HarnessName] = next;
|
|
180
277
|
}
|
|
181
278
|
return merged;
|
package/src/knowledge/pi.ts
CHANGED
|
@@ -62,7 +62,6 @@ export const piCli: HarnessDescriptor = deepFreeze({
|
|
|
62
62
|
models: ["zai/glm-5.2"],
|
|
63
63
|
aliases: {},
|
|
64
64
|
efforts: ["off", "minimal", "low", "medium", "high", "xhigh", "max"],
|
|
65
|
-
effortFlag: "--thinking",
|
|
66
65
|
// D-008: providers register models at runtime (~/.pi/models.json), so
|
|
67
66
|
// the vocabulary is open - validation accepts clean unknown selectors
|
|
68
67
|
// and capability claims degrade to unknown instead.
|
|
@@ -78,9 +77,7 @@ export const piCli: HarnessDescriptor = deepFreeze({
|
|
|
78
77
|
},
|
|
79
78
|
contextHook: null,
|
|
80
79
|
resumeLast: null,
|
|
81
|
-
provider: { flag: "--provider" },
|
|
82
80
|
stdin: "close-required",
|
|
83
|
-
discoveryDisableFlags: ["-nt", "-nc", "-ne", "-ns"],
|
|
84
81
|
presence: {
|
|
85
82
|
headlessMarkers: ["-p", "--print"],
|
|
86
83
|
},
|
|
@@ -94,4 +91,29 @@ export const piCli: HarnessDescriptor = deepFreeze({
|
|
|
94
91
|
},
|
|
95
92
|
session: false,
|
|
96
93
|
},
|
|
94
|
+
turnOptions: {
|
|
95
|
+
effort: { kind: "effort", render: { kind: "flag-value", flag: "--thinking" } },
|
|
96
|
+
provider: { kind: "selector", render: { kind: "flag-value", flag: "--provider" } },
|
|
97
|
+
discovery: {
|
|
98
|
+
kind: "discovery",
|
|
99
|
+
facets: {
|
|
100
|
+
tools: {
|
|
101
|
+
polarity: "disables",
|
|
102
|
+
render: { kind: "flag-list", flags: ["-nt"] },
|
|
103
|
+
},
|
|
104
|
+
instructionFiles: {
|
|
105
|
+
polarity: "disables",
|
|
106
|
+
render: { kind: "flag-list", flags: ["-nc"] },
|
|
107
|
+
},
|
|
108
|
+
extensions: {
|
|
109
|
+
polarity: "disables",
|
|
110
|
+
render: { kind: "flag-list", flags: ["-ne"] },
|
|
111
|
+
},
|
|
112
|
+
skills: {
|
|
113
|
+
polarity: "disables",
|
|
114
|
+
render: { kind: "flag-list", flags: ["-ns"] },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
97
119
|
});
|