@dungle-scrubs/harness-cli-normalizer 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/README.md +33 -6
  2. package/dist/cli/args.d.ts +9 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +36 -26
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/check.d.ts +14 -0
  7. package/dist/cli/check.d.ts.map +1 -1
  8. package/dist/cli/check.js +10 -3
  9. package/dist/cli/check.js.map +1 -1
  10. package/dist/cli/config.d.ts +4 -4
  11. package/dist/cli/config.d.ts.map +1 -1
  12. package/dist/cli/config.js +8 -6
  13. package/dist/cli/config.js.map +1 -1
  14. package/dist/cli/exit-codes.d.ts +0 -1
  15. package/dist/cli/exit-codes.d.ts.map +1 -1
  16. package/dist/cli/exit-codes.js +0 -1
  17. package/dist/cli/exit-codes.js.map +1 -1
  18. package/dist/cli/help.d.ts +2 -2
  19. package/dist/cli/help.d.ts.map +1 -1
  20. package/dist/cli/help.js +10 -2
  21. package/dist/cli/help.js.map +1 -1
  22. package/dist/cli/index.d.ts.map +1 -1
  23. package/dist/cli/index.js +3 -1
  24. package/dist/cli/index.js.map +1 -1
  25. package/dist/cli/inspect.d.ts.map +1 -1
  26. package/dist/cli/inspect.js +84 -265
  27. package/dist/cli/inspect.js.map +1 -1
  28. package/dist/cli/plan-turn.d.ts +68 -0
  29. package/dist/cli/plan-turn.d.ts.map +1 -0
  30. package/dist/cli/plan-turn.js +268 -0
  31. package/dist/cli/plan-turn.js.map +1 -0
  32. package/dist/cli/refuse.d.ts.map +1 -1
  33. package/dist/cli/refuse.js +2 -1
  34. package/dist/cli/refuse.js.map +1 -1
  35. package/dist/cli/resolve-harness.d.ts.map +1 -1
  36. package/dist/cli/resolve-harness.js +2 -3
  37. package/dist/cli/resolve-harness.js.map +1 -1
  38. package/dist/cli/run.d.ts.map +1 -1
  39. package/dist/cli/run.js +33 -367
  40. package/dist/cli/run.js.map +1 -1
  41. package/dist/cli/session-json.d.ts +1 -1
  42. package/dist/cli/session-json.d.ts.map +1 -1
  43. package/dist/cli/session-json.js +1 -3
  44. package/dist/cli/session-json.js.map +1 -1
  45. package/dist/cli/session.d.ts.map +1 -1
  46. package/dist/cli/session.js +35 -43
  47. package/dist/cli/session.js.map +1 -1
  48. package/dist/cli/skills-root.d.ts +1 -1
  49. package/dist/cli/skills-root.d.ts.map +1 -1
  50. package/dist/cli/skills-root.js +1 -1
  51. package/dist/cli/skills-root.js.map +1 -1
  52. package/dist/execution/decode.d.ts.map +1 -1
  53. package/dist/execution/decode.js +11 -36
  54. package/dist/execution/decode.js.map +1 -1
  55. package/dist/execution/events.d.ts +4 -1
  56. package/dist/execution/events.d.ts.map +1 -1
  57. package/dist/execution/events.js.map +1 -1
  58. package/dist/execution/failure.d.ts +3 -1
  59. package/dist/execution/failure.d.ts.map +1 -1
  60. package/dist/execution/failure.js +14 -11
  61. package/dist/execution/failure.js.map +1 -1
  62. package/dist/execution/open-session.d.ts +1 -1
  63. package/dist/execution/open-session.d.ts.map +1 -1
  64. package/dist/execution/open-session.js +150 -223
  65. package/dist/execution/open-session.js.map +1 -1
  66. package/dist/execution/stream-turn.d.ts +9 -14
  67. package/dist/execution/stream-turn.d.ts.map +1 -1
  68. package/dist/execution/stream-turn.js +85 -188
  69. package/dist/execution/stream-turn.js.map +1 -1
  70. package/dist/execution/supervisor.d.ts +71 -0
  71. package/dist/execution/supervisor.d.ts.map +1 -0
  72. package/dist/execution/supervisor.js +127 -0
  73. package/dist/execution/supervisor.js.map +1 -0
  74. package/dist/interpretation/argv.d.ts +39 -12
  75. package/dist/interpretation/argv.d.ts.map +1 -1
  76. package/dist/interpretation/argv.js +28 -20
  77. package/dist/interpretation/argv.js.map +1 -1
  78. package/dist/interpretation/content.d.ts +10 -1
  79. package/dist/interpretation/content.d.ts.map +1 -1
  80. package/dist/interpretation/content.js +20 -11
  81. package/dist/interpretation/content.js.map +1 -1
  82. package/dist/interpretation/hints.d.ts +1 -1
  83. package/dist/interpretation/hints.d.ts.map +1 -1
  84. package/dist/interpretation/hints.js +10 -11
  85. package/dist/interpretation/hints.js.map +1 -1
  86. package/dist/interpretation/index.d.ts +0 -1
  87. package/dist/interpretation/index.d.ts.map +1 -1
  88. package/dist/interpretation/index.js +0 -1
  89. package/dist/interpretation/index.js.map +1 -1
  90. package/dist/interpretation/isolation.d.ts +10 -0
  91. package/dist/interpretation/isolation.d.ts.map +1 -0
  92. package/dist/interpretation/isolation.js +30 -0
  93. package/dist/interpretation/isolation.js.map +1 -0
  94. package/dist/interpretation/limits.d.ts +2 -5
  95. package/dist/interpretation/limits.d.ts.map +1 -1
  96. package/dist/interpretation/limits.js +8 -62
  97. package/dist/interpretation/limits.js.map +1 -1
  98. package/dist/interpretation/question.d.ts +27 -0
  99. package/dist/interpretation/question.d.ts.map +1 -1
  100. package/dist/interpretation/question.js +28 -0
  101. package/dist/interpretation/question.js.map +1 -1
  102. package/dist/interpretation/refusal.d.ts +10 -5
  103. package/dist/interpretation/refusal.d.ts.map +1 -1
  104. package/dist/interpretation/refusal.js +24 -15
  105. package/dist/interpretation/refusal.js.map +1 -1
  106. package/dist/interpretation/resolve-options.d.ts +36 -2
  107. package/dist/interpretation/resolve-options.d.ts.map +1 -1
  108. package/dist/interpretation/resolve-options.js +53 -22
  109. package/dist/interpretation/resolve-options.js.map +1 -1
  110. package/dist/interpretation/session-input.d.ts +44 -1
  111. package/dist/interpretation/session-input.d.ts.map +1 -1
  112. package/dist/interpretation/session-input.js +59 -4
  113. package/dist/interpretation/session-input.js.map +1 -1
  114. package/dist/interpretation/shape.d.ts +1 -0
  115. package/dist/interpretation/shape.d.ts.map +1 -1
  116. package/dist/interpretation/shape.js +10 -0
  117. package/dist/interpretation/shape.js.map +1 -1
  118. package/dist/interpretation/skills-selection.d.ts +25 -33
  119. package/dist/interpretation/skills-selection.d.ts.map +1 -1
  120. package/dist/interpretation/skills-selection.js +41 -48
  121. package/dist/interpretation/skills-selection.js.map +1 -1
  122. package/dist/interpretation/support.d.ts.map +1 -1
  123. package/dist/interpretation/support.js +25 -8
  124. package/dist/interpretation/support.js.map +1 -1
  125. package/dist/interpretation/tool-selection.d.ts +7 -6
  126. package/dist/interpretation/tool-selection.d.ts.map +1 -1
  127. package/dist/interpretation/tool-selection.js +54 -116
  128. package/dist/interpretation/tool-selection.js.map +1 -1
  129. package/dist/interpretation/tool-vocabulary.d.ts +31 -12
  130. package/dist/interpretation/tool-vocabulary.d.ts.map +1 -1
  131. package/dist/interpretation/tool-vocabulary.js +19 -41
  132. package/dist/interpretation/tool-vocabulary.js.map +1 -1
  133. package/dist/interpretation/turn-options.d.ts +4 -0
  134. package/dist/interpretation/turn-options.d.ts.map +1 -1
  135. package/dist/interpretation/turn-options.js +107 -241
  136. package/dist/interpretation/turn-options.js.map +1 -1
  137. package/dist/interpretation/vocabulary.d.ts +8 -6
  138. package/dist/interpretation/vocabulary.d.ts.map +1 -1
  139. package/dist/interpretation/vocabulary.js +22 -9
  140. package/dist/interpretation/vocabulary.js.map +1 -1
  141. package/dist/knowledge/claude-code.d.ts +3 -1
  142. package/dist/knowledge/claude-code.d.ts.map +1 -1
  143. package/dist/knowledge/claude-code.js +17 -4
  144. package/dist/knowledge/claude-code.js.map +1 -1
  145. package/dist/knowledge/codex.d.ts.map +1 -1
  146. package/dist/knowledge/codex.js +31 -7
  147. package/dist/knowledge/codex.js.map +1 -1
  148. package/dist/knowledge/descriptor.d.ts +67 -36
  149. package/dist/knowledge/descriptor.d.ts.map +1 -1
  150. package/dist/knowledge/descriptor.js +50 -15
  151. package/dist/knowledge/descriptor.js.map +1 -1
  152. package/dist/knowledge/matchers.d.ts +12 -0
  153. package/dist/knowledge/matchers.d.ts.map +1 -1
  154. package/dist/knowledge/matchers.js +34 -0
  155. package/dist/knowledge/matchers.js.map +1 -1
  156. package/dist/knowledge/muse.d.ts.map +1 -1
  157. package/dist/knowledge/muse.js +13 -6
  158. package/dist/knowledge/muse.js.map +1 -1
  159. package/dist/knowledge/overrides.d.ts.map +1 -1
  160. package/dist/knowledge/overrides.js +9 -20
  161. package/dist/knowledge/overrides.js.map +1 -1
  162. package/dist/knowledge/pi.d.ts.map +1 -1
  163. package/dist/knowledge/pi.js +8 -6
  164. package/dist/knowledge/pi.js.map +1 -1
  165. package/dist/knowledge/profile.d.ts +1 -0
  166. package/dist/knowledge/profile.d.ts.map +1 -1
  167. package/dist/knowledge/profile.js +1 -0
  168. package/dist/knowledge/profile.js.map +1 -1
  169. package/package.json +1 -1
  170. package/src/cli/args.ts +39 -27
  171. package/src/cli/check.ts +10 -3
  172. package/src/cli/config.ts +14 -11
  173. package/src/cli/exit-codes.ts +0 -2
  174. package/src/cli/help.ts +10 -2
  175. package/src/cli/index.ts +3 -1
  176. package/src/cli/inspect.ts +88 -269
  177. package/src/cli/plan-turn.ts +350 -0
  178. package/src/cli/refuse.ts +2 -1
  179. package/src/cli/resolve-harness.ts +4 -5
  180. package/src/cli/run.ts +36 -399
  181. package/src/cli/session-json.ts +1 -6
  182. package/src/cli/session.ts +51 -44
  183. package/src/cli/skills-root.ts +1 -4
  184. package/src/execution/decode.ts +10 -36
  185. package/src/execution/events.ts +4 -1
  186. package/src/execution/failure.ts +18 -11
  187. package/src/execution/open-session.ts +154 -228
  188. package/src/execution/stream-turn.ts +90 -193
  189. package/src/execution/supervisor.ts +199 -0
  190. package/src/interpretation/argv.ts +65 -34
  191. package/src/interpretation/content.ts +30 -13
  192. package/src/interpretation/hints.ts +2 -12
  193. package/src/interpretation/index.ts +0 -1
  194. package/src/interpretation/isolation.ts +39 -0
  195. package/src/interpretation/limits.ts +14 -75
  196. package/src/interpretation/question.ts +46 -0
  197. package/src/interpretation/refusal.ts +28 -18
  198. package/src/interpretation/resolve-options.ts +98 -33
  199. package/src/interpretation/session-input.ts +88 -5
  200. package/src/interpretation/shape.ts +10 -0
  201. package/src/interpretation/skills-selection.ts +70 -74
  202. package/src/interpretation/support.ts +21 -6
  203. package/src/interpretation/tool-selection.ts +67 -122
  204. package/src/interpretation/tool-vocabulary.ts +48 -53
  205. package/src/interpretation/turn-options.ts +125 -220
  206. package/src/interpretation/vocabulary.ts +15 -10
  207. package/src/knowledge/claude-code.ts +17 -4
  208. package/src/knowledge/codex.ts +31 -7
  209. package/src/knowledge/descriptor.ts +106 -49
  210. package/src/knowledge/matchers.ts +35 -0
  211. package/src/knowledge/muse.ts +13 -6
  212. package/src/knowledge/overrides.ts +17 -32
  213. package/src/knowledge/pi.ts +8 -6
  214. package/src/knowledge/profile.ts +1 -0
  215. package/dist/interpretation/dimensions.d.ts +0 -11
  216. package/dist/interpretation/dimensions.d.ts.map +0 -1
  217. package/dist/interpretation/dimensions.js +0 -4
  218. package/dist/interpretation/dimensions.js.map +0 -1
  219. package/src/interpretation/dimensions.ts +0 -14
