@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
@@ -41,9 +41,50 @@ export const CANONICAL_TOOLS = deepFreeze([
41
41
  ] as const);
42
42
  export type CanonicalTool = (typeof CANONICAL_TOOLS)[number];
43
43
 
44
- export type StreamingGranularity = "token" | "message" | "none";
44
+ /** Every closed string vocabulary on a descriptor is declared once as a
45
+ * runtime array with its type derived (RFC-02 change 9), so a CLI list, an
46
+ * override validator, or a test reads the array instead of restating it. */
47
+ export const STREAMING_GRANULARITIES = deepFreeze(["token", "message", "none"] as const);
48
+ export type StreamingGranularity = (typeof STREAMING_GRANULARITIES)[number];
45
49
 
46
- export type HarnessMode = "headless-turn" | "headless-session" | "interactive";
50
+ export const HARNESS_MODES = deepFreeze([
51
+ "headless-turn",
52
+ "headless-session",
53
+ "interactive",
54
+ ] as const);
55
+ export type HarnessMode = (typeof HARNESS_MODES)[number];
56
+
57
+ export const STDIN_POLICIES = deepFreeze(["inherit", "close-required"] as const);
58
+ export type StdinPolicy = (typeof STDIN_POLICIES)[number];
59
+
60
+ export const CWD_SLUGS = deepFreeze(["dash-separators", "pi-dash-wrapped", "verbatim"] as const);
61
+ export type CwdSlug = (typeof CWD_SLUGS)[number];
62
+
63
+ export const RESUME_STYLES = deepFreeze(["flag", "positional"] as const);
64
+ export type ResumeStyle = (typeof RESUME_STYLES)[number];
65
+
66
+ export const RESUME_ON_MISSING = deepFreeze(["error", "create"] as const);
67
+ export type ResumeOnMissing = (typeof RESUME_ON_MISSING)[number];
68
+
69
+ export const IDENTITY_AUTHORITIES = deepFreeze(["caller-assigned", "harness-minted"] as const);
70
+ export type IdentityAuthority = (typeof IDENTITY_AUTHORITIES)[number];
71
+
72
+ export const DENY_SEMANTICS = deepFreeze(["remove-from-set", "policy-gate", "no-lists"] as const);
73
+ export type DenySemantics = (typeof DENY_SEMANTICS)[number];
74
+
75
+ export const SKILLS_OVERRIDES_VIA = deepFreeze([
76
+ "settings-skilloverrides",
77
+ "config-skills-array",
78
+ ] as const);
79
+ export type SkillsOverridesVia = (typeof SKILLS_OVERRIDES_VIA)[number];
80
+
81
+ export const VERSION_SOURCE_KINDS = deepFreeze(["npm", "installed"] as const);
82
+ export type VersionSourceKind = (typeof VERSION_SOURCE_KINDS)[number];
83
+
84
+ /** The access preset's value vocabulary: `read` narrows to the read-only
85
+ * tool subset, `write` is no restriction. */
86
+ export const ACCESS_VALUES = deepFreeze(["read", "write"] as const);
87
+ export type AccessValue = (typeof ACCESS_VALUES)[number];
47
88
 
48
89
  export const SESSION_INPUT_KINDS = ["claude-sdk-user-message", "pi-rpc-prompt"] as const;
49
90
  export type SessionInputKind = (typeof SESSION_INPUT_KINDS)[number];
