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

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 (57) hide show
  1. package/README.md +28 -6
  2. package/dist/cli/help.d.ts +1 -1
  3. package/dist/cli/help.d.ts.map +1 -1
  4. package/dist/cli/help.js +5 -2
  5. package/dist/cli/help.js.map +1 -1
  6. package/dist/cli/inspect-context.js +4 -6
  7. package/dist/cli/inspect-context.js.map +1 -1
  8. package/dist/cli/inspect-session.js +1 -1
  9. package/dist/cli/inspect-session.js.map +1 -1
  10. package/dist/cli/inspect.d.ts.map +1 -1
  11. package/dist/cli/inspect.js +9 -2
  12. package/dist/cli/inspect.js.map +1 -1
  13. package/dist/cli/runtime-compatibility.d.ts +4 -3
  14. package/dist/cli/runtime-compatibility.d.ts.map +1 -1
  15. package/dist/cli/runtime-compatibility.js +4 -3
  16. package/dist/cli/runtime-compatibility.js.map +1 -1
  17. package/dist/execution/context-inspection.d.ts +1 -1
  18. package/dist/execution/context-inspection.d.ts.map +1 -1
  19. package/dist/execution/context-inspection.js +28 -30
  20. package/dist/execution/context-inspection.js.map +1 -1
  21. package/dist/execution/lines.d.ts +2 -1
  22. package/dist/execution/lines.d.ts.map +1 -1
  23. package/dist/execution/lines.js +4 -1
  24. package/dist/execution/lines.js.map +1 -1
  25. package/dist/interpretation/context-inspection.d.ts +8 -0
  26. package/dist/interpretation/context-inspection.d.ts.map +1 -1
  27. package/dist/interpretation/context-inspection.js +55 -5
  28. package/dist/interpretation/context-inspection.js.map +1 -1
  29. package/dist/knowledge/claude-code.d.ts.map +1 -1
  30. package/dist/knowledge/claude-code.js +5 -0
  31. package/dist/knowledge/claude-code.js.map +1 -1
  32. package/dist/knowledge/codex.d.ts +2 -3
  33. package/dist/knowledge/codex.d.ts.map +1 -1
  34. package/dist/knowledge/codex.js +11 -5
  35. package/dist/knowledge/codex.js.map +1 -1
  36. package/dist/knowledge/descriptor.d.ts +14 -1
  37. package/dist/knowledge/descriptor.d.ts.map +1 -1
  38. package/dist/knowledge/muse.d.ts.map +1 -1
  39. package/dist/knowledge/muse.js +1 -0
  40. package/dist/knowledge/muse.js.map +1 -1
  41. package/dist/knowledge/pi.d.ts.map +1 -1
  42. package/dist/knowledge/pi.js +1 -0
  43. package/dist/knowledge/pi.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/cli/help.ts +5 -2
  46. package/src/cli/inspect-context.ts +4 -8
  47. package/src/cli/inspect-session.ts +1 -1
  48. package/src/cli/inspect.ts +10 -2
  49. package/src/cli/runtime-compatibility.ts +10 -4
  50. package/src/execution/context-inspection.ts +35 -30
  51. package/src/execution/lines.ts +5 -1
  52. package/src/interpretation/context-inspection.ts +73 -4
  53. package/src/knowledge/claude-code.ts +5 -0
  54. package/src/knowledge/codex.ts +11 -5
  55. package/src/knowledge/descriptor.ts +14 -1
  56. package/src/knowledge/muse.ts +1 -0
  57. package/src/knowledge/pi.ts +1 -0
@@ -1,5 +1,8 @@
1
1
  import type { ContextInspection } from "../interpretation/context-inspection.js";
2
- import { createContextProbe } from "../interpretation/context-inspection.js";
2
+ import {
3
+ createContextProbe,
4
+ settledContextInspection,
5
+ } from "../interpretation/context-inspection.js";
3
6
  import { detectAuthFailureInLine, detectLimitInLine } from "../interpretation/limits.js";
4
7
  import type { HarnessDescriptor } from "../knowledge/descriptor.js";
5
8
 
@@ -10,7 +13,7 @@ import { LineBuffer } from "./lines.js";
10
13
  import { KILL_GRACE_MS, superviseTermination } from "./supervisor.js";
11
14
 
