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

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 (114) hide show
  1. package/README.md +50 -1
  2. package/dist/cli/args.d.ts.map +1 -1
  3. package/dist/cli/args.js +4 -0
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/cli/check.d.ts +5 -1
  6. package/dist/cli/check.d.ts.map +1 -1
  7. package/dist/cli/check.js +47 -8
  8. package/dist/cli/check.js.map +1 -1
  9. package/dist/cli/help.d.ts +1 -1
  10. package/dist/cli/help.d.ts.map +1 -1
  11. package/dist/cli/help.js +23 -1
  12. package/dist/cli/help.js.map +1 -1
  13. package/dist/cli/inspect-context.d.ts +5 -0
  14. package/dist/cli/inspect-context.d.ts.map +1 -0
  15. package/dist/cli/inspect-context.js +86 -0
  16. package/dist/cli/inspect-context.js.map +1 -0
  17. package/dist/cli/inspect-session.d.ts +4 -0
  18. package/dist/cli/inspect-session.d.ts.map +1 -0
  19. package/dist/cli/inspect-session.js +71 -0
  20. package/dist/cli/inspect-session.js.map +1 -0
  21. package/dist/cli/inspect.d.ts.map +1 -1
  22. package/dist/cli/inspect.js +53 -24
  23. package/dist/cli/inspect.js.map +1 -1
  24. package/dist/cli/plan-turn.js +2 -2
  25. package/dist/cli/plan-turn.js.map +1 -1
  26. package/dist/cli/runtime-compatibility.d.ts +17 -0
  27. package/dist/cli/runtime-compatibility.d.ts.map +1 -0
  28. package/dist/cli/runtime-compatibility.js +46 -0
  29. package/dist/cli/runtime-compatibility.js.map +1 -0
  30. package/dist/execution/context-inspection.d.ts +20 -0
  31. package/dist/execution/context-inspection.d.ts.map +1 -0
  32. package/dist/execution/context-inspection.js +174 -0
  33. package/dist/execution/context-inspection.js.map +1 -0
  34. package/dist/execution/deps.d.ts +3 -0
  35. package/dist/execution/deps.d.ts.map +1 -1
  36. package/dist/execution/environment.d.ts +3 -0
  37. package/dist/execution/environment.d.ts.map +1 -0
  38. package/dist/execution/environment.js +16 -0
  39. package/dist/execution/environment.js.map +1 -0
  40. package/dist/execution/lines.d.ts +3 -1
  41. package/dist/execution/lines.d.ts.map +1 -1
  42. package/dist/execution/lines.js +26 -23
  43. package/dist/execution/lines.js.map +1 -1
  44. package/dist/execution/node-deps.d.ts.map +1 -1
  45. package/dist/execution/node-deps.js +14 -19
  46. package/dist/execution/node-deps.js.map +1 -1
  47. package/dist/execution/stream-turn.d.ts +1 -1
  48. package/dist/execution/stream-turn.d.ts.map +1 -1
  49. package/dist/execution/stream-turn.js +29 -5
  50. package/dist/execution/stream-turn.js.map +1 -1
  51. package/dist/execution/supervisor.d.ts +5 -0
  52. package/dist/execution/supervisor.d.ts.map +1 -1
  53. package/dist/execution/supervisor.js +26 -19
  54. package/dist/execution/supervisor.js.map +1 -1
  55. package/dist/interpretation/argv.d.ts +2 -0
  56. package/dist/interpretation/argv.d.ts.map +1 -1
  57. package/dist/interpretation/argv.js +26 -4
  58. package/dist/interpretation/argv.js.map +1 -1
  59. package/dist/interpretation/context-inspection.d.ts +41 -0
  60. package/dist/interpretation/context-inspection.d.ts.map +1 -0
  61. package/dist/interpretation/context-inspection.js +114 -0
  62. package/dist/interpretation/context-inspection.js.map +1 -0
  63. package/dist/interpretation/refusal.d.ts +1 -1
  64. package/dist/interpretation/refusal.d.ts.map +1 -1
  65. package/dist/interpretation/refusal.js.map +1 -1
  66. package/dist/interpretation/resolve-options.d.ts +1 -0
  67. package/dist/interpretation/resolve-options.d.ts.map +1 -1
  68. package/dist/interpretation/resolve-options.js +21 -28
  69. package/dist/interpretation/resolve-options.js.map +1 -1
  70. package/dist/interpretation/turn-options.d.ts +1 -1
  71. package/dist/interpretation/turn-options.d.ts.map +1 -1
  72. package/dist/interpretation/turn-options.js +9 -1
  73. package/dist/interpretation/turn-options.js.map +1 -1
  74. package/dist/knowledge/claude-code.d.ts +1 -1
  75. package/dist/knowledge/claude-code.d.ts.map +1 -1
  76. package/dist/knowledge/claude-code.js +22 -7
  77. package/dist/knowledge/claude-code.js.map +1 -1
  78. package/dist/knowledge/codex.d.ts.map +1 -1
  79. package/dist/knowledge/codex.js +1 -0
  80. package/dist/knowledge/codex.js.map +1 -1
  81. package/dist/knowledge/descriptor.d.ts +12 -0
  82. package/dist/knowledge/descriptor.d.ts.map +1 -1
  83. package/dist/knowledge/muse.d.ts.map +1 -1
  84. package/dist/knowledge/muse.js +1 -0
  85. package/dist/knowledge/muse.js.map +1 -1
  86. package/dist/knowledge/pi.d.ts.map +1 -1
  87. package/dist/knowledge/pi.js +1 -0
  88. package/dist/knowledge/pi.js.map +1 -1
  89. package/package.json +2 -2
  90. package/src/cli/args.ts +4 -0
  91. package/src/cli/check.ts +56 -11
  92. package/src/cli/help.ts +23 -1
  93. package/src/cli/inspect-context.ts +96 -0
  94. package/src/cli/inspect-session.ts +85 -0
  95. package/src/cli/inspect.ts +65 -28
  96. package/src/cli/plan-turn.ts +2 -2
  97. package/src/cli/runtime-compatibility.ts +55 -0
  98. package/src/execution/context-inspection.ts +177 -0
  99. package/src/execution/deps.ts +3 -0
  100. package/src/execution/environment.ts +15 -0
  101. package/src/execution/lines.ts +22 -23
  102. package/src/execution/node-deps.ts +14 -17
  103. package/src/execution/stream-turn.ts +31 -4
  104. package/src/execution/supervisor.ts +27 -17
  105. package/src/interpretation/argv.ts +24 -4
  106. package/src/interpretation/context-inspection.ts +169 -0
  107. package/src/interpretation/refusal.ts +1 -0
  108. package/src/interpretation/resolve-options.ts +29 -30
  109. package/src/interpretation/turn-options.ts +8 -0
  110. package/src/knowledge/claude-code.ts +23 -7
  111. package/src/knowledge/codex.ts +1 -0
  112. package/src/knowledge/descriptor.ts +9 -0
  113. package/src/knowledge/muse.ts +1 -0
  114. package/src/knowledge/pi.ts +1 -0