@@ -23,7 +23,6 @@ export const codexCli: HarnessDescriptor = deepFreeze({
23
23
  baseFlags: ["exec", "--json", "--skip-git-repo-check"],
24
24
  subcommands: ["exec"],
25
25
  promptStyle: "positional",
26
- toolsFlag: null,
27
26
  streamFlags: [],
28
27
  // Codex mints its own thread id; there is nothing to assign at launch.
29
28
  idFlag: null,
@@ -62,11 +61,13 @@ export const codexCli: HarnessDescriptor = deepFreeze({
62
61
  autonomy: { flag: "--yolo" },
63
62
  vocabulary: {
64
63
  modelFlag: "--model",
65
- models: ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"],
64
+ models: ["gpt-6-astra", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"],
66
65
  aliases: {},
67
66
  efforts: ["minimal", "low", "medium", "high", "xhigh", "max"],
68
67
  // Codex constrains ladders per model generation (v1 registry).
69
68
  effortsByModel: {
69
+ // https://developers.openai.com/api/docs/models/gpt-6-astra (2026-09-06).
70
+ "gpt-6-astra": ["low", "medium", "high", "xhigh", "max"],
70
71
  "gpt-5.5": ["minimal", "low", "medium", "high"],
71
72
  "gpt-5.6-sol": ["medium", "high", "xhigh", "max"],
72
73
  "gpt-5.6-terra": ["medium", "high", "xhigh", "max"],
@@ -109,6 +110,14 @@ export const codexCli: HarnessDescriptor = deepFreeze({
109
110
  observedOn: { harness: "codex", model: "", version: "0.146.1", date: "2026-08-19" },
110
111
  },
111
112
  turnOptions: {
113
+ // Codex config reference; accepted as an integer on CLI 0.153.4.
114
+ // Native compaction uses this window; hcn does not count request tokens.
115
+ contextWindow: {
116
+ kind: "integer",
117
+ min: 1,
118
+ max: 272000,
119
+ render: { kind: "config-kv", flag: "-c", key: "model_context_window" },
120
+ },
112
121
  effort: {
113
122
  kind: "effort",
114
123
  render: { kind: "config-kv", flag: "-c", key: "model_reasoning_effort" },
@@ -138,11 +147,27 @@ export const codexCli: HarnessDescriptor = deepFreeze({
138
147
  // caller asked.
139
148
  resumeRender: { kind: "config-kv", flag: "-c", key: "sandbox_mode" },
140
149
  },
150
+ // The preset maps onto the sandbox dimension and therefore CLAIMS it:
151
+ // an explicit --sandbox alongside --access refuses, and the profile's
152
+ // sandbox default yields. Data here, not a harness-name branch.
153
+ // On resume the preset rides the same config-kv spelling the sandbox
154
+ // dimension uses (issue #72 evidence above): `codex exec resume`
155
+ // rejects --sandbox, and -c sandbox_mode is enforced there.
141
156
  access: {
142
- kind: "flag-value",
143
- flag: "--sandbox",
144
- values: { read: "read-only", write: "workspace-write" },
145
- render: { kind: "flag-value", flag: "--sandbox" },
157
+ kind: "access",
158
+ claims: "sandbox",
159
+ renders: {
160
+ read: {
161
+ render: { kind: "flag-value", flag: "--sandbox" },
162
+ resumeRender: { kind: "config-kv", flag: "-c", key: "sandbox_mode" },
163
+ value: "read-only",
164
+ },
165
+ write: {
166
+ render: { kind: "flag-value", flag: "--sandbox" },
167
+ resumeRender: { kind: "config-kv", flag: "-c", key: "sandbox_mode" },
168
+ value: "workspace-write",
169
+ },
170
+ },
146
171
  },
147
172
  },
148
173
  // Tools: no built-in name lists; control is feature booleans
@@ -158,7 +183,6 @@ export const codexCli: HarnessDescriptor = deepFreeze({
158
183
  includeFlag: null,
159
184
  excludeFlag: null,
160
185
  includeIsStrictAllowlist: false,
161
- composable: false,
162
186
  builtins: [],
163
187
  categories: [
164
188
  { key: "shell", disableFlag: null, configKey: "features.shell_tool", canonical: [] },
@@ -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];
@@ -105,8 +146,10 @@ export type UnavailableMatcher = PhraseMatcher;
105
146
  * arm would be dead data that can only drift. Render order is the tuple
106
147
  * order, so argv is deterministic regardless of caller field order. */
107
148
  export const TURN_OPTION_KEYS = deepFreeze([
149
+ "isolation",
108
150
  "effort",
109
151
  "sandbox",
152
+ "contextWindow",
110
153
  "provider",
111
154
  "discovery",
112
155
  "write",
@@ -158,6 +201,12 @@ export interface SpecBase {
158
201
  readonly resumeRender?: OptionRender | null;
159
202
  }
160
203
 
204
+ /** How one access value renders on one harness (RFC-02 change 3): the
205
+ * marker for "render the read preset through the tool list", null for
206
+ * "emit nothing", or a phase-aware render carrying the harness value it
207
+ * maps to (codex: `read` is `--sandbox read-only`). */
208
+ export type AccessRender = "tool-preset" | null | (SpecBase & { readonly value?: string });
209
+
161
210
  export type TurnOptionSpec =
162
211
  /** Closed value vocabulary. `default` renders on LAUNCH ONLY. */
163
212
  | (SpecBase & {
@@ -165,17 +214,15 @@ export type TurnOptionSpec =
165
214
  readonly values: readonly string[];
166
215
  readonly default?: string;
167
216
  })
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
- })
217
+ /** The access preset: one kind on every harness that expresses it, a
218
+ * render per value, and `claims` naming the turn option the preset
219
+ * displaces when set (codex: sandbox) so exclusivity is data, not a
220
+ * harness name. */
221
+ | {
222
+ readonly kind: "access";
223
+ readonly renders: Readonly<Record<AccessValue, AccessRender>>;
224
+ readonly claims?: TurnOptionKey;
225
+ }
179
226
  /** Ladder comes from vocabulary.efforts / effortsByModel, not from here. */
180
227
  | (SpecBase & { readonly kind: "effort" })
181
228
  /** Open selector, CLEAN_SELECTOR-validated. */
@@ -208,28 +255,37 @@ export const resolveRender = (spec: SpecBase, phase: "launch" | "resume"): Optio
208
255
  return spec.resumeRender ?? spec.render;
209
256
  };
210
257
 
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);
258
+ export type Quoting = "toml" | "verbatim";
259
+
260
+ /** The argv tokens one resolved render produces for one value - the only
261
+ * place a render kind becomes tokens (RFC-02 change 1). `toml` quoting is
262
+ * `JSON.stringify`, sufficient for the closed vocabularies that ride
263
+ * config-kv; `verbatim` passes prose (prompt text) and bare TOML literals
264
+ * through unchanged. A flag-value or config-kv render with no value is a
265
+ * descriptor error, never an empty token. */
266
+ export const tokensFor = (
267
+ render: OptionRender,
268
+ value?: string,
269
+ quoting: Quoting = "toml",
270
+ ): readonly string[] => {
271
+ switch (render.kind) {
272
+ case "flag-value":
273
+ if (value === undefined) {
274
+ throw new Error(`render ${render.flag} needs a value and none was given`);
275
+ }
276
+ return [...(render.extraFlags ?? []), render.flag, value];
277
+ case "config-kv":
278
+ if (value === undefined) {
279
+ throw new Error(`render ${render.flag} ${render.key} needs a value and none was given`);
280
+ }
281
+ return [render.flag, `${render.key}=${quoting === "toml" ? JSON.stringify(value) : value}`];
282
+ case "flag-list":
283
+ return [...render.flags];
284
+ default: {
285
+ const exhaustive: never = render;
286
+ return exhaustive;
287
+ }
288
+ }
233
289
  };
234
290
 
235
291
  export interface HarnessDescriptor {
@@ -251,8 +307,8 @@ export interface HarnessDescriptor {
251
307
  * have no registry to poll, so the check falls back to the locally
252
308
  * installed `<bin> --version` and is skipped where the CLI is absent. */
253
309
  readonly versionSource:
254
- | { readonly kind: "npm"; readonly package: string }
255
- | { readonly kind: "installed" };
310
+ | { readonly kind: Extract<VersionSourceKind, "npm">; readonly package: string }
311
+ | { readonly kind: Extract<VersionSourceKind, "installed"> };
256
312
  /** Headless one-turn launch shape. `promptStyle: "positional"` means the
257
313
  * prompt travels as a bare argv entry (ordering constraints apply).
258
314
  * `streamFlags` is the output flag set a headless turn launches with so
@@ -267,7 +323,6 @@ export interface HarnessDescriptor {
267
323
  * `workspace-write` into subcommands. */
268
324
  readonly subcommands: readonly string[];
269
325
  readonly promptStyle: "positional";
270
- readonly toolsFlag: string | null;
271
326
  readonly streamFlags: readonly string[];
272
327
  /** The flag that pins a caller-assigned id at LAUNCH (spawn-time
273
328
  * assignment; the execution layer consumes it), or null when the
@@ -281,7 +336,7 @@ export interface HarnessDescriptor {
281
336
  * anywhere else (the v1 first-UUID-wins scar: a UUID inside quoted prompt
282
337
  * text was returned as the session id, and resuming it started a stranger). */
283
338
  readonly resume: {
284
- readonly style: "flag" | "positional";
339
+ readonly style: ResumeStyle;
285
340
  readonly flag: string;
286
341
  readonly aliases: readonly string[];
287
342
  readonly idShape: RegExp;
@@ -299,7 +354,7 @@ export interface HarnessDescriptor {
299
354
  * The protocol layer must know this: a consumer resuming a session it
300
355
  * believes exists gets a blank session, not an error, on a "create"
301
356
  * harness. */
302
- readonly onMissing: "error" | "create";
357
+ readonly onMissing: ResumeOnMissing;
303
358
  };
304
359
  /** Persistent headless session support: the complete flag list that follows
305
360
  * the binary to open one lucid-owned process serving many turns, or null
@@ -327,7 +382,11 @@ export interface HarnessDescriptor {
327
382
  readonly resumeFlag: SessionResumeFlag;
328
383
  readonly input: SessionInputContract;
329
384
  readonly turnEnd: Readonly<Record<string, string>>;
330
- readonly identityProbe: { readonly command: string } | null;
385
+ /** The command the runner writes at spawn to learn the session id, and
386
+ * the dot-path to that id in the response (pi rpc: `get_state`,
387
+ * `data.sessionId`). Encoded and decoded in interpretation
388
+ * (session-input.ts); execution holds no field names (ADR 0005). */
389
+ readonly identityProbe: { readonly command: string; readonly responseIdField: string } | null;
331
390
  } | null;
332
391
  /** Streaming is a property of the INVOCATION, not the harness: each pin
333
392
  * names the flag set that unlocks a granularity, checked in order, first
@@ -348,7 +407,7 @@ export interface HarnessDescriptor {
348
407
  * {type: "system", subtype: "init"}, re-emitted at every turn start with
349
408
  * the same value (A-001) - consumers dedupe via decodeIdentity. */
350
409
  readonly identity: {
351
- readonly authority: "caller-assigned" | "harness-minted";
410
+ readonly authority: IdentityAuthority;
352
411
  /** `idField` is a dot-path (muse nests its id at `stream.id`); an empty
353
412
  * `match` means "any record carrying the id path". */
354
413
  readonly announce: {
@@ -387,7 +446,7 @@ export interface HarnessDescriptor {
387
446
  readonly template: string;
388
447
  /** claude: '/', '.' -> '-'; pi: '/' -> '-' wrapped in leading/trailing
389
448
  * dashes, dots preserved. */
390
- readonly cwdSlug: "dash-separators" | "pi-dash-wrapped" | "verbatim";
449
+ readonly cwdSlug: CwdSlug;
391
450
  };
392
451
  /** How the harness exposes context-window usage; the interpretation layer
393
452
  * surfaces it as a `context` HarnessEvent. */
@@ -400,7 +459,7 @@ export interface HarnessDescriptor {
400
459
  readonly resumeLast: { readonly flag: string } | null;
401
460
  /** Whether backgrounded headless calls must have stdin closed (pi hangs
402
461
  * without `< /dev/null`). */
403
- readonly stdin: "inherit" | "close-required";
462
+ readonly stdin: StdinPolicy;
404
463
  /** Presence recognition: how an interactive process for a session id shows
405
464
  * up in a process listing. `headlessMarkers` mark a process as headless
406
465
  * (not interactive presence). Known blind spot, inherent to argv matching:
@@ -445,8 +504,7 @@ export interface HarnessDescriptor {
445
504
  * the claude asymmetry: claude's include flag pre-approves without
446
505
  * restricting the visible set, so an exact allowlist must render as a
447
506
  * 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/
507
+ * `builtins`: curated names + default-enabled state - grep/find/
450
508
  * ls ship off on pi, everything ships on elsewhere. `categories`:
451
509
  * non-list switches (muse disable flags, codex config booleans).
452
510
  * `denySemantics`: whether a deny removes the tool from the model-visible
@@ -461,13 +519,12 @@ export interface HarnessDescriptor {
461
519
  * muse: null (structural gap - trust/config scoped only). */
462
520
  readonly skills: {
463
521
  readonly loadFlag: string | null;
464
- readonly overridesVia: "settings-skilloverrides" | "config-skills-array" | null;
522
+ readonly overridesVia: SkillsOverridesVia | null;
465
523
  } | null;
466
524
  readonly tools: {
467
525
  readonly includeFlag: string | null;
468
526
  readonly excludeFlag: string | null;
469
527
  readonly includeIsStrictAllowlist: boolean;
470
- readonly composable: boolean;
471
528
  readonly builtins: ReadonlyArray<{
472
529
  readonly name: string;
473
530
  readonly defaultEnabled: boolean;
@@ -479,6 +536,6 @@ export interface HarnessDescriptor {
479
536
  readonly configKey: string | null;
480
537
  readonly canonical: readonly CanonicalTool[];
481
538
  }>;
482
- readonly denySemantics: "remove-from-set" | "policy-gate" | "no-lists";
539
+ readonly denySemantics: DenySemantics;
483
540
  };
484
541
  }
@@ -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
  },
@@ -60,7 +59,12 @@ export const museCode: HarnessDescriptor = deepFreeze({
60
59
  autonomy: { flag: "--yolo" },
61
60
  vocabulary: {
62
61
  modelFlag: "--model",
63
- models: ["muse-spark-1.2-contributor", "muse-spark-1.2", "muse-spark-1.1"],
62
+ models: [
63
+ "muse-spark-1.3-contributor",
64
+ "muse-spark-1.2-contributor",
65
+ "muse-spark-1.2",
66
+ "muse-spark-1.1",
67
+ ],
64
68
  aliases: {},
65
69
  efforts: ["none", "minimal", "low", "medium", "high", "xhigh"],
66
70
  extensible: false,
@@ -116,10 +120,14 @@ export const museCode: HarnessDescriptor = deepFreeze({
116
120
  max: 10000,
117
121
  render: { kind: "flag-value", flag: "--max-model-steps" },
118
122
  },
123
+ // read gates the write and shell categories; write is the harness
124
+ // default and emits nothing.
119
125
  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"] },
126
+ kind: "access",
127
+ renders: {
128
+ read: { render: { kind: "flag-list", flags: ["--disable-write", "--disable-shell"] } },
129
+ write: null,
130
+ },
123
131
  },
124
132
  },
125
133
  // Phase 0 fixtures: muse-category-flags.md. No name lists; category
@@ -133,7 +141,6 @@ export const museCode: HarnessDescriptor = deepFreeze({
133
141
  includeFlag: null,
134
142
  excludeFlag: null,
135
143
  includeIsStrictAllowlist: false,
136
- composable: false,
137
144
  builtins: [],
138
145
  categories: [
139
146
  {
@@ -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;