@@ -107,6 +148,7 @@ export type UnavailableMatcher = PhraseMatcher;
107
148
  export const TURN_OPTION_KEYS = deepFreeze([
108
149
  "effort",
109
150
  "sandbox",
151
+ "contextWindow",
110
152
  "provider",
111
153
  "discovery",
112
154
  "write",
@@ -158,6 +200,12 @@ export interface SpecBase {
158
200
  readonly resumeRender?: OptionRender | null;
159
201
  }
160
202
 
203
+ /** How one access value renders on one harness (RFC-02 change 3): the
204
+ * marker for "render the read preset through the tool list", null for
205
+ * "emit nothing", or a phase-aware render carrying the harness value it
206
+ * maps to (codex: `read` is `--sandbox read-only`). */
207
+ export type AccessRender = "tool-preset" | null | (SpecBase & { readonly value?: string });
208
+
161
209
  export type TurnOptionSpec =
162
210
  /** Closed value vocabulary. `default` renders on LAUNCH ONLY. */
163
211
  | (SpecBase & {
@@ -165,17 +213,15 @@ export type TurnOptionSpec =
165
213
  readonly values: readonly string[];
166
214
  readonly default?: string;
167
215
  })
168
- /** Access preset: read/write dimension with per-harness rendering. */
169
- | (SpecBase & { readonly kind: "tool-preset" })
170
- | (SpecBase & {
171
- readonly kind: "flag-value";
172
- readonly flag: string;
173
- readonly values: Readonly<Record<string, string>>;
174
- })
175
- | (SpecBase & {
176
- readonly kind: "flag-list-by-value";
177
- readonly flags: Readonly<Record<string, readonly string[]>>;
178
- })
216
+ /** The access preset: one kind on every harness that expresses it, a
217
+ * render per value, and `claims` naming the turn option the preset
218
+ * displaces when set (codex: sandbox) so exclusivity is data, not a
219
+ * harness name. */
220
+ | {
221
+ readonly kind: "access";
222
+ readonly renders: Readonly<Record<AccessValue, AccessRender>>;
223
+ readonly claims?: TurnOptionKey;
224
+ }
179
225
  /** Ladder comes from vocabulary.efforts / effortsByModel, not from here. */
180
226
  | (SpecBase & { readonly kind: "effort" })
181
227
  /** Open selector, CLEAN_SELECTOR-validated. */
@@ -208,28 +254,37 @@ export const resolveRender = (spec: SpecBase, phase: "launch" | "resume"): Optio
208
254
  return spec.resumeRender ?? spec.render;
209
255
  };
210
256
 
211
- /** Alias for `resolveRender` with the resume-only null semantics made
212
- * explicit in the name; useful for tests asserting the "omitted => same as
213
- * render, null => unexpressible" contract. */
214
- export const resolveResumeRender = (spec: SpecBase): OptionRender | null =>
215
- resolveRender(spec, "resume");
216
-
217
- /** Like `resolveRender` but for a `TurnOptionSpec` that may be a `discovery`
218
- * table. Returns null for an unexpressible resume, the spec's render for
219
- * non-discovery specs, and for discovery returns the spec itself (facets are
220
- * resolved per-facet via `resolveRender`). */
221
- export const getOptionRender = (
222
- spec: TurnOptionSpec,
223
- phase: "launch" | "resume",
224
- ): OptionRender | null => {
225
- if (
226
- spec.kind === "discovery" ||
227
- spec.kind === "tool-preset" ||
228
- spec.kind === "flag-value" ||
229
- spec.kind === "flag-list-by-value"
230
- )
231
- return null;
232
- return resolveRender(spec as SpecBase, phase);
257
+ export type Quoting = "toml" | "verbatim";
258
+
259
+ /** The argv tokens one resolved render produces for one value - the only
260
+ * place a render kind becomes tokens (RFC-02 change 1). `toml` quoting is
261
+ * `JSON.stringify`, sufficient for the closed vocabularies that ride
262
+ * config-kv; `verbatim` passes prose (prompt text) and bare TOML literals
263
+ * through unchanged. A flag-value or config-kv render with no value is a
264
+ * descriptor error, never an empty token. */
265
+ export const tokensFor = (
266
+ render: OptionRender,
267
+ value?: string,
268
+ quoting: Quoting = "toml",
269
+ ): readonly string[] => {
270
+ switch (render.kind) {
271
+ case "flag-value":
272
+ if (value === undefined) {
273
+ throw new Error(`render ${render.flag} needs a value and none was given`);
274
+ }
275
+ return [...(render.extraFlags ?? []), render.flag, value];
276
+ case "config-kv":
277
+ if (value === undefined) {
278
+ throw new Error(`render ${render.flag} ${render.key} needs a value and none was given`);
279
+ }
280
+ return [render.flag, `${render.key}=${quoting === "toml" ? JSON.stringify(value) : value}`];
281
+ case "flag-list":
282
+ return [...render.flags];
283
+ default: {
284
+ const exhaustive: never = render;
285
+ return exhaustive;
286
+ }
287
+ }
233
288
  };
234
289
 
235
290
  export interface HarnessDescriptor {
@@ -251,8 +306,8 @@ export interface HarnessDescriptor {
251
306
  * have no registry to poll, so the check falls back to the locally
252
307
  * installed `<bin> --version` and is skipped where the CLI is absent. */
253
308
  readonly versionSource:
254
- | { readonly kind: "npm"; readonly package: string }
255
- | { readonly kind: "installed" };
309
+ | { readonly kind: Extract<VersionSourceKind, "npm">; readonly package: string }
310
+ | { readonly kind: Extract<VersionSourceKind, "installed"> };
256
311
  /** Headless one-turn launch shape. `promptStyle: "positional"` means the
257
312
  * prompt travels as a bare argv entry (ordering constraints apply).
258
313
  * `streamFlags` is the output flag set a headless turn launches with so
@@ -267,7 +322,6 @@ export interface HarnessDescriptor {
267
322
  * `workspace-write` into subcommands. */
268
323
  readonly subcommands: readonly string[];
269
324
  readonly promptStyle: "positional";
270
- readonly toolsFlag: string | null;
271
325
  readonly streamFlags: readonly string[];
272
326
  /** The flag that pins a caller-assigned id at LAUNCH (spawn-time
273
327
  * assignment; the execution layer consumes it), or null when the
@@ -281,7 +335,7 @@ export interface HarnessDescriptor {
281
335
  * anywhere else (the v1 first-UUID-wins scar: a UUID inside quoted prompt
282
336
  * text was returned as the session id, and resuming it started a stranger). */
283
337
  readonly resume: {
284
- readonly style: "flag" | "positional";
338
+ readonly style: ResumeStyle;
285
339
  readonly flag: string;
286
340
  readonly aliases: readonly string[];
287
341
  readonly idShape: RegExp;
@@ -299,7 +353,7 @@ export interface HarnessDescriptor {
299
353
  * The protocol layer must know this: a consumer resuming a session it
300
354
  * believes exists gets a blank session, not an error, on a "create"
301
355
  * harness. */
302
- readonly onMissing: "error" | "create";
356
+ readonly onMissing: ResumeOnMissing;
303
357
  };
304
358
  /** Persistent headless session support: the complete flag list that follows
305
359
  * the binary to open one lucid-owned process serving many turns, or null
@@ -327,7 +381,11 @@ export interface HarnessDescriptor {
327
381
  readonly resumeFlag: SessionResumeFlag;
328
382
  readonly input: SessionInputContract;
329
383
  readonly turnEnd: Readonly<Record<string, string>>;
330
- readonly identityProbe: { readonly command: string } | null;
384
+ /** The command the runner writes at spawn to learn the session id, and
385
+ * the dot-path to that id in the response (pi rpc: `get_state`,
386
+ * `data.sessionId`). Encoded and decoded in interpretation
387
+ * (session-input.ts); execution holds no field names (ADR 0005). */
388
+ readonly identityProbe: { readonly command: string; readonly responseIdField: string } | null;
331
389
  } | null;
332
390
  /** Streaming is a property of the INVOCATION, not the harness: each pin
333
391
  * names the flag set that unlocks a granularity, checked in order, first
@@ -348,7 +406,7 @@ export interface HarnessDescriptor {
348
406
  * {type: "system", subtype: "init"}, re-emitted at every turn start with
349
407
  * the same value (A-001) - consumers dedupe via decodeIdentity. */
350
408
  readonly identity: {
351
- readonly authority: "caller-assigned" | "harness-minted";
409
+ readonly authority: IdentityAuthority;
352
410
  /** `idField` is a dot-path (muse nests its id at `stream.id`); an empty
353
411
  * `match` means "any record carrying the id path". */
354
412
  readonly announce: {
@@ -387,7 +445,7 @@ export interface HarnessDescriptor {
387
445
  readonly template: string;
388
446
  /** claude: '/', '.' -> '-'; pi: '/' -> '-' wrapped in leading/trailing
389
447
  * dashes, dots preserved. */
390
- readonly cwdSlug: "dash-separators" | "pi-dash-wrapped" | "verbatim";
448
+ readonly cwdSlug: CwdSlug;
391
449
  };
392
450
  /** How the harness exposes context-window usage; the interpretation layer
393
451
  * surfaces it as a `context` HarnessEvent. */
@@ -400,7 +458,7 @@ export interface HarnessDescriptor {
400
458
  readonly resumeLast: { readonly flag: string } | null;
401
459
  /** Whether backgrounded headless calls must have stdin closed (pi hangs
402
460
  * without `< /dev/null`). */
403
- readonly stdin: "inherit" | "close-required";
461
+ readonly stdin: StdinPolicy;
404
462
  /** Presence recognition: how an interactive process for a session id shows
405
463
  * up in a process listing. `headlessMarkers` mark a process as headless
406
464
  * (not interactive presence). Known blind spot, inherent to argv matching:
@@ -445,8 +503,7 @@ export interface HarnessDescriptor {
445
503
  * the claude asymmetry: claude's include flag pre-approves without
446
504
  * restricting the visible set, so an exact allowlist must render as a
447
505
  * disallow-complement there; pi's include IS strict (over built-ins).
448
- * `composable`: both flags legal at once (pi: exclude subtracts from
449
- * include). `builtins`: curated names + default-enabled state - grep/find/
506
+ * `builtins`: curated names + default-enabled state - grep/find/
450
507
  * ls ship off on pi, everything ships on elsewhere. `categories`:
451
508
  * non-list switches (muse disable flags, codex config booleans).
452
509
  * `denySemantics`: whether a deny removes the tool from the model-visible
@@ -461,13 +518,12 @@ export interface HarnessDescriptor {
461
518
  * muse: null (structural gap - trust/config scoped only). */
462
519
  readonly skills: {
463
520
  readonly loadFlag: string | null;
464
- readonly overridesVia: "settings-skilloverrides" | "config-skills-array" | null;
521
+ readonly overridesVia: SkillsOverridesVia | null;
465
522
  } | null;
466
523
  readonly tools: {
467
524
  readonly includeFlag: string | null;
468
525
  readonly excludeFlag: string | null;
469
526
  readonly includeIsStrictAllowlist: boolean;
470
- readonly composable: boolean;
471
527
  readonly builtins: ReadonlyArray<{
472
528
  readonly name: string;
473
529
  readonly defaultEnabled: boolean;
@@ -479,6 +535,6 @@ export interface HarnessDescriptor {
479
535
  readonly configKey: string | null;
480
536
  readonly canonical: readonly CanonicalTool[];
481
537
  }>;
482
- readonly denySemantics: "remove-from-set" | "policy-gate" | "no-lists";
538
+ readonly denySemantics: DenySemantics;
483
539
  };
484
540
  }
@@ -11,6 +11,41 @@ import type {
11
11
  UnavailableMatcher,
12
12
  } from "./descriptor.js";
13
13
 
14
+ /** Bounds on a matcher, enforced wherever one is compiled - the limits
15
+ * scanner and the override loader alike (RFC-02 change 9). They keep a
16
+ * crafted override file from DoS'ing the compiler or the scanner; the
17
+ * input window, not pattern analysis, is the backtracking bound. */
18
+ export const MAX_PATTERN_LENGTH = 200;
19
+ export const MAX_MATCHERS_PER_KIND = 64;
20
+ /** The only flags a matcher may carry. `g` and `y` are stateful and
21
+ * would make `test()` order-dependent. */
22
+ export const MATCHER_FLAGS = "imsu";
23
+
24
+ /** Compile one matcher under the bounds. Throws a plain Error naming the
25
+ * violated bound; loaders wrap it in their own refusal type. */
26
+ export const compileMatcher = (pattern: string, flags: string | undefined): RegExp => {
27
+ const f = flags ?? "i";
28
+ if (pattern.length > MAX_PATTERN_LENGTH) {
29
+ throw new Error(`pattern over ${MAX_PATTERN_LENGTH} characters`);
30
+ }
31
+ if (pattern.length === 0) {
32
+ throw new Error("pattern must not be empty");
33
+ }
34
+ if (f.includes("g") || f.includes("y")) {
35
+ throw new Error(`flags must not contain g or y (got ${JSON.stringify(f)})`);
36
+ }
37
+ for (const ch of f) {
38
+ if (!MATCHER_FLAGS.includes(ch)) {
39
+ throw new Error(`flag ${JSON.stringify(ch)} outside ${MATCHER_FLAGS}`);
40
+ }
41
+ }
42
+ try {
43
+ return new RegExp(pattern, f);
44
+ } catch (e) {
45
+ throw new Error(`uncompilable pattern ${JSON.stringify(pattern)}: ${(e as Error).message}`);
46
+ }
47
+ };
48
+
14
49
  export const SHARED_LIMIT_MATCHERS: ReadonlyArray<LimitMatcher> = [
15
50
  { pattern: "you'?ve hit your usage limit", flags: "i", code: "usage-limit" },
16
51
  { pattern: "usage limit (?:reached|exceeded)", flags: "i", code: "usage-limit" },
@@ -24,7 +24,6 @@ export const museCode: HarnessDescriptor = deepFreeze({
24
24
  baseFlags: ["exec", "--json"],
25
25
  subcommands: ["exec"],
26
26
  promptStyle: "positional",
27
- toolsFlag: null,
28
27
  streamFlags: [],
29
28
  idFlag: "--session-id",
30
29
  },
@@ -116,10 +115,14 @@ export const museCode: HarnessDescriptor = deepFreeze({
116
115
  max: 10000,
117
116
  render: { kind: "flag-value", flag: "--max-model-steps" },
118
117
  },
118
+ // read gates the write and shell categories; write is the harness
119
+ // default and emits nothing.
119
120
  access: {
120
- kind: "flag-list-by-value",
121
- flags: { read: ["--disable-write", "--disable-shell"], write: [] },
122
- render: { kind: "flag-list", flags: ["--disable-write", "--disable-shell"] },
121
+ kind: "access",
122
+ renders: {
123
+ read: { render: { kind: "flag-list", flags: ["--disable-write", "--disable-shell"] } },
124
+ write: null,
125
+ },
123
126
  },
124
127
  },
125
128
  // Phase 0 fixtures: muse-category-flags.md. No name lists; category
@@ -133,7 +136,6 @@ export const museCode: HarnessDescriptor = deepFreeze({
133
136
  includeFlag: null,
134
137
  excludeFlag: null,
135
138
  includeIsStrictAllowlist: false,
136
- composable: false,
137
139
  builtins: [],
138
140
  categories: [
139
141
  {
@@ -20,6 +20,7 @@ import {
20
20
  type HarnessName,
21
21
  SESSION_INPUT_KINDS,
22
22
  } from "./descriptor.js";
23
+ import { compileMatcher, MAX_MATCHERS_PER_KIND } from "./matchers.js";
23
24
  import { museCode } from "./muse.js";
24
25
  import { piCli } from "./pi.js";
25
26
 
@@ -159,36 +160,12 @@ const validateMatcherPattern = (
159
160
  path: string,
160
161
  harness: string,
161
162
  ): void => {
162
- const f = flags ?? "i";
163
- if (pattern.length > 200) {
164
- throw new OverrideRefusalError(
165
- path,
166
- `pattern over 200 characters: ${JSON.stringify(pattern.slice(0, 40))}`,
167
- harness,
168
- );
169
- }
170
- if (pattern.length === 0) {
171
- throw new OverrideRefusalError(path, `pattern must not be empty`, harness);
172
- }
173
- if (f.includes("g") || f.includes("y")) {
174
- throw new OverrideRefusalError(
175
- path,
176
- `flags must not contain g or y (got ${JSON.stringify(f)})`,
177
- harness,
178
- );
179
- }
180
- for (const ch of f) {
181
- if (!"imsu".includes(ch))
182
- throw new OverrideRefusalError(path, `flag ${JSON.stringify(ch)} outside imsu`, harness);
183
- }
163
+ // One compiler and one set of bounds, shared with the limits scanner
164
+ // (RFC-02 change 9); this loader only wraps the refusal.
184
165
  try {
185
- new RegExp(pattern, f);
166
+ compileMatcher(pattern, flags);
186
167
  } catch (e) {
187
- throw new OverrideRefusalError(
188
- path,
189
- `uncompilable pattern ${JSON.stringify(pattern)}: ${(e as Error).message}`,
190
- harness,
191
- );
168
+ throw new OverrideRefusalError(path, (e as Error).message, harness);
192
169
  }
193
170
  };
194
171
 
@@ -196,8 +173,12 @@ const validateMatchers = (desc: HarnessDescriptor, path: string, harness: string
196
173
  const limitMatchers = (desc as unknown as { limitMatchers: readonly unknown[] }).limitMatchers;
197
174
  const authMatchers = (desc as unknown as { authMatchers: readonly unknown[] }).authMatchers;
198
175
  if (Array.isArray(limitMatchers)) {
199
- if (limitMatchers.length > 64) {
200
- throw new OverrideRefusalError(path, `more than 64 limit matchers`, harness);
176
+ if (limitMatchers.length > MAX_MATCHERS_PER_KIND) {
177
+ throw new OverrideRefusalError(
178
+ path,
179
+ `more than ${MAX_MATCHERS_PER_KIND} limit matchers`,
180
+ harness,
181
+ );
201
182
  }
202
183
  for (const m of limitMatchers) {
203
184
  if (!isPlain(m as unknown as Record<string, unknown>)) {
@@ -211,8 +192,12 @@ const validateMatchers = (desc: HarnessDescriptor, path: string, harness: string
211
192
  }
212
193
  }
213
194
  if (Array.isArray(authMatchers)) {
214
- if (authMatchers.length > 64) {
215
- throw new OverrideRefusalError(path, `more than 64 auth matchers`, harness);
195
+ if (authMatchers.length > MAX_MATCHERS_PER_KIND) {
196
+ throw new OverrideRefusalError(
197
+ path,
198
+ `more than ${MAX_MATCHERS_PER_KIND} auth matchers`,
199
+ harness,
200
+ );
216
201
  }
217
202
  for (const m of authMatchers) {
218
203
  if (!isPlain(m as unknown as Record<string, unknown>)) {
@@ -22,7 +22,6 @@ export const piCli: HarnessDescriptor = deepFreeze({
22
22
  baseFlags: ["-p", "--mode", "json"],
23
23
  subcommands: [],
24
24
  promptStyle: "positional",
25
- toolsFlag: null,
26
25
  streamFlags: [],
27
26
  idFlag: "--session-id",
28
27
  },
@@ -49,8 +48,10 @@ export const piCli: HarnessDescriptor = deepFreeze({
49
48
  // --mode rpc exists on 0.84.2 and its session semantics are now VERIFIED
50
49
  // against a live run (2026-08-19 spike, evidence at
51
50
  // test/fixtures/pi-rpc-spike): JSONL both directions, agent_settled
52
- // delimits turns, steer/follow_up queue mid-run (hcn never needs them -
53
- // it queues sends itself), identity is silent at startup and readable
51
+ // delimits turns, steer/follow_up queue mid-run (hcn keeps no queue of
52
+ // its own since ADR 0007 and writes a send when it arrives; a bare
53
+ // prompt mid-run is refused with success:false - spike fixture 05, a
54
+ // pending change), identity is silent at startup and readable
54
55
  // only via a get_state round trip, stdin EOF exits rc=0. The claude
55
56
  // slice remains the proven vertical (D-003); this entry is the second.
56
57
  sessionMode: {
@@ -73,7 +74,7 @@ export const piCli: HarnessDescriptor = deepFreeze({
73
74
  resumeFlag: "--session-id",
74
75
  input: { kind: "pi-rpc-prompt" },
75
76
  turnEnd: { type: "agent_settled" },
76
- identityProbe: { command: "get_state" },
77
+ identityProbe: { command: "get_state", responseIdField: "data.sessionId" },
77
78
  },
78
79
  output: {
79
80
  // pi -p prints plain text; --mode json emits structured v3 records
@@ -173,7 +174,9 @@ export const piCli: HarnessDescriptor = deepFreeze({
173
174
  },
174
175
  },
175
176
  },
176
- access: { kind: "tool-preset", render: { kind: "flag-value", flag: "--tools" } },
177
+ // read renders the read preset through --tools (strict allowlist);
178
+ // write is the harness default and emits nothing.
179
+ access: { kind: "access", renders: { read: "tool-preset", write: null } },
177
180
  },
178
181
  // Phase 0 fixtures: pi-both-tool-flags.md. Both list flags legal at once;
179
182
  // exclude subtracts from include. --tools is strict over BUILT-INS but
@@ -184,7 +187,6 @@ export const piCli: HarnessDescriptor = deepFreeze({
184
187
  includeFlag: "--tools",
185
188
  excludeFlag: "--exclude-tools",
186
189
  includeIsStrictAllowlist: true,
187
- composable: true,
188
190
  builtins: [
189
191
  { name: "read", defaultEnabled: true, canonical: "read" },
190
192
  { name: "bash", defaultEnabled: true, canonical: "shell" },
@@ -19,6 +19,7 @@ export const DEFAULT_TURN_PROFILE = deepFreeze({
19
19
  // profile tier. On the other three the dimension is unrenderable and
20
20
  // reports as divergence.
21
21
  sandbox: "workspace-write",
22
+ contextWindow: 272000,
22
23
  // Ratified: discovery fully ON. The harnesses' bare runs already
23
24
  // discover instruction files, skills, and extensions; the profile makes
24
25
  // that a stated contract. The off-spellings exist per harness
@@ -1,11 +0,0 @@
1
- /**
2
- * Small dimension accessors: one interpretation function per PLAN 3.1
3
- * dimension whose value is a direct descriptor lookup. They exist so every
4
- * dimension has a single named owner in the interpretation layer - callers
5
- * never reach into descriptor internals.
6
- */
7
- import type { HarnessDescriptor } from "../knowledge/descriptor.js";
8
- export declare const stdinPolicyOf: (h: HarnessDescriptor) => "inherit" | "close-required";
9
- export declare const toolsFlagOf: (h: HarnessDescriptor) => string | null;
10
- export declare const autonomyFlagOf: (h: HarnessDescriptor) => string | null;
11
- //# sourceMappingURL=dimensions.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dimensions.d.ts","sourceRoot":"","sources":["../../src/interpretation/dimensions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEpE,eAAO,MAAM,aAAa,MAAO,iBAAiB,KAAG,SAAS,GAAG,gBAA2B,CAAC;AAE7F,eAAO,MAAM,WAAW,MAAO,iBAAiB,KAAG,MAAM,GAAG,IAA0B,CAAC;AAEvF,eAAO,MAAM,cAAc,MAAO,iBAAiB,KAAG,MAAM,GAAG,IACjB,CAAC"}
@@ -1,4 +0,0 @@
1
- export const stdinPolicyOf = (h) => h.stdin;
2
- export const toolsFlagOf = (h) => h.launch.toolsFlag;
3
- export const autonomyFlagOf = (h) => h.autonomy === null ? null : h.autonomy.flag;
4
- //# sourceMappingURL=dimensions.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dimensions.js","sourceRoot":"","sources":["../../src/interpretation/dimensions.ts"],"names":[],"mappings":"AAQA,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAoB,EAAgC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;AAE7F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAoB,EAAiB,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC;AAEvF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAoB,EAAiB,EAAE,CACpE,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC"}
@@ -1,14 +0,0 @@
1
- /**
2
- * Small dimension accessors: one interpretation function per PLAN 3.1
3
- * dimension whose value is a direct descriptor lookup. They exist so every
4
- * dimension has a single named owner in the interpretation layer - callers
5
- * never reach into descriptor internals.
6
- */
7
- import type { HarnessDescriptor } from "../knowledge/descriptor.js";
8
-
9
- export const stdinPolicyOf = (h: HarnessDescriptor): "inherit" | "close-required" => h.stdin;
10
-
11
- export const toolsFlagOf = (h: HarnessDescriptor): string | null => h.launch.toolsFlag;
12
-
13
- export const autonomyFlagOf = (h: HarnessDescriptor): string | null =>
14
- h.autonomy === null ? null : h.autonomy.flag;