@@ -27,6 +27,30 @@ import {
27
27
  * signal. */
28
28
  export const KILL_GRACE_MS = 5_000;
29
29
 
30
+ /** Shared process termination policy for turn runners and control probes. */
31
+ export function superviseTermination(
32
+ clock: Clock,
33
+ signal: (sig: SignalName) => void,
34
+ ): { readonly escalate: () => void; readonly settle: () => void } {
35
+ let settled = false;
36
+ let timer: TimerHandle | null = null;
37
+ return {
38
+ escalate(): void {
39
+ if (settled || timer !== null) return;
40
+ signal("SIGTERM");
41
+ timer = clock.setTimeout(() => {
42
+ timer = null;
43
+ if (!settled) signal("SIGKILL");
44
+ }, KILL_GRACE_MS);
45
+ },
46
+ settle(): void {
47
+ settled = true;
48
+ if (timer !== null) clock.clearTimeout(timer);
49
+ timer = null;
50
+ },
51
+ };
52
+ }
53
+
30
54
  /** Bounded tail of unmatched stderr - the crash context a nonzero exit is
31
55
  * explained by (v1 kept the turn's output slice for exactly this). */
32
56
  export class StderrTail {
@@ -106,30 +130,18 @@ export const superviseTurn = (
106
130
  questionMode: QuestionMode,
107
131
  io: SupervisorIo,
108
132
  ): TurnSupervisor => {
109
- let settled = false;
110
- let killTimer: TimerHandle | null = null;
133
+ const termination = superviseTermination(io.clock, io.signal);
111
134
  let stallTimer: TimerHandle | null = null;
112
135
  let turnOpen = false;
113
136
  let lastAssistantText: string | null = null;
114
137
 
115
- const signal = (sig: SignalName): void => {
116
- if (!settled) io.signal(sig);
117
- };
118
-
119
138
  const disarm = (): void => {
120
139
  if (stallTimer !== null) io.clock.clearTimeout(stallTimer);
121
140
  stallTimer = null;
122
141
  turnOpen = false;
123
142
  };
124
143
 
125
- const escalate = (): void => {
126
- if (settled || killTimer !== null) return;
127
- signal("SIGTERM");
128
- killTimer = io.clock.setTimeout(() => {
129
- killTimer = null;
130
- signal("SIGKILL");
131
- }, KILL_GRACE_MS);
132
- };
144
+ const escalate = termination.escalate;
133
145
 
134
146
  const armStall = (): void => {
135
147
  if (io.stallMs === undefined || !turnOpen) return;
@@ -156,9 +168,7 @@ export const superviseTurn = (
156
168
  disarm,
157
169
  escalate,
158
170
  settle(): void {
159
- settled = true;
160
- if (killTimer !== null) io.clock.clearTimeout(killTimer);
161
- killTimer = null;
171
+ termination.settle();
162
172
  disarm();
163
173
  },
164
174
  async stderrLine(line: string): Promise<void> {
@@ -12,6 +12,7 @@ import type {
12
12
  import { defaultDescriptors } from "../knowledge/overrides.js";
13
13
  import { assertIsolationCombination } from "./isolation.js";
14
14
  import { ArgvRefusalError } from "./refusal.js";
15
+ import { assertAccessExclusivity } from "./resolve-options.js";
15
16
  import { assertUsableSessionId, SESSION_ID_MAX, SessionIdRefusalError } from "./session-id.js";
16
17
  import { renderSkillsSelection, type SkillsSelection } from "./skills-selection.js";
17
18
  import { supportedBy } from "./support.js";
@@ -105,12 +106,28 @@ export interface ResumeOptions extends TurnOptions {
105
106
 
106
107
  export type LaunchOptions = TurnOptions;
107
108
 
109
+ /** Returns the exact text to pipe, or null when it travels in argv. */
110
+ export function stdinPromptOf(h: HarnessDescriptor, opts: TurnOptions): string | null {
111
+ const transport = h.launch.stdinPrompt;
112
+ if (!transport) return null;
113
+ const text = promptTextOf(opts);
114
+ if (text.length <= transport.aboveBytes / 3) return null;
115
+ return text.length > transport.aboveBytes ||
116
+ new TextEncoder().encode(text).byteLength > transport.aboveBytes
117
+ ? text
118
+ : null;
119
+ }
120
+
108
121
  /** The shared tail of every headless-turn argv: prompt, stream flags, then
109
122
  * validated selections, with the variadic tools flag LAST and fed exactly
110
123
  * one joined token so nothing after it can be swallowed as a tool name. */
111
124
  const turnTail = (h: HarnessDescriptor, opts: TurnOptions): string[] => {
112
- assertCleanPrompt(h, opts.prompt);
113
- const tail = [promptTextOf(opts), ...h.launch.streamFlags];
125
+ const stdinPrompt = stdinPromptOf(h, opts);
126
+ if (stdinPrompt === null) assertCleanPrompt(h, opts.prompt);
127
+ const tail = [
128
+ stdinPrompt === null ? promptTextOf(opts) : (h.launch.stdinPrompt?.argument ?? ""),
129
+ ...h.launch.streamFlags,
130
+ ];
114
131
  if (opts.model !== undefined) {
115
132
  const validated = validateModel(h, opts.model);
116
133
  if (!validated.ok) {
@@ -154,8 +171,9 @@ const turnTail = (h: HarnessDescriptor, opts: TurnOptions): string[] => {
154
171
  export const buildLaunchArgv = (h: HarnessDescriptor, opts: LaunchOptions): string[] => [
155
172
  h.bin,
156
173
  ...h.launch.baseFlags,
157
- ...renderTurnOptions(h, opts, "launch"),
174
+ ...renderTurnOptions(h, opts, "launch", "before-prompt"),
158
175
  ...turnTail(h, opts),
176
+ ...renderTurnOptions(h, opts, "launch", "after-prompt"),
159
177
  ];
160
178
 
161
179
  /** A session id that fails the shape rule is a spawn-boundary refusal like
@@ -180,6 +198,7 @@ const refuseUnusableSessionId = (h: HarnessDescriptor, sessionId: string): void
180
198
 
181
199
  export const buildResumeArgv = (h: HarnessDescriptor, opts: ResumeOptions): string[] => {
182
200
  refuseUnusableSessionId(h, opts.sessionId);
201
+ assertAccessExclusivity(h, opts);
183
202
  // Subcommands lead, then the resume token and id, then the flags the
184
203
  // RESUME grammar accepts (never inherited launch flags - codex exec
185
204
  // resume rejects --sandbox). One shape serves both styles:
@@ -191,8 +210,9 @@ export const buildResumeArgv = (h: HarnessDescriptor, opts: ResumeOptions): stri
191
210
  h.resume.flag,
192
211
  opts.sessionId,
193
212
  ...h.resume.extraFlags,
194
- ...renderTurnOptions(h, opts, "resume"),
213
+ ...renderTurnOptions(h, opts, "resume", "before-prompt"),
195
214
  ...turnTail(h, opts),
215
+ ...renderTurnOptions(h, opts, "resume", "after-prompt"),
196
216
  ];
197
217
  };
198
218
 
@@ -0,0 +1,169 @@
1
+ import type { HarnessDescriptor } from "../knowledge/descriptor.js";
2
+ import type { SpawnArgvOptions } from "./argv.js";
3
+ import { buildSpawnArgv } from "./argv.js";
4
+ import { ArgvRefusalError } from "./refusal.js";
5
+ import { asRecord } from "./shape.js";
6
+
7
+ /** Preserve the real turn's launch settings, replacing only its prompt
8
+ * transport. The pending task is staged later over the control channel. */
9
+ export function buildContextInspectionArgv(
10
+ harness: HarnessDescriptor,
11
+ options: SpawnArgvOptions,
12
+ ): string[] {
13
+ assertContextInspectionOptions(harness, options);
14
+ if (harness.contextInspection?.kind !== "claude-control-v1") {
15
+ throw new ArgvRefusalError({
16
+ harness: harness.name,
17
+ issue: "invalid-option-value",
18
+ option: "context",
19
+ supported: ["verified native context adapter without native passthrough"],
20
+ });
21
+ }
22
+ return [
23
+ ...buildSpawnArgv(harness, { ...options, prompt: { text: "", explicit: true } }),
24
+ ...harness.contextInspection.flags,
25
+ ...(options.resume === undefined ? [] : [harness.contextInspection.forkFlag]),
26
+ ];
27
+ }
28
+
29
+ export function assertContextInspectionOptions(
30
+ harness: HarnessDescriptor,
31
+ options: SpawnArgvOptions,
32
+ ): void {
33
+ if (!options.passthrough?.length) return;
34
+ throw new ArgvRefusalError({
35
+ harness: harness.name,
36
+ issue: "invalid-option-value",
37
+ option: "context",
38
+ supported: ["context inspection without native passthrough"],
39
+ });
40
+ }
41
+
42
+ /** A native estimate of the complete staged request, including recalled
43
+ * history. No inferred token ratio or model table is used here. The caller
44
+ * owns its additional reserve and dispatch policy. */
45
+ export type ContextInspection =
46
+ | {
47
+ readonly contextWindowTokens: number;
48
+ readonly inputLimitTokens: number;
49
+ readonly method: "native-context-estimate";
50
+ readonly model: string;
51
+ readonly status: "available";
52
+ readonly totalTokens: number;
53
+ }
54
+ | {
55
+ readonly reason:
56
+ | "cancelled"
57
+ | "transport-limit"
58
+ | "transport"
59
+ | "protocol"
60
+ | "timeout"
61
+ | "cleanup";
62
+ readonly status: "unavailable";
63
+ }
64
+ | {
65
+ readonly reason:
66
+ | "unverified-adapter"
67
+ | "unsupported-adapter"
68
+ | "auth"
69
+ | "limit"
70
+ | "native-exit";
71
+ readonly status: "unavailable";
72
+ };
73
+
74
+ const positive = (value: unknown): value is number =>
75
+ typeof value === "number" && Number.isSafeInteger(value) && value > 0;
76
+
77
+ /** Normalize only a complete native response. A window alone says nothing
78
+ * about remaining capacity, and invalid compaction data must not increase it. */
79
+ export function contextInspectionOf(input: unknown): ContextInspection {
80
+ const value = asRecord(input);
81
+ if (
82
+ value === null ||
83
+ Array.isArray(input) ||
84
+ typeof value.model !== "string" ||
85
+ value.model.length === 0 ||
86
+ !positive(value.maxTokens) ||
87
+ typeof value.totalTokens !== "number" ||
88
+ !Number.isSafeInteger(value.totalTokens) ||
89
+ value.totalTokens < 0 ||
90
+ typeof value.isAutoCompactEnabled !== "boolean" ||
91
+ (value.isAutoCompactEnabled && !positive(value.autoCompactThreshold))
92
+ ) {
93
+ return { status: "unavailable", reason: "protocol" };
94
+ }
95
+ return {
96
+ contextWindowTokens: value.maxTokens,
97
+ inputLimitTokens:
98
+ value.isAutoCompactEnabled && positive(value.autoCompactThreshold)
99
+ ? Math.min(value.maxTokens, value.autoCompactThreshold)
100
+ : value.maxTokens,
101
+ method: "native-context-estimate",
102
+ model: value.model,
103
+ status: "available",
104
+ totalTokens: value.totalTokens,
105
+ };
106
+ }
107
+
108
+ export interface ContextProbe {
109
+ readonly initial: string;
110
+ readonly staged: string;
111
+ accept(
112
+ line: string,
113
+ ):
114
+ | { readonly kind: "send"; readonly line: string }
115
+ | { readonly kind: "complete"; readonly accounting: ContextInspection }
116
+ | null;
117
+ }
118
+
119
+ /** Pure protocol state: only an acknowledgement of this staged UUID allows
120
+ * the usage query. The execution owner handles transport and cancellation. */
121
+ export function createContextProbe(inputId: string, prompt: string): ContextProbe {
122
+ let phase: "initialize" | "ack" | "usage" | "finished" = "initialize";
123
+ const initializeId = `${inputId}:initialize`;
124
+ const usageId = `${inputId}:usage`;
125
+ const control = (id: string, request: unknown): string =>
126
+ JSON.stringify({ type: "control_request", request_id: id, request });
127
+ const staged = JSON.stringify({
128
+ message: { role: "user", content: prompt },
129
+ parent_tool_use_id: null,
130
+ shouldQuery: false,
131
+ type: "user",
132
+ uuid: inputId,
133
+ });
134
+ return {
135
+ initial: control(initializeId, { subtype: "initialize", hooks: null }),
136
+ staged,
137
+ accept(line) {
138
+ if (phase === "finished") return null;
139
+ let decoded: unknown;
140
+ try {
141
+ decoded = JSON.parse(line);
142
+ } catch {
143
+ phase = "finished";
144
+ return { kind: "complete", accounting: { status: "unavailable", reason: "protocol" } };
145
+ }
146
+ const frame = asRecord(decoded);
147
+ if (frame === null || Array.isArray(decoded)) return null;
148
+ if (phase === "ack" && frame.type === "user" && frame.uuid === inputId) {
149
+ phase = "usage";
150
+ return { kind: "send", line: control(usageId, { subtype: "get_context_usage" }) };
151
+ }
152
+ const response = asRecord(frame.response);
153
+ if (frame.type !== "control_response" || response === null || Array.isArray(frame.response))
154
+ return null;
155
+ const expected = phase === "initialize" ? initializeId : phase === "usage" ? usageId : null;
156
+ if (expected === null || response.request_id !== expected) return null;
157
+ if (response.subtype !== "success") {
158
+ phase = "finished";
159
+ return { kind: "complete", accounting: { status: "unavailable", reason: "protocol" } };
160
+ }
161
+ if (phase === "initialize") {
162
+ phase = "ack";
163
+ return { kind: "send", line: staged };
164
+ }
165
+ phase = "finished";
166
+ return { kind: "complete", accounting: contextInspectionOf(response.response) };
167
+ },
168
+ };
169
+ }
@@ -31,6 +31,7 @@ export type RefusalIssue = (typeof REFUSAL_ISSUES)[number];
31
31
  * branch on it without a default arm. */
32
32
  export type RefusalOption =
33
33
  | TurnOptionKey
34
+ | "context"
34
35
  | "tools"
35
36
  | "excludeTools"
36
37
  | "skills"
@@ -51,6 +51,30 @@ export interface ResolvedOptions {
51
51
  readonly unrenderable: readonly string[];
52
52
  }
53
53
 
54
+ export function assertAccessExclusivity(
55
+ harness: HarnessDescriptor,
56
+ options: Partial<TurnOptions>,
57
+ explicit: (key: keyof TurnOptions) => boolean = (key) => options[key] !== undefined,
58
+ ): void {
59
+ if (options.access === undefined) return;
60
+ const spec = harness.turnOptions.access;
61
+ const claimed = spec?.kind === "access" ? spec.claims : undefined;
62
+ const claimedConflict = claimed !== undefined && explicit(claimed as keyof TurnOptions);
63
+ const conflict = explicit("tools") || explicit("excludeTools") || claimedConflict;
64
+ if (conflict)
65
+ throw new ArgvRefusalError({
66
+ issue: "mutually-exclusive-options",
67
+ harness: harness.name,
68
+ option: "access",
69
+ supported: [
70
+ claimedConflict
71
+ ? `--access or --${claimed}, not both on ${harness.name}`
72
+ : "--access is a preset allowlist, not a filter over --tools/--exclude-tools",
73
+ ],
74
+ detail: "mutual exclusion",
75
+ });
76
+ }
77
+
54
78
  /** Expressibility per profile dimension. Dimensions whose "on" state is
55
79
  * the harness's own default (discovery all-on) or whose "off" state emits
56
80
  * nothing (autonomy false) are expressible EVERYWHERE - the profile value
@@ -211,36 +235,11 @@ export const resolveEffectiveOptions = (
211
235
  // from the descriptor, so no harness name appears here.
212
236
  const accessSpec = h.turnOptions.access;
213
237
  const claimed = accessSpec?.kind === "access" ? accessSpec.claims : undefined;
214
- if (claimed !== undefined && resolved.access !== undefined) {
215
- const hasExplicit =
216
- effectiveArgs[claimed as keyof TurnOptions] !== undefined ||
217
- sourceTier(claimed) !== undefined;
218
- if (hasExplicit) {
219
- throw new ArgvRefusalError({
220
- issue: "mutually-exclusive-options",
221
- harness: h.name,
222
- option: "access",
223
- supported: [`--access or --${claimed}, not both on ${h.name}`],
224
- detail: "mutual exclusion",
225
- });
226
- }
227
- }
228
- // Access vs tools exclusivity: only explicit tools/excludeTools from args or config, never profile-derived.
229
- if (resolved.access !== undefined) {
230
- const explicitTools = effectiveArgs.tools !== undefined || sourceTier("tools") !== undefined;
231
- const explicitExclude =
232
- (effectiveArgs as unknown as Record<string, unknown>).excludeTools !== undefined ||
233
- sourceTier("excludeTools") !== undefined;
234
- if (explicitTools || explicitExclude) {
235
- throw new ArgvRefusalError({
236
- issue: "mutually-exclusive-options",
237
- harness: h.name,
238
- option: "access",
239
- supported: ["--access is a preset allowlist, not a filter over --tools/--exclude-tools"],
240
- detail: "mutual exclusion",
241
- });
242
- }
243
- }
238
+ assertAccessExclusivity(
239
+ h,
240
+ resolved,
241
+ (key) => effectiveArgs[key] !== undefined || sourceTier(key) !== undefined,
242
+ );
244
243
 
245
244
  // toolMap merge per harness per canonical (project > user). The merged
246
245
  // shape is the one shape past this point (RFC-02 change 8).
@@ -41,6 +41,7 @@ export const renderTurnOptions = (
41
41
  h: HarnessDescriptor,
42
42
  opts: TurnOptions,
43
43
  phase: "launch" | "resume",
44
+ placement: "all" | "before-prompt" | "after-prompt" = "all",
44
45
  ): string[] => {
45
46
  assertIsolationCombination(h, opts);
46
47
  const sequences: string[][] = [];
@@ -52,6 +53,13 @@ export const renderTurnOptions = (
52
53
  for (const key of TURN_OPTION_KEYS) {
53
54
  const spec = h.turnOptions[key];
54
55
  const raw = (opts as unknown as Record<string, unknown>)[key];
56
+ const afterPrompt =
57
+ key === "access" &&
58
+ spec?.kind === "access" &&
59
+ opts.access !== undefined &&
60
+ spec.renders[opts.access] === "tool-preset";
61
+ if (placement === "before-prompt" && afterPrompt) continue;
62
+ if (placement === "after-prompt" && !afterPrompt) continue;
55
63
 
56
64
  // Discovery is a table of facets, handled separately.
57
65
  if (key === "discovery") {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The claude-code descriptor: facts about the `claude` CLI as data, verified
3
- * against claude 2.1.233 and the 00-chat-substrate spike evidence (A-001,
3
+ * against claude 2.1.263 and the 00-chat-substrate spike evidence (A-001,
4
4
  * A-002, A-005). No process logic lives here.
5
5
  *
6
6
  * Discovery: claude 2.1.233 has no isolated instruction-file toggle.
@@ -20,15 +20,20 @@
20
20
  import { deepFreeze, type HarnessDescriptor, UUID_SHAPE } from "./descriptor.js";
21
21
  import { SHARED_AUTH_MATCHERS, SHARED_LIMIT_MATCHERS } from "./matchers.js";
22
22
 
23
+ const STREAM_INPUT_FLAGS = ["--input-format", "stream-json"] as const;
24
+
23
25
  export const claudeCode: HarnessDescriptor = deepFreeze({
24
26
  name: "claude",
25
27
  bin: "claude",
26
- verifiedAgainst: "2.1.233",
28
+ verifiedAgainst: "2.1.263",
27
29
  versionSource: { kind: "npm", package: "@anthropic-ai/claude-code" },
28
30
  launch: {
29
31
  baseFlags: ["-p"],
30
32
  subcommands: [],
31
33
  promptStyle: "positional",
34
+ // Print mode accepts piped text. Keep ordinary argv introspection
35
+ // stable while staying below per-argument limits for large requests.
36
+ stdinPrompt: { argument: "", aboveBytes: 65_536 },
32
37
  // A headless turn launches with the full stream-json output set so the
33
38
  // runner can decode identity/limits and stream token deltas; bare -p
34
39
  // (granularity none) is a degraded invocation this builder never emits.
@@ -67,8 +72,7 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
67
72
  // recalls codeword "pomegranate".
68
73
  flags: [
69
74
  "-p",
70
- "--input-format",
71
- "stream-json",
75
+ ...STREAM_INPUT_FLAGS,
72
76
  "--output-format",
73
77
  "stream-json",
74
78
  "--include-partial-messages",
@@ -117,9 +121,16 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
117
121
  autonomy: { flag: "--dangerously-skip-permissions" },
118
122
  vocabulary: {
119
123
  modelFlag: "--model",
120
- models: ["claude-fable-5", "claude-opus-5", "claude-sonnet-5", "claude-haiku-4-5-20251001"],
124
+ // https://platform.claude.com/docs/en/models/fable-5-1/overview
125
+ models: [
126
+ "claude-fable-5-1",
127
+ "claude-fable-5",
128
+ "claude-opus-5",
129
+ "claude-sonnet-5",
130
+ "claude-haiku-4-5-20251001",
131
+ ],
121
132
  aliases: {
122
- fable: "claude-fable-5",
133
+ fable: "claude-fable-5-1",
123
134
  opus: "claude-opus-5",
124
135
  sonnet: "claude-sonnet-5",
125
136
  haiku: "claude-haiku-4-5-20251001",
@@ -140,6 +151,11 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
140
151
  object: "context_window",
141
152
  usedPctField: "used_percentage",
142
153
  },
154
+ contextInspection: {
155
+ kind: "claude-control-v1",
156
+ flags: [...STREAM_INPUT_FLAGS, "--no-session-persistence", "--replay-user-messages"],
157
+ forkFlag: "--fork-session",
158
+ },
143
159
  resumeLast: null,
144
160
  stdin: "inherit",
145
161
  presence: {
@@ -160,7 +176,7 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
160
176
  // records a model id - absence of evidence, not an unset field.
161
177
  escalation: {
162
178
  supported: true,
163
- observedOn: { harness: "claude", model: "", version: "2.1.235", date: "2026-08-19" },
179
+ observedOn: { harness: "claude", model: "sonnet", version: "2.1.263", date: "2026-09-07" },
164
180
  },
165
181
  turnOptions: {
166
182
  // Native CLI reference: bare removes discovery; the empty built-in list and
@@ -83,6 +83,7 @@ export const codexCli: HarnessDescriptor = deepFreeze({
83
83
  cwdSlug: "verbatim",
84
84
  },
85
85
  contextHook: null,
86
+ contextInspection: null,
86
87
  // Valid only in the `exec resume` context: `codex exec resume --last`.
87
88
  resumeLast: { flag: "--last" },
88
89
  // codex exec appends piped stdin as a <stdin> block and can block on an
@@ -323,6 +323,8 @@ export interface HarnessDescriptor {
323
323
  * `workspace-write` into subcommands. */
324
324
  readonly subcommands: readonly string[];
325
325
  readonly promptStyle: "positional";
326
+ /** A verified alternate transport for prompts too large for argv. */
327
+ readonly stdinPrompt?: { readonly argument: string; readonly aboveBytes: number };
326
328
  readonly streamFlags: readonly string[];
327
329
  /** The flag that pins a caller-assigned id at LAUNCH (spawn-time
328
330
  * assignment; the execution layer consumes it), or null when the
@@ -454,6 +456,13 @@ export interface HarnessDescriptor {
454
456
  readonly object: string;
455
457
  readonly usedPctField: string;
456
458
  } | null;
459
+ /** Disposable native context accounting, verified at verifiedAgainst.
460
+ * Null is unknown support, never a model-window estimate. */
461
+ readonly contextInspection: {
462
+ readonly flags: readonly string[];
463
+ readonly forkFlag: string;
464
+ readonly kind: "claude-control-v1";
465
+ } | null;
457
466
  /** Resume-most-recent support (codex --last), or null. The race it opens
458
467
  * is owned by the corroboration ranking in interpretation. */
459
468
  readonly resumeLast: { readonly flag: string } | null;
@@ -77,6 +77,7 @@ export const museCode: HarnessDescriptor = deepFreeze({
77
77
  cwdSlug: "verbatim",
78
78
  },
79
79
  contextHook: null,
80
+ contextInspection: null,
80
81
  // `muse resume --last` exists (muse resume --help).
81
82
  resumeLast: { flag: "--last" },
82
83
  stdin: "inherit",
@@ -115,6 +115,7 @@ export const piCli: HarnessDescriptor = deepFreeze({
115
115
  cwdSlug: "pi-dash-wrapped",
116
116
  },
117
117
  contextHook: null,
118
+ contextInspection: null,
118
119
  resumeLast: null,
119
120
  stdin: "close-required",
120
121
  presence: {