12
15
  export interface ContextInspectionOptions {
13
- /** Already validated, version-bound ephemeral argv. The CLI adapter owns
16
+ /** Already validated, executable-bound ephemeral argv. The CLI adapter owns
14
17
  * no-persistence and fork-on-resume enforcement before this layer runs. */
15
18
  readonly argv: readonly string[];
16
19
  readonly cwd?: string;
@@ -53,11 +56,14 @@ export async function inspectContext(
53
56
  return { status: "unavailable", reason: "transport" };
54
57
  }
55
58
  let finished = false;
59
+ const outcome: { accounting: ContextInspection } = {
60
+ accounting: { status: "unavailable", reason: "transport" },
61
+ };
56
62
  let exited = false;
57
63
  let exitCode: number | null = null;
58
64
  let nativeFailure: "auth" | "limit" | null = null;
59
65
  let disposalTimer: TimerHandle | undefined;
60
- const result = deferred<ContextInspection>();
66
+ const result = deferred<void>();
61
67
  const stopped = deferred<boolean>();
62
68
  const signal = (kind: "SIGTERM" | "SIGKILL"): void => {
63
69
  try {
@@ -68,18 +74,17 @@ export async function inspectContext(
68
74
  };
69
75
  const termination = superviseTermination(deps.clock, signal);
70
76
  const finish = (value: ContextInspection): void => {
77
+ if (!finished || (outcome.accounting.status === "available" && value.status === "unavailable"))
78
+ outcome.accounting = value;
71
79
  if (finished) return;
72
80
  finished = true;
73
- result.resolve(value);
81
+ result.resolve();
74
82
  try {
75
83
  child.stdin?.end();
76
84
  } catch {
77
85
  /* A broken pipe still needs cleanup. */
78
86
  }
79
- if (exited) {
80
- stopped.resolve(true);
81
- return;
82
- }
87
+ if (exited) return;
83
88
  termination.escalate();
84
89
  disposalTimer = deps.clock.setTimeout(() => {
85
90
  child.disposeOutput();
@@ -99,9 +104,9 @@ export async function inspectContext(
99
104
  }
100
105
  };
101
106
  const onLine = (line: string): void => {
102
- if (finished) return;
107
+ if (finished && outcome.accounting.status !== "available") return;
103
108
  const action = probe.accept(line);
104
- if (action?.kind === "send") write(action.line);
109
+ if (action?.kind === "send" && !finished) write(action.line);
105
110
  else if (action?.kind === "complete") finish(action.accounting);
106
111
  };
107
112
  const deadline = deps.clock.setTimeout(
@@ -111,14 +116,16 @@ export async function inspectContext(
111
116
  const abort = (): void => finish({ status: "unavailable", reason: "cancelled" });
112
117
  options.signal?.addEventListener("abort", abort, { once: true });
113
118
  const stdout = (async (): Promise<void> => {
114
- const lines = new LineBuffer(CONTEXT_TRANSPORT_MAX + 65_536);
119
+ const lines = new LineBuffer(CONTEXT_TRANSPORT_MAX + 65_536, () =>
120
+ finish({ status: "unavailable", reason: "transport-limit" }),
121
+ );
115
122
  try {
116
123
  for await (const chunk of child.stdout) {
117
- if (!finished) for (const line of lines.push(chunk)) onLine(line);
124
+ for (const line of lines.push(chunk)) onLine(line);
118
125
  }
119
126
  const tail = lines.flush();
120
127
  if (tail !== null) onLine(tail);
121
- finish({ status: "unavailable", reason: "transport" });
128
+ if (!finished) finish({ status: "unavailable", reason: "transport" });
122
129
  } catch {
123
130
  finish({ status: "unavailable", reason: "transport" });
124
131
  }
@@ -142,36 +149,34 @@ export async function inspectContext(
142
149
  void child.exited.then(async (code) => {
143
150
  exited = true;
144
151
  exitCode = code;
145
- if (code !== null && code !== 0) finish({ status: "unavailable", reason: "transport" });
152
+ if (!finished && code !== null && code !== 0)
153
+ finish({ status: "unavailable", reason: "transport" });
146
154
  termination.settle();
147
155
  if (disposalTimer !== undefined) deps.clock.clearTimeout(disposalTimer);
148
156
  // Exit can precede the final bytes reaching the runtime's read buffer.
149
157
  // Drain naturally first; inherited pipes get a bounded disposal fallback.
150
158
  let drainTimer: TimerHandle | undefined;
151
- await Promise.race([
152
- Promise.all([stdout, stderr]),
153
- new Promise<void>((resolve) => {
154
- drainTimer = deps.clock.setTimeout(resolve, KILL_GRACE_MS);
159
+ const drained = await Promise.race([
160
+ Promise.all([stdout, stderr]).then(() => true),
161
+ new Promise<boolean>((resolve) => {
162
+ drainTimer = deps.clock.setTimeout(() => resolve(false), KILL_GRACE_MS);
155
163
  }),
156
164
  ]);
157
165
  if (drainTimer !== undefined) deps.clock.clearTimeout(drainTimer);
158
166
  child.disposeOutput();
159
- stopped.resolve(true);
167
+ stopped.resolve(drained);
160
168
  });
161
169
  write(probe.initial);
162
170
  // Abort can arrive during spawn or listener registration.
163
171
  if (options.signal?.aborted) abort();
164
- const value = await result.promise;
172
+ await result.promise;
165
173
  deps.clock.clearTimeout(deadline);
166
- options.signal?.removeEventListener("abort", abort);
167
174
  const clean = await stopped.promise;
168
- if (clean) await Promise.all([stdout, stderr]);
169
- if (!clean) return { status: "unavailable", reason: "cleanup" };
170
- if (value.status === "unavailable" && value.reason === "transport") {
171
- return {
172
- status: "unavailable",
173
- reason: nativeFailure ?? (exitCode !== null && exitCode !== 0 ? "native-exit" : "transport"),
174
- };
175
- }
176
- return value;
175
+ options.signal?.removeEventListener("abort", abort);
176
+ return settledContextInspection({
177
+ accounting: outcome.accounting,
178
+ clean,
179
+ exitCode,
180
+ nativeFailure,
181
+ });
177
182
  }
@@ -11,7 +11,10 @@
11
11
  export const LINE_MAX = 65_536;
12
12
 
13
13
  export class LineBuffer {
14
- constructor(private readonly limit = LINE_MAX) {}
14
+ constructor(
15
+ private readonly limit = LINE_MAX,
16
+ private readonly onOverflow?: () => void,
17
+ ) {}
15
18
  // Per-instance and stateful ({stream:true}): a shared decoder would carry
16
19
  // partial-sequence state across two streams and corrupt both.
17
20
  private readonly decoder = new TextDecoder();
@@ -44,6 +47,7 @@ export class LineBuffer {
44
47
  if (this.pending.length + text.length > this.limit) {
45
48
  this.pending = "";
46
49
  this.discarding = true;
50
+ this.onOverflow?.();
47
51
  } else this.pending += text;
48
52
  }
49
53
 
@@ -1,6 +1,7 @@
1
1
  import type { HarnessDescriptor } from "../knowledge/descriptor.js";
2
2
  import type { SpawnArgvOptions } from "./argv.js";
3
3
  import { buildSpawnArgv } from "./argv.js";
4
+ import { contentEventsOf } from "./content.js";
4
5
  import { ArgvRefusalError } from "./refusal.js";
5
6
  import { asRecord } from "./shape.js";
6
7
 
@@ -16,7 +17,7 @@ export function buildContextInspectionArgv(
16
17
  harness: harness.name,
17
18
  issue: "invalid-option-value",
18
19
  option: "context",
19
- supported: ["verified native context adapter without native passthrough"],
20
+ supported: ["supported native context adapter without native passthrough"],
20
21
  });
21
22
  }
22
23
  return [
@@ -74,6 +75,32 @@ export type ContextInspection =
74
75
  const positive = (value: unknown): value is number =>
75
76
  typeof value === "number" && Number.isSafeInteger(value) && value > 0;
76
77
 
78
+ /** Cleanup and caller interruption take precedence. Otherwise a recognized
79
+ * native failure explains a failed exchange more precisely than its framing. */
80
+ export function settledContextInspection(facts: {
81
+ readonly accounting: ContextInspection;
82
+ readonly clean: boolean;
83
+ readonly exitCode: number | null;
84
+ readonly nativeFailure: "auth" | "limit" | null;
85
+ }): ContextInspection {
86
+ const { accounting, clean, exitCode, nativeFailure } = facts;
87
+ if (!clean) return { status: "unavailable", reason: "cleanup" };
88
+ if (
89
+ accounting.status === "unavailable" &&
90
+ (accounting.reason === "cancelled" || accounting.reason === "timeout")
91
+ )
92
+ return accounting;
93
+ if (nativeFailure !== null) return { status: "unavailable", reason: nativeFailure };
94
+ if (
95
+ accounting.status === "unavailable" &&
96
+ accounting.reason === "transport" &&
97
+ exitCode !== null &&
98
+ exitCode !== 0
99
+ )
100
+ return { status: "unavailable", reason: "native-exit" };
101
+ return accounting;
102
+ }
103
+
77
104
  /** Normalize only a complete native response. A window alone says nothing
78
105
  * about remaining capacity, and invalid compaction data must not increase it. */
79
106
  export function contextInspectionOf(input: unknown): ContextInspection {
@@ -135,7 +162,6 @@ export function createContextProbe(inputId: string, prompt: string): ContextProb
135
162
  initial: control(initializeId, { subtype: "initialize", hooks: null }),
136
163
  staged,
137
164
  accept(line) {
138
- if (phase === "finished") return null;
139
165
  let decoded: unknown;
140
166
  try {
141
167
  decoded = JSON.parse(line);
@@ -144,7 +170,46 @@ export function createContextProbe(inputId: string, prompt: string): ContextProb
144
170
  return { kind: "complete", accounting: { status: "unavailable", reason: "protocol" } };
145
171
  }
146
172
  const frame = asRecord(decoded);
147
- if (frame === null || Array.isArray(decoded)) return null;
173
+ const failure = contentEventsOf("claude", decoded).find(
174
+ (event) => event.kind === "limit" || (event.kind === "error" && event.terminal),
175
+ );
176
+ if (failure) {
177
+ phase = "finished";
178
+ return {
179
+ kind: "complete",
180
+ accounting: {
181
+ status: "unavailable",
182
+ reason: failure.kind === "limit" ? "limit" : "native-exit",
183
+ },
184
+ };
185
+ }
186
+ const content = asRecord(frame?.message)?.content;
187
+ const toolResult =
188
+ frame?.type === "user" &&
189
+ (frame.parent_tool_use_id != null ||
190
+ (Array.isArray(content) &&
191
+ content.some((block) => asRecord(block)?.type === "tool_result")));
192
+ const allowed =
193
+ frame !== null &&
194
+ !Array.isArray(decoded) &&
195
+ !toolResult &&
196
+ (frame.type === "user" ||
197
+ frame.type === "control_response" ||
198
+ frame.type === "command_lifecycle" ||
199
+ (frame.type === "system" &&
200
+ ["init", "hook_started", "hook_progress", "hook_response"].includes(
201
+ String(frame.subtype),
202
+ )) ||
203
+ (frame.type === "result" && frame.subtype === "success" && frame.num_turns === 0) ||
204
+ (frame.type === "rate_limit_event" &&
205
+ ["allowed", "allowed_warning"].includes(
206
+ String(asRecord(frame.rate_limit_info)?.status),
207
+ )));
208
+ if (!allowed || frame === null) {
209
+ phase = "finished";
210
+ return { kind: "complete", accounting: { status: "unavailable", reason: "protocol" } };
211
+ }
212
+ if (phase === "finished") return null;
148
213
  if (phase === "ack" && frame.type === "user" && frame.uuid === inputId) {
149
214
  phase = "usage";
150
215
  return { kind: "send", line: control(usageId, { subtype: "get_context_usage" }) };
@@ -154,7 +219,11 @@ export function createContextProbe(inputId: string, prompt: string): ContextProb
154
219
  return null;
155
220
  const expected = phase === "initialize" ? initializeId : phase === "usage" ? usageId : null;
156
221
  if (expected === null || response.request_id !== expected) return null;
157
- if (response.subtype !== "success") {
222
+ if (
223
+ response.subtype !== "success" ||
224
+ (phase === "initialize" &&
225
+ (asRecord(response.response) === null || Array.isArray(response.response)))
226
+ ) {
158
227
  phase = "finished";
159
228
  return { kind: "complete", accounting: { status: "unavailable", reason: "protocol" } };
160
229
  }
@@ -41,6 +41,7 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
41
41
  idFlag: "--session-id",
42
42
  },
43
43
  resume: {
44
+ admission: { kind: "invocation", modes: ["headless-turn"] },
44
45
  // A-005: claude resumes are id-stable - the caller-assigned id survives
45
46
  // every resume, so there is no rotation handling and forking is only the
46
47
  // explicit --fork-session flag (deliberate branching, never a default).
@@ -151,6 +152,10 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
151
152
  object: "context_window",
152
153
  usedPctField: "used_percentage",
153
154
  },
155
+ nativeContextManagement: {
156
+ kind: "native-session-auto-compaction",
157
+ modes: ["headless-turn"],
158
+ },
154
159
  contextInspection: {
155
160
  kind: "claude-control-v1",
156
161
  flags: [...STREAM_INPUT_FLAGS, "--no-session-persistence", "--replay-user-messages"],
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * The codex descriptor: facts about the `codex` CLI as data, verified
3
- * against codex-cli 0.147.0 and lucid v1's registry. Descriptor groundwork
4
- * only (D-003): not exercised through the chat protocol until the claude
5
- * vertical slice is green.
3
+ * against codex-cli 0.153.4. Native capability and question recordings
4
+ * live in test/fixtures/codex-0.153.4.
6
5
  */
7
6
  import { deepFreeze, type HarnessDescriptor, UUID_SHAPE } from "./descriptor.js";
8
7
  import { SHARED_AUTH_MATCHERS, SHARED_LIMIT_MATCHERS } from "./matchers.js";
@@ -10,7 +9,7 @@ import { SHARED_AUTH_MATCHERS, SHARED_LIMIT_MATCHERS } from "./matchers.js";
10
9
  export const codexCli: HarnessDescriptor = deepFreeze({
11
10
  name: "codex",
12
11
  bin: "codex",
13
- verifiedAgainst: "0.147.0",
12
+ verifiedAgainst: "0.153.4",
14
13
  versionSource: { kind: "npm", package: "@openai/codex" },
15
14
  launch: {
16
15
  // exec --json emits structured item events; without --json, identity
@@ -84,6 +83,8 @@ export const codexCli: HarnessDescriptor = deepFreeze({
84
83
  },
85
84
  contextHook: null,
86
85
  contextInspection: null,
86
+ // Codex 0.153.4 core/session/turn.rs runs native automatic compaction.
87
+ nativeContextManagement: { kind: "auto-compaction", modes: ["headless-turn"] },
87
88
  // Valid only in the `exec resume` context: `codex exec resume --last`.
88
89
  resumeLast: { flag: "--last" },
89
90
  // codex exec appends piped stdin as a <stdin> block and can block on an
@@ -108,7 +109,12 @@ export const codexCli: HarnessDescriptor = deepFreeze({
108
109
  // records a model id - absence of evidence, not an unset field.
109
110
  escalation: {
110
111
  supported: true,
111
- observedOn: { harness: "codex", model: "", version: "0.146.1", date: "2026-08-19" },
112
+ observedOn: {
113
+ harness: "codex",
114
+ model: "gpt-6-astra",
115
+ version: "0.153.4",
116
+ date: "2026-09-08",
117
+ },
112
118
  },
113
119
  turnOptions: {
114
120
  // Codex config reference; accepted as an integer on CLI 0.153.4.
@@ -338,6 +338,12 @@ export interface HarnessDescriptor {
338
338
  * anywhere else (the v1 first-UUID-wins scar: a UUID inside quoted prompt
339
339
  * text was returned as the session id, and resuming it started a stranger). */
340
340
  readonly resume: {
341
+ /** Declared invocation support by mode, not proof that a saved session exists.
342
+ * Omitted retains the verification-version admission policy. */
343
+ readonly admission?: {
344
+ readonly kind: "invocation";
345
+ readonly modes: readonly HarnessMode[];
346
+ };
341
347
  readonly style: ResumeStyle;
342
348
  readonly flag: string;
343
349
  readonly aliases: readonly string[];
@@ -456,13 +462,20 @@ export interface HarnessDescriptor {
456
462
  readonly object: string;
457
463
  readonly usedPctField: string;
458
464
  } | null;
459
- /** Disposable native context accounting, verified at verifiedAgainst.
465
+ /** Disposable native context accounting; the exchange validates support.
460
466
  * Null is unknown support, never a model-window estimate. */
461
467
  readonly contextInspection: {
462
468
  readonly flags: readonly string[];
463
469
  readonly forkFlag: string;
464
470
  readonly kind: "claude-control-v1";
465
471
  } | null;
472
+ /** Curated native context handling, not a count or enabled-state observation.
473
+ * Native-session handling covers the native session; callers must prepare
474
+ * imported history. Disabled native compaction is not detected here. */
475
+ readonly nativeContextManagement: {
476
+ readonly kind: "auto-compaction" | "native-session-auto-compaction";
477
+ readonly modes: readonly HarnessMode[];
478
+ } | null;
466
479
  /** Resume-most-recent support (codex --last), or null. The race it opens
467
480
  * is owned by the corroboration ranking in interpretation. */
468
481
  readonly resumeLast: { readonly flag: string } | null;
@@ -78,6 +78,7 @@ export const museCode: HarnessDescriptor = deepFreeze({
78
78
  },
79
79
  contextHook: null,
80
80
  contextInspection: null,
81
+ nativeContextManagement: null,
81
82
  // `muse resume --last` exists (muse resume --help).
82
83
  resumeLast: { flag: "--last" },
83
84
  stdin: "inherit",
@@ -116,6 +116,7 @@ export const piCli: HarnessDescriptor = deepFreeze({
116
116
  },
117
117
  contextHook: null,
118
118
  contextInspection: null,
119
+ nativeContextManagement: null,
119
120
  resumeLast: null,
120
121
  stdin: "close-required",
121
122
  presence: {