@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.
- package/README.md +50 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/check.d.ts +5 -1
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +47 -8
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +23 -1
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/inspect-context.d.ts +5 -0
- package/dist/cli/inspect-context.d.ts.map +1 -0
- package/dist/cli/inspect-context.js +86 -0
- package/dist/cli/inspect-context.js.map +1 -0
- package/dist/cli/inspect-session.d.ts +4 -0
- package/dist/cli/inspect-session.d.ts.map +1 -0
- package/dist/cli/inspect-session.js +71 -0
- package/dist/cli/inspect-session.js.map +1 -0
- package/dist/cli/inspect.d.ts.map +1 -1
- package/dist/cli/inspect.js +53 -24
- package/dist/cli/inspect.js.map +1 -1
- package/dist/cli/plan-turn.js +2 -2
- package/dist/cli/plan-turn.js.map +1 -1
- package/dist/cli/runtime-compatibility.d.ts +17 -0
- package/dist/cli/runtime-compatibility.d.ts.map +1 -0
- package/dist/cli/runtime-compatibility.js +46 -0
- package/dist/cli/runtime-compatibility.js.map +1 -0
- package/dist/execution/context-inspection.d.ts +20 -0
- package/dist/execution/context-inspection.d.ts.map +1 -0
- package/dist/execution/context-inspection.js +174 -0
- package/dist/execution/context-inspection.js.map +1 -0
- package/dist/execution/deps.d.ts +3 -0
- package/dist/execution/deps.d.ts.map +1 -1
- package/dist/execution/environment.d.ts +3 -0
- package/dist/execution/environment.d.ts.map +1 -0
- package/dist/execution/environment.js +16 -0
- package/dist/execution/environment.js.map +1 -0
- package/dist/execution/lines.d.ts +3 -1
- package/dist/execution/lines.d.ts.map +1 -1
- package/dist/execution/lines.js +26 -23
- package/dist/execution/lines.js.map +1 -1
- package/dist/execution/node-deps.d.ts.map +1 -1
- package/dist/execution/node-deps.js +14 -19
- package/dist/execution/node-deps.js.map +1 -1
- package/dist/execution/stream-turn.d.ts +1 -1
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +29 -5
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/execution/supervisor.d.ts +5 -0
- package/dist/execution/supervisor.d.ts.map +1 -1
- package/dist/execution/supervisor.js +26 -19
- package/dist/execution/supervisor.js.map +1 -1
- package/dist/interpretation/argv.d.ts +2 -0
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +26 -4
- package/dist/interpretation/argv.js.map +1 -1
- package/dist/interpretation/context-inspection.d.ts +41 -0
- package/dist/interpretation/context-inspection.d.ts.map +1 -0
- package/dist/interpretation/context-inspection.js +114 -0
- package/dist/interpretation/context-inspection.js.map +1 -0
- package/dist/interpretation/refusal.d.ts +1 -1
- package/dist/interpretation/refusal.d.ts.map +1 -1
- package/dist/interpretation/refusal.js.map +1 -1
- package/dist/interpretation/resolve-options.d.ts +1 -0
- package/dist/interpretation/resolve-options.d.ts.map +1 -1
- package/dist/interpretation/resolve-options.js +21 -28
- package/dist/interpretation/resolve-options.js.map +1 -1
- package/dist/interpretation/turn-options.d.ts +1 -1
- package/dist/interpretation/turn-options.d.ts.map +1 -1
- package/dist/interpretation/turn-options.js +9 -1
- package/dist/interpretation/turn-options.js.map +1 -1
- package/dist/knowledge/claude-code.d.ts +1 -1
- package/dist/knowledge/claude-code.d.ts.map +1 -1
- package/dist/knowledge/claude-code.js +22 -7
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +1 -0
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +12 -0
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/muse.d.ts.map +1 -1
- package/dist/knowledge/muse.js +1 -0
- package/dist/knowledge/muse.js.map +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +1 -0
- package/dist/knowledge/pi.js.map +1 -1
- package/package.json +2 -2
- package/src/cli/args.ts +4 -0
- package/src/cli/check.ts +56 -11
- package/src/cli/help.ts +23 -1
- package/src/cli/inspect-context.ts +96 -0
- package/src/cli/inspect-session.ts +85 -0
- package/src/cli/inspect.ts +65 -28
- package/src/cli/plan-turn.ts +2 -2
- package/src/cli/runtime-compatibility.ts +55 -0
- package/src/execution/context-inspection.ts +177 -0
- package/src/execution/deps.ts +3 -0
- package/src/execution/environment.ts +15 -0
- package/src/execution/lines.ts +22 -23
- package/src/execution/node-deps.ts +14 -17
- package/src/execution/stream-turn.ts +31 -4
- package/src/execution/supervisor.ts +27 -17
- package/src/interpretation/argv.ts +24 -4
- package/src/interpretation/context-inspection.ts +169 -0
- package/src/interpretation/refusal.ts +1 -0
- package/src/interpretation/resolve-options.ts +29 -30
- package/src/interpretation/turn-options.ts +8 -0
- package/src/knowledge/claude-code.ts +23 -7
- package/src/knowledge/codex.ts +1 -0
- package/src/knowledge/descriptor.ts +9 -0
- package/src/knowledge/muse.ts +1 -0
- 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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
package/src/knowledge/codex.ts
CHANGED
|
@@ -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;
|
package/src/knowledge/muse.ts
CHANGED