@dungle-scrubs/harness-cli-normalizer 0.5.4 → 0.5.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.
- package/README.md +3 -2
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +16 -8
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +11 -4
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/help.d.ts +3 -3
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +32 -23
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/inspect.d.ts.map +1 -1
- package/dist/cli/inspect.js +10 -7
- package/dist/cli/inspect.js.map +1 -1
- package/dist/cli/render.d.ts +15 -1
- package/dist/cli/render.d.ts.map +1 -1
- package/dist/cli/render.js +18 -2
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/resume-guard.js +29 -2
- package/dist/cli/resume-guard.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +35 -34
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/session-json.d.ts +5 -2
- package/dist/cli/session-json.d.ts.map +1 -1
- package/dist/cli/session-json.js +36 -5
- package/dist/cli/session-json.js.map +1 -1
- package/dist/cli/session.d.ts.map +1 -1
- package/dist/cli/session.js +103 -36
- package/dist/cli/session.js.map +1 -1
- package/dist/execution/events.d.ts +8 -1
- package/dist/execution/events.d.ts.map +1 -1
- package/dist/execution/events.js.map +1 -1
- package/dist/execution/open-session.d.ts +7 -6
- package/dist/execution/open-session.d.ts.map +1 -1
- package/dist/execution/open-session.js +88 -59
- package/dist/execution/open-session.js.map +1 -1
- package/dist/execution/stream-turn.d.ts +4 -7
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +43 -22
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/interpretation/argv.d.ts +9 -5
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +2 -7
- package/dist/interpretation/argv.js.map +1 -1
- package/dist/interpretation/capabilities.d.ts +20 -0
- package/dist/interpretation/capabilities.d.ts.map +1 -1
- package/dist/interpretation/capabilities.js +27 -0
- package/dist/interpretation/capabilities.js.map +1 -1
- package/dist/interpretation/hints.js +1 -1
- package/dist/interpretation/hints.js.map +1 -1
- package/dist/interpretation/question.d.ts +6 -2
- package/dist/interpretation/question.d.ts.map +1 -1
- package/dist/interpretation/question.js +13 -4
- package/dist/interpretation/question.js.map +1 -1
- package/dist/interpretation/refusal.d.ts +1 -1
- package/dist/interpretation/refusal.d.ts.map +1 -1
- package/dist/interpretation/refusal.js +1 -1
- package/dist/interpretation/refusal.js.map +1 -1
- package/dist/interpretation/skills-selection.d.ts +18 -1
- package/dist/interpretation/skills-selection.d.ts.map +1 -1
- package/dist/interpretation/skills-selection.js +33 -6
- package/dist/interpretation/skills-selection.js.map +1 -1
- package/dist/knowledge/claude-code.d.ts.map +1 -1
- package/dist/knowledge/claude-code.js +21 -0
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +16 -6
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +39 -12
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/descriptor.js +1 -0
- package/dist/knowledge/descriptor.js.map +1 -1
- package/dist/knowledge/muse.d.ts.map +1 -1
- package/dist/knowledge/muse.js +7 -0
- package/dist/knowledge/muse.js.map +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +23 -4
- package/dist/knowledge/pi.js.map +1 -1
- package/package.json +3 -2
- package/src/cli/args.ts +16 -6
- package/src/cli/config.ts +13 -4
- package/src/cli/help.ts +32 -23
- package/src/cli/inspect.ts +8 -6
- package/src/cli/render.ts +18 -1
- package/src/cli/resume-guard.ts +26 -2
- package/src/cli/run.ts +36 -38
- package/src/cli/session-json.ts +45 -7
- package/src/cli/session.ts +122 -40
- package/src/execution/events.ts +9 -1
- package/src/execution/open-session.ts +99 -72
- package/src/execution/stream-turn.ts +53 -30
- package/src/interpretation/argv.ts +11 -12
- package/src/interpretation/capabilities.ts +50 -0
- package/src/interpretation/hints.ts +1 -1
- package/src/interpretation/question.ts +17 -7
- package/src/interpretation/refusal.ts +2 -1
- package/src/interpretation/skills-selection.ts +46 -8
- package/src/knowledge/claude-code.ts +21 -0
- package/src/knowledge/codex.ts +16 -6
- package/src/knowledge/descriptor.ts +40 -12
- package/src/knowledge/muse.ts +7 -0
- package/src/knowledge/pi.ts +23 -4
|
@@ -22,14 +22,18 @@ import {
|
|
|
22
22
|
detectTransportInLine,
|
|
23
23
|
detectUnavailableInLine,
|
|
24
24
|
} from "../interpretation/limits.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
composeEscalatedPrompt,
|
|
27
|
+
detectQuestionBlock,
|
|
28
|
+
type QuestionMode,
|
|
29
|
+
} from "../interpretation/question.js";
|
|
26
30
|
import { ArgvRefusalError } from "../interpretation/refusal.js";
|
|
27
31
|
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
28
32
|
import { matcherOverridesOf } from "../knowledge/overrides.js";
|
|
29
33
|
import { AsyncChannel } from "./channel.js";
|
|
30
34
|
import { decodeLine, freshDecodeState } from "./decode.js";
|
|
31
35
|
import type { RunnerDeps, SpawnedProcess } from "./deps.js";
|
|
32
|
-
import type { ExitCause, HarnessEvent } from "./events.js";
|
|
36
|
+
import type { EscalationDetection, ExitCause, HarnessEvent } from "./events.js";
|
|
33
37
|
import type { FailureSummary } from "./failure.js";
|
|
34
38
|
import {
|
|
35
39
|
failureFromAuth,
|
|
@@ -111,13 +115,9 @@ export interface TurnRunOptions extends LaunchOptions {
|
|
|
111
115
|
* normalized argv. Wrong-harness flags here fail in the harness itself
|
|
112
116
|
* and surface as native errors - hcn never validates them. */
|
|
113
117
|
readonly passthrough?: readonly string[];
|
|
114
|
-
/**
|
|
115
|
-
*
|
|
116
|
-
|
|
117
|
-
* detection; false prepends the state-the-assumption instruction and
|
|
118
|
-
* disarms detection. Applies on launch AND resume: it shapes each
|
|
119
|
-
* turn's prompt and event stream, never a session setting. */
|
|
120
|
-
readonly escalateQuestions?: boolean;
|
|
118
|
+
/** question mode: which preamble to inject (ask/assume/none).
|
|
119
|
+
* Behavior instruction - never a harness flag. Defaults to "ask". */
|
|
120
|
+
readonly questions?: QuestionMode;
|
|
121
121
|
/** F-05: caller-requested stop. When aborted, the runner escalates
|
|
122
122
|
* SIGTERM then SIGKILL and classifies the exit as killed with no
|
|
123
123
|
* transport failure for the kill itself. */
|
|
@@ -132,20 +132,17 @@ export async function* streamTurn(
|
|
|
132
132
|
const turnId = deps.turnId ?? `turn-${++turnCounter}`;
|
|
133
133
|
const log = deps.log ?? (() => {});
|
|
134
134
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
// and arm detection in the true mode. Composition is idempotent, so a
|
|
138
|
-
// caller that already composed (the CLI does, for spawn-line truth)
|
|
139
|
-
// never double-prepends.
|
|
140
|
-
const escalateQuestions = opts.escalateQuestions !== false;
|
|
135
|
+
// compose the preamble onto the prompt based on question mode.
|
|
136
|
+
const questionMode: QuestionMode = opts.questions ?? "ask";
|
|
141
137
|
const effective: TurnRunOptions = {
|
|
142
138
|
...opts,
|
|
143
|
-
prompt: composeEscalatedPrompt(opts.prompt,
|
|
139
|
+
prompt: composeEscalatedPrompt(opts.prompt, questionMode),
|
|
144
140
|
};
|
|
145
141
|
// The turn's last assistant message - where the protocol says the
|
|
146
142
|
// hcn-question block lives. Tracked only when detection is armed.
|
|
147
143
|
let lastAssistantText: string | null = null;
|
|
148
144
|
let asked = false;
|
|
145
|
+
let escalationDetection: EscalationDetection = "none";
|
|
149
146
|
|
|
150
147
|
// Validate env before building argv so an invalid env is a refusal, not a spawn
|
|
151
148
|
if (opts.env !== undefined) {
|
|
@@ -172,7 +169,13 @@ export async function* streamTurn(
|
|
|
172
169
|
argv: redactArgv([], effective.prompt),
|
|
173
170
|
});
|
|
174
171
|
yield { kind: "failure", ...failure };
|
|
175
|
-
yield {
|
|
172
|
+
yield {
|
|
173
|
+
kind: "done",
|
|
174
|
+
exitCode: null,
|
|
175
|
+
cause: "failed",
|
|
176
|
+
failure,
|
|
177
|
+
escalation: { mode: questionMode, detection: "none" },
|
|
178
|
+
};
|
|
176
179
|
return;
|
|
177
180
|
}
|
|
178
181
|
}
|
|
@@ -188,12 +191,11 @@ export async function* streamTurn(
|
|
|
188
191
|
if (effective.passthrough !== undefined && effective.passthrough.length > 0) {
|
|
189
192
|
argv = [...argv, "--", ...effective.passthrough];
|
|
190
193
|
}
|
|
191
|
-
// issue #38: claude
|
|
192
|
-
// the argv tail (
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
argv = [...argv, ...claudeSkillTokens];
|
|
194
|
+
// issue #38: claude/codex render the skills allowlist as complement-off
|
|
195
|
+
// tokens at the argv tail (claude: settings JSON; codex: -c skills.config).
|
|
196
|
+
const skillTokens = (opts as unknown as { __skillTokens?: string[] }).__skillTokens;
|
|
197
|
+
if (skillTokens !== undefined && skillTokens.length > 0) {
|
|
198
|
+
argv = [...argv, ...skillTokens];
|
|
197
199
|
}
|
|
198
200
|
granularity = streamingGranularityOf(h, argv);
|
|
199
201
|
} catch (e) {
|
|
@@ -223,7 +225,13 @@ export async function* streamTurn(
|
|
|
223
225
|
argv: argvForLog,
|
|
224
226
|
});
|
|
225
227
|
yield { kind: "failure", ...failure };
|
|
226
|
-
yield {
|
|
228
|
+
yield {
|
|
229
|
+
kind: "done",
|
|
230
|
+
exitCode: null,
|
|
231
|
+
cause: "failed",
|
|
232
|
+
failure,
|
|
233
|
+
escalation: { mode: questionMode, detection: "none" },
|
|
234
|
+
};
|
|
227
235
|
return;
|
|
228
236
|
}
|
|
229
237
|
throw e;
|
|
@@ -271,7 +279,13 @@ export async function* streamTurn(
|
|
|
271
279
|
if (resumeCreateWarning !== null) yield { kind: "error", message: resumeCreateWarning };
|
|
272
280
|
yield { kind: "error", message: `spawn failed: ${message}` };
|
|
273
281
|
yield { kind: "failure", ...failure };
|
|
274
|
-
yield {
|
|
282
|
+
yield {
|
|
283
|
+
kind: "done",
|
|
284
|
+
exitCode: 127,
|
|
285
|
+
cause: "failed",
|
|
286
|
+
failure,
|
|
287
|
+
escalation: { mode: questionMode, detection: "none" },
|
|
288
|
+
};
|
|
275
289
|
return;
|
|
276
290
|
}
|
|
277
291
|
|
|
@@ -439,7 +453,7 @@ export async function* streamTurn(
|
|
|
439
453
|
if (event.terminal === true) await pushFailure(failureFromTerminalError(h, event.message));
|
|
440
454
|
return;
|
|
441
455
|
}
|
|
442
|
-
if (
|
|
456
|
+
if (questionMode === "ask" && event.kind === "message" && event.role === "assistant") {
|
|
443
457
|
lastAssistantText = event.text;
|
|
444
458
|
}
|
|
445
459
|
await queue.push(event);
|
|
@@ -469,18 +483,26 @@ export async function* streamTurn(
|
|
|
469
483
|
/** issue #41: scan the last assistant message for the hcn-question
|
|
470
484
|
* block. Structured-first - the block's fields become the event; no
|
|
471
485
|
* prose parsing. Runs after the pumps settle (the last message is only
|
|
472
|
-
* last then) and only when detection is armed (
|
|
473
|
-
*
|
|
486
|
+
* last then) and only when detection is armed (questions ask).
|
|
487
|
+
* A malformed block surfaces as an error event, never a silent
|
|
474
488
|
* no-op. */
|
|
475
489
|
const emitQuestionIfAsked = async (): Promise<void> => {
|
|
476
|
-
if (
|
|
490
|
+
if (questionMode !== "ask" || lastAssistantText === null) {
|
|
491
|
+
escalationDetection = "none";
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
477
494
|
const detection = detectQuestionBlock(lastAssistantText);
|
|
478
|
-
if (detection === null)
|
|
495
|
+
if (detection === null) {
|
|
496
|
+
escalationDetection = "none";
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
479
499
|
if ("malformed" in detection) {
|
|
500
|
+
escalationDetection = "malformed";
|
|
480
501
|
await queue.push({ kind: "error", message: detection.malformed });
|
|
481
502
|
await pushFailure(failureFromTask(`malformed hcn-question block: ${detection.malformed}`));
|
|
482
503
|
return;
|
|
483
504
|
}
|
|
505
|
+
escalationDetection = "block";
|
|
484
506
|
log({
|
|
485
507
|
event: "question",
|
|
486
508
|
turnId,
|
|
@@ -680,6 +702,7 @@ export async function* streamTurn(
|
|
|
680
702
|
exitCode: nativeReduced ? null : exitCode,
|
|
681
703
|
cause,
|
|
682
704
|
...(reduced ? { failure: reduced } : {}),
|
|
705
|
+
escalation: { mode: questionMode, detection: escalationDetection },
|
|
683
706
|
};
|
|
684
707
|
} finally {
|
|
685
708
|
if (abortHandler !== null) opts.signal?.removeEventListener("abort", abortHandler);
|
|
@@ -76,11 +76,11 @@ export interface TurnOptions {
|
|
|
76
76
|
readonly systemPrompt?: string;
|
|
77
77
|
/** issue #48: appends to the built-in prompt (claude/pi only). */
|
|
78
78
|
readonly appendSystemPrompt?: string;
|
|
79
|
-
/**
|
|
80
|
-
* option. It never renders into any
|
|
81
|
-
* it into the prompt preamble and
|
|
82
|
-
* Undefined means the default:
|
|
83
|
-
readonly
|
|
79
|
+
/** question mode: which preamble hcn injects (ask/assume/none).
|
|
80
|
+
* A BEHAVIOR INSTRUCTION, not a turn option. It never renders into any
|
|
81
|
+
* harness argv; the CLI layer turns it into the prompt preamble and
|
|
82
|
+
* arms question-block detection. Undefined means the default: "ask". */
|
|
83
|
+
readonly questions?: import("./question.js").QuestionMode;
|
|
84
84
|
/** Internal: set by CLI when prompt came from --prompt/--prompt-file to bypass leading '-' guard */
|
|
85
85
|
readonly __explicitPrompt?: boolean;
|
|
86
86
|
/** toolMap extensible vocabulary per harness (issue toolMap) */
|
|
@@ -192,6 +192,10 @@ export interface SessionOptions {
|
|
|
192
192
|
/** Provider selector (pi). A harness with no provider selector refuses,
|
|
193
193
|
* the same way a one-shot turn does. */
|
|
194
194
|
readonly provider?: string;
|
|
195
|
+
/** True when this argv should resume an existing conversation, false for a
|
|
196
|
+
* fresh session. Controls which descriptor flag is rendered: resumeFlag
|
|
197
|
+
* vs idFlag. Only consumers that alias --resume/--session-id set this. */
|
|
198
|
+
readonly isResume?: boolean;
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
export const buildSessionArgv = (h: HarnessDescriptor, opts: SessionOptions): string[] => {
|
|
@@ -206,13 +210,8 @@ export const buildSessionArgv = (h: HarnessDescriptor, opts: SessionOptions): st
|
|
|
206
210
|
});
|
|
207
211
|
}
|
|
208
212
|
refuseUnusableSessionId(h, opts.sessionId);
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
...h.sessionMode.flags,
|
|
212
|
-
// idFlag null = the harness refuses unknown ids and mints its own
|
|
213
|
-
// (pi rpc); the caller-side sessionId stays a correlation handle.
|
|
214
|
-
...(h.sessionMode.idFlag !== null ? [h.sessionMode.idFlag, opts.sessionId] : []),
|
|
215
|
-
];
|
|
213
|
+
const flag = opts.isResume ? h.sessionMode.resumeFlag : h.sessionMode.idFlag;
|
|
214
|
+
const argv = [h.bin, ...h.sessionMode.flags, ...(flag !== null ? [flag, opts.sessionId] : [])];
|
|
216
215
|
if (opts.model !== undefined) {
|
|
217
216
|
const validated = validateModel(h, opts.model);
|
|
218
217
|
if (!validated.ok) {
|
|
@@ -11,8 +11,30 @@ import type {
|
|
|
11
11
|
HarnessMode,
|
|
12
12
|
StreamingGranularity,
|
|
13
13
|
} from "../knowledge/descriptor.js";
|
|
14
|
+
import { compareVersions } from "./versions.js";
|
|
14
15
|
import { resolveModel } from "./vocabulary.js";
|
|
15
16
|
|
|
17
|
+
export interface EscalationObservedOn {
|
|
18
|
+
readonly harness: string;
|
|
19
|
+
readonly model: string;
|
|
20
|
+
readonly version: string;
|
|
21
|
+
readonly date: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Escalation claim: whether this harness and model were observed to emit
|
|
26
|
+
* the structured hcn-question block when instructed, NOT "this harness can
|
|
27
|
+
* ask" - live probes show models ask unprompted, so capability-to-ask would
|
|
28
|
+
* be false.
|
|
29
|
+
*/
|
|
30
|
+
export interface EscalationClaim {
|
|
31
|
+
/** True when this harness and model were observed to emit the structured block when instructed. */
|
|
32
|
+
readonly supported: boolean;
|
|
33
|
+
readonly source: "runtime-verified" | "curated" | "unknown";
|
|
34
|
+
readonly confidence: "high" | "medium" | "none";
|
|
35
|
+
readonly observedOn?: EscalationObservedOn;
|
|
36
|
+
}
|
|
37
|
+
|
|
16
38
|
export interface CapabilityResult {
|
|
17
39
|
readonly vision: boolean;
|
|
18
40
|
readonly images: boolean;
|
|
@@ -20,8 +42,34 @@ export interface CapabilityResult {
|
|
|
20
42
|
readonly session: boolean;
|
|
21
43
|
readonly source: "runtime-verified" | "curated" | "unknown";
|
|
22
44
|
readonly confidence: "high" | "medium" | "none";
|
|
45
|
+
readonly escalation: EscalationClaim;
|
|
23
46
|
}
|
|
24
47
|
|
|
48
|
+
const escalationOf = (h: HarnessDescriptor): EscalationClaim => {
|
|
49
|
+
const obs = h.escalation.observedOn;
|
|
50
|
+
if (obs !== undefined) {
|
|
51
|
+
const cmp = compareVersions(obs.version, h.verifiedAgainst);
|
|
52
|
+
if (cmp < 0) {
|
|
53
|
+
return {
|
|
54
|
+
supported: true,
|
|
55
|
+
source: "runtime-verified",
|
|
56
|
+
confidence: "medium",
|
|
57
|
+
observedOn: obs,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
supported: true,
|
|
62
|
+
source: "runtime-verified",
|
|
63
|
+
confidence: "high",
|
|
64
|
+
observedOn: obs,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (h.escalation.supported) {
|
|
68
|
+
return { supported: true, source: "curated", confidence: "medium" };
|
|
69
|
+
}
|
|
70
|
+
return { supported: false, source: "unknown", confidence: "none" };
|
|
71
|
+
};
|
|
72
|
+
|
|
25
73
|
export const capabilitiesOf = (
|
|
26
74
|
h: HarnessDescriptor,
|
|
27
75
|
model: string,
|
|
@@ -41,6 +89,7 @@ export const capabilitiesOf = (
|
|
|
41
89
|
session: false,
|
|
42
90
|
source: "unknown",
|
|
43
91
|
confidence: "none",
|
|
92
|
+
escalation: { supported: false, source: "unknown", confidence: "none" },
|
|
44
93
|
};
|
|
45
94
|
}
|
|
46
95
|
return {
|
|
@@ -50,5 +99,6 @@ export const capabilitiesOf = (
|
|
|
50
99
|
session: h.capabilities.session,
|
|
51
100
|
source: "curated",
|
|
52
101
|
confidence: "medium",
|
|
102
|
+
escalation: escalationOf(h),
|
|
53
103
|
};
|
|
54
104
|
};
|
|
@@ -49,7 +49,7 @@ const HINTS: Readonly<Record<string, Readonly<Record<string, string>>>> = deepFr
|
|
|
49
49
|
"discovery.extensions":
|
|
50
50
|
"codex loads MCP servers and plugins from config; disable per-server with -c or codex mcp remove rather than a call-time toggle",
|
|
51
51
|
"discovery.skills":
|
|
52
|
-
|
|
52
|
+
'codex has no global skills-off switch (skills.enabled is not a key); disable per skill per call with -c skills.config=[{path=".../SKILL.md", enabled=false}] and bundled skills with -c skills.bundled.enabled=false',
|
|
53
53
|
},
|
|
54
54
|
pi: {
|
|
55
55
|
sandbox:
|
|
@@ -47,7 +47,15 @@ If and only if a genuine decision you cannot make defensibly blocks correct prog
|
|
|
47
47
|
|
|
48
48
|
Say nothing after the block and stop generating. The caller's user will answer, and the answer arrives as the next user message in this session - continue from it. For every choice you can make defensibly yourself, do not ask - decide, act, and state the decision you made.`;
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
export type QuestionMode = "ask" | "assume" | "none";
|
|
51
|
+
|
|
52
|
+
export const QUESTION_MODES: readonly QuestionMode[] = ["ask", "assume", "none"] as const;
|
|
53
|
+
|
|
54
|
+
export const isQuestionMode = (value: string): value is QuestionMode =>
|
|
55
|
+
(QUESTION_MODES as readonly string[]).includes(value);
|
|
56
|
+
|
|
57
|
+
/** Compose the transport preamble onto a prompt. `mode` selects which
|
|
58
|
+
* preamble to inject (ask/assume/none), and `channel` selects the
|
|
51
59
|
* contract wording: "turn" (exit-and-resume transport, hcn run) or
|
|
52
60
|
* "session" (live channel, hcn session). Idempotent: a prompt that
|
|
53
61
|
* already carries any preamble passes through unchanged - a turn-mode
|
|
@@ -55,12 +63,14 @@ Say nothing after the block and stop generating. The caller's user will answer,
|
|
|
55
63
|
* both; the already-composed contract stands). */
|
|
56
64
|
export const composeEscalatedPrompt = (
|
|
57
65
|
prompt: string,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
): string =>
|
|
61
|
-
prompt.startsWith(QUESTION_PREAMBLE_MARKER)
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
mode: QuestionMode,
|
|
67
|
+
channel: "turn" | "session" = "turn",
|
|
68
|
+
): string => {
|
|
69
|
+
if (prompt.startsWith(QUESTION_PREAMBLE_MARKER)) return prompt;
|
|
70
|
+
if (mode === "none") return prompt;
|
|
71
|
+
if (mode === "assume") return `${NO_ESCALATION_PREAMBLE}\n\n${prompt}`;
|
|
72
|
+
return `${channel === "session" ? SESSION_ESCALATION_PREAMBLE : ESCALATION_PREAMBLE}\n\n${prompt}`;
|
|
73
|
+
};
|
|
64
74
|
|
|
65
75
|
/** The structured question a worker asks (the block's fields). */
|
|
66
76
|
export interface QuestionBlock {
|
|
@@ -35,6 +35,7 @@ export type RefusalOption =
|
|
|
35
35
|
| "excludeTools"
|
|
36
36
|
| "skills"
|
|
37
37
|
| "autonomy"
|
|
38
|
+
| "questions"
|
|
38
39
|
| `discovery.${string}`;
|
|
39
40
|
|
|
40
41
|
/** One helper builds the message from the structured fields so message and
|
|
@@ -89,7 +90,7 @@ export const buildRefusalMessage = (
|
|
|
89
90
|
case "no-autonomy-mode":
|
|
90
91
|
return `${harness} has no unattended-run flag; ${supportedStr} - drop autonomy or route to a supporting harness (claude --dangerously-skip-permissions, codex/muse --yolo)`;
|
|
91
92
|
case "no-session-mode":
|
|
92
|
-
return `${harness} declares no persistent headless session mode; ${supportedStr} - use
|
|
93
|
+
return `${harness} declares no persistent headless session mode; ${supportedStr} - use hcn run --resume <id>`;
|
|
93
94
|
default: {
|
|
94
95
|
const exhaustive: never = issue;
|
|
95
96
|
return `${exhaustive as string} for ${harness}${optionPart}${detailSuffix}; ${supportedStr}`;
|
|
@@ -10,7 +10,15 @@
|
|
|
10
10
|
* OFF - `--settings '{"skillOverrides":{"<name>":"off",...}}'` for every
|
|
11
11
|
* known skill except the picks. Known set comes from the caller's root
|
|
12
12
|
* listing (same source that resolved the names).
|
|
13
|
-
* - codex
|
|
13
|
+
* - codex: per-skill disable via config-kv array `-c
|
|
14
|
+
* skills.config=[{path="...", enabled=false}]` for every known skill
|
|
15
|
+
* except the picks (complement-off, same inversion as claude). Uses
|
|
16
|
+
* `path` selector rather than `name` because a skill's frontmatter
|
|
17
|
+
* `name` need not equal its directory basename, and `path` is exact.
|
|
18
|
+
* The path for skill <n> under root <root> is <root>/<n>/SKILL.md.
|
|
19
|
+
* No global `skills.enabled` switch exists. Requires the known set
|
|
20
|
+
* and the resolved picks (root derived from picks via dirname).
|
|
21
|
+
* - muse: refuse (structural) with the standard hint shape.
|
|
14
22
|
*/
|
|
15
23
|
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
16
24
|
import { ArgvRefusalError } from "./refusal.js";
|
|
@@ -20,6 +28,11 @@ export const basenameOf = (p: string): string => {
|
|
|
20
28
|
return i === -1 ? p : p.slice(i + 1);
|
|
21
29
|
};
|
|
22
30
|
|
|
31
|
+
const dirnameOf = (p: string): string => {
|
|
32
|
+
const i = p.lastIndexOf("/");
|
|
33
|
+
return i === -1 ? "" : p.slice(0, i);
|
|
34
|
+
};
|
|
35
|
+
|
|
23
36
|
export const renderSkillsSelection = (
|
|
24
37
|
h: HarnessDescriptor,
|
|
25
38
|
skills: readonly string[],
|
|
@@ -35,11 +48,9 @@ export const renderSkillsSelection = (
|
|
|
35
48
|
supportedBy: [
|
|
36
49
|
{ harness: "pi", spelling: "--skill" },
|
|
37
50
|
{ harness: "claude", spelling: "skillOverrides" },
|
|
51
|
+
{ harness: "codex", spelling: "-c skills.config" },
|
|
38
52
|
],
|
|
39
|
-
hint:
|
|
40
|
-
h.name === "codex"
|
|
41
|
-
? "codex discovers skills from its own directory with no call-time surface - stage the skills into $CODEX_HOME/skills or pass their content in the prompt"
|
|
42
|
-
: "muse scopes skills by workspace trust with no per-skill surface - include the skill content in the prompt or use --trust-workspace for the whole registry",
|
|
53
|
+
hint: "muse scopes skills by workspace trust with no per-skill surface - include the skill content in the prompt or use --trust-workspace for the whole registry",
|
|
43
54
|
});
|
|
44
55
|
}
|
|
45
56
|
|
|
@@ -50,9 +61,9 @@ export const renderSkillsSelection = (
|
|
|
50
61
|
return tokens;
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
// claude: complement-off via settings JSON
|
|
54
|
-
//
|
|
55
|
-
//
|
|
64
|
+
// claude and codex: complement-off via CLI layer (settings JSON / config
|
|
65
|
+
// array). The descriptor-level render returns [] and tokens append in
|
|
66
|
+
// stream-turn / CLI.
|
|
56
67
|
return [];
|
|
57
68
|
};
|
|
58
69
|
|
|
@@ -71,3 +82,30 @@ export const claudeSkillOverridesArg = (
|
|
|
71
82
|
const json = JSON.stringify({ skillOverrides: offs });
|
|
72
83
|
return ["--settings", json];
|
|
73
84
|
};
|
|
85
|
+
|
|
86
|
+
/** Codex complement form: every known skill except the picks gets
|
|
87
|
+
* `{path="<root>/<name>/SKILL.md>", enabled=false}` via `-c
|
|
88
|
+
* skills.config=[...]`. Uses `path` (exact) over `name` because
|
|
89
|
+
* frontmatter name may diverge from directory basename. Root is derived
|
|
90
|
+
* from the picks' dirname (all picks share the same root); if picks is
|
|
91
|
+
* empty the complement cannot be rooted and we return [] (caller picks
|
|
92
|
+
* nothing - no integration point needs this, and the CLI layer never
|
|
93
|
+
* calls with empty picks). Empty complement returns [] (no flag). */
|
|
94
|
+
export const codexSkillConfigArg = (
|
|
95
|
+
knownSkills: readonly string[],
|
|
96
|
+
pickedPaths: readonly string[],
|
|
97
|
+
): string[] => {
|
|
98
|
+
const picks = new Set(pickedPaths.map(basenameOf));
|
|
99
|
+
// Derive root from first pick's dirname; all picks are under same root
|
|
100
|
+
// (skills-root guarantees this). Fall back to "" if no picks.
|
|
101
|
+
const root = pickedPaths.length > 0 ? dirnameOf(pickedPaths[0] as string) : "";
|
|
102
|
+
const entries: string[] = [];
|
|
103
|
+
for (const name of knownSkills) {
|
|
104
|
+
if (!picks.has(name)) {
|
|
105
|
+
const absPath = root ? `${root}/${name}/SKILL.md` : `${name}/SKILL.md`;
|
|
106
|
+
entries.push(`{path=${JSON.stringify(absPath)}, enabled=false}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (entries.length === 0) return [];
|
|
110
|
+
return ["-c", `skills.config=[${entries.join(", ")}]`];
|
|
111
|
+
};
|
|
@@ -51,6 +51,19 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
|
|
|
51
51
|
// hooks (D-025). Token deltas require this exact output flag set.
|
|
52
52
|
// sessionMode.flags is the complete flag list after the binary, so -p
|
|
53
53
|
// lives here rather than being inherited from launch.baseFlags.
|
|
54
|
+
// claude has two session-id flags with opposite unknown-id behaviour:
|
|
55
|
+
// `--session-id` NAMES a fresh session (creates if missing, silent) while
|
|
56
|
+
// `--resume` RESTORES an existing one (refuses unknown id, exit 1
|
|
57
|
+
// "No conversation found" - verified phase11,
|
|
58
|
+
// test/fixtures/phase11-claude-session-resume/resume-unknown-id.stderr.txt).
|
|
59
|
+
// `idFlag` is the naming flag (`--session-id`); `resumeFlag` is the
|
|
60
|
+
// restoring flag (`--resume`) - distinct as data so the renderer cannot
|
|
61
|
+
// pick the wrong one. Unknown-id behaviour reuses `resume.onMissing:
|
|
62
|
+
// "error"` as the single source; sessionMode does not restate it.
|
|
63
|
+
// Verified phase11 (test/fixtures/phase11-claude-session-resume):
|
|
64
|
+
// `session-id-establish.ndjson` creates with `--session-id`, and
|
|
65
|
+
// `resume-restores.ndjson` restores the same id with `--resume` and
|
|
66
|
+
// recalls codeword "pomegranate".
|
|
54
67
|
flags: [
|
|
55
68
|
"-p",
|
|
56
69
|
"--input-format",
|
|
@@ -63,6 +76,7 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
|
|
|
63
76
|
"project",
|
|
64
77
|
],
|
|
65
78
|
idFlag: "--session-id",
|
|
79
|
+
resumeFlag: "--resume",
|
|
66
80
|
input: { kind: "claude-sdk-user-message" },
|
|
67
81
|
turnEnd: { type: "result" },
|
|
68
82
|
identityProbe: null,
|
|
@@ -140,6 +154,13 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
|
|
|
140
154
|
},
|
|
141
155
|
session: true,
|
|
142
156
|
},
|
|
157
|
+
// Escalation provenance transcribed from test/fixtures/phase7-questions/,
|
|
158
|
+
// committed 2026-08-19. `model` is empty because no fixture on that stream
|
|
159
|
+
// records a model id - absence of evidence, not an unset field.
|
|
160
|
+
escalation: {
|
|
161
|
+
supported: true,
|
|
162
|
+
observedOn: { harness: "claude", model: "", version: "2.1.235", date: "2026-08-19" },
|
|
163
|
+
},
|
|
143
164
|
turnOptions: {
|
|
144
165
|
effort: { kind: "effort", render: { kind: "flag-value", flag: "--effort" } },
|
|
145
166
|
// issue #48, live-verified 2.1.235: --system-prompt replaces the built-in
|
package/src/knowledge/codex.ts
CHANGED
|
@@ -101,6 +101,13 @@ export const codexCli: HarnessDescriptor = deepFreeze({
|
|
|
101
101
|
},
|
|
102
102
|
session: false,
|
|
103
103
|
},
|
|
104
|
+
// Escalation provenance transcribed from test/fixtures/phase7-questions/,
|
|
105
|
+
// committed 2026-08-19. `model` is empty because no fixture on that stream
|
|
106
|
+
// records a model id - absence of evidence, not an unset field.
|
|
107
|
+
escalation: {
|
|
108
|
+
supported: true,
|
|
109
|
+
observedOn: { harness: "codex", model: "", version: "0.146.1", date: "2026-08-19" },
|
|
110
|
+
},
|
|
104
111
|
turnOptions: {
|
|
105
112
|
effort: {
|
|
106
113
|
kind: "effort",
|
|
@@ -129,12 +136,15 @@ export const codexCli: HarnessDescriptor = deepFreeze({
|
|
|
129
136
|
render: { kind: "flag-value", flag: "--sandbox" },
|
|
130
137
|
},
|
|
131
138
|
},
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
// (
|
|
137
|
-
skills
|
|
139
|
+
// Tools: no built-in name lists; control is feature booleans
|
|
140
|
+
// (reachable per-call via -c key=value), sandbox, and approval policy.
|
|
141
|
+
// MCP servers do have per-tool keys (mcp_servers.<id>.tools.<tool>)
|
|
142
|
+
// but built-ins do not.
|
|
143
|
+
// Skills (verified 2026-08-22, codex 0.147.0, --strict-config probes):
|
|
144
|
+
// per-skill disable via config-kv array -c skills.config=[{path=...,
|
|
145
|
+
// enabled=false}] (selector path or name, validated per entry); also
|
|
146
|
+
// -c skills.bundled.enabled=false. No global skills.enabled switch.
|
|
147
|
+
skills: { loadFlag: null, overridesVia: "config-skills-array" },
|
|
138
148
|
tools: {
|
|
139
149
|
includeFlag: null,
|
|
140
150
|
excludeFlag: null,
|
|
@@ -52,6 +52,9 @@ export interface SessionInputContract {
|
|
|
52
52
|
readonly kind: SessionInputKind;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export const SESSION_RESUME_FLAGS = ["--resume", "--session-id"] as const;
|
|
56
|
+
export type SessionResumeFlag = (typeof SESSION_RESUME_FLAGS)[number];
|
|
57
|
+
|
|
55
58
|
/** Consumers branch on these (session-limit: wait for reset; weekly-limit:
|
|
56
59
|
* route elsewhere), so the vocabulary is closed - a descriptor cannot invent
|
|
57
60
|
* a code a consumer has no arm for. */
|
|
@@ -302,19 +305,26 @@ export interface HarnessDescriptor {
|
|
|
302
305
|
* the binary to open one lucid-owned process serving many turns, or null
|
|
303
306
|
* when the harness has no such mode. This is the entire argv prefix after
|
|
304
307
|
* the binary - no launch flags are prepended. `idFlag` pins the
|
|
305
|
-
* caller-assigned session identity
|
|
306
|
-
* delimits one turn (claude: the `result`
|
|
307
|
-
* `agent_settled`). `identityProbe`, when present, names
|
|
308
|
-
* runner writes at spawn whose response carries the session
|
|
309
|
-
* is identity-silent at startup (spike evidence:
|
|
308
|
+
* caller-assigned session identity for a FRESH session (naming). `turnEnd`
|
|
309
|
+
* is the stdout record that delimits one turn (claude: the `result`
|
|
310
|
+
* record; pi rpc: `agent_settled`). `identityProbe`, when present, names
|
|
311
|
+
* a command the runner writes at spawn whose response carries the session
|
|
312
|
+
* id - pi rpc is identity-silent at startup (spike evidence:
|
|
310
313
|
* test/fixtures/pi-rpc-spike), so identity needs a round trip. */
|
|
311
314
|
readonly sessionMode: {
|
|
312
315
|
readonly flags: readonly string[];
|
|
313
|
-
/**
|
|
314
|
-
*
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
/** The flag that names a fresh session (caller-assigned identity at
|
|
317
|
+
* spawn). Null only when the harness mints the id itself. */
|
|
318
|
+
readonly idFlag: SessionResumeFlag | null;
|
|
319
|
+
/** The flag that carries an EXISTING session id to resume it in
|
|
320
|
+
* session mode, distinct from `idFlag` which names a fresh session.
|
|
321
|
+
* Closed vocabulary: pi reuses `--session-id` for both (verified
|
|
322
|
+
* phase10, test/fixtures/phase10-pi-rpc-resume); claude uses
|
|
323
|
+
* `--resume` to restore vs `--session-id` to name (verified phase11,
|
|
324
|
+
* test/fixtures/phase11-claude-session-resume). Unknown-id behavior
|
|
325
|
+
* is not duplicated here; it reuses `resume.onMissing` as the single
|
|
326
|
+
* source. */
|
|
327
|
+
readonly resumeFlag: SessionResumeFlag;
|
|
318
328
|
readonly input: SessionInputContract;
|
|
319
329
|
readonly turnEnd: Readonly<Record<string, string>>;
|
|
320
330
|
readonly identityProbe: { readonly command: string } | null;
|
|
@@ -407,6 +417,23 @@ export interface HarnessDescriptor {
|
|
|
407
417
|
readonly streamingByMode: Readonly<Record<HarnessMode, StreamingGranularity>>;
|
|
408
418
|
readonly session: boolean;
|
|
409
419
|
};
|
|
420
|
+
/** Escalation provenance: whether this harness and model were observed to
|
|
421
|
+
* emit the structured hcn-question block when instructed, and when that
|
|
422
|
+
* observation was made. `supported` is documented as "this harness and
|
|
423
|
+
* model were observed to emit the structured block when instructed", NOT
|
|
424
|
+
* as "this harness can ask" - live probes show models ask unprompted, so
|
|
425
|
+
* a capability-to-ask name would be false. `observedOn` is optional;
|
|
426
|
+
* absence means no probe exists for this harness/model. Staleness is
|
|
427
|
+
* derived from version comparison at read time, not stored. */
|
|
428
|
+
readonly escalation: {
|
|
429
|
+
readonly supported: boolean;
|
|
430
|
+
readonly observedOn?: {
|
|
431
|
+
readonly harness: string;
|
|
432
|
+
readonly model: string;
|
|
433
|
+
readonly version: string;
|
|
434
|
+
readonly date: string;
|
|
435
|
+
};
|
|
436
|
+
};
|
|
410
437
|
/** Per-call turn options this harness can express, keyed by the closed
|
|
411
438
|
* `TurnOptionKey` vocabulary. Absent keys are unexpressible on this
|
|
412
439
|
* harness; a call passing them must refuse. Discovery is a table of
|
|
@@ -430,10 +457,11 @@ export interface HarnessDescriptor {
|
|
|
430
457
|
* pi model registry, D-008). */
|
|
431
458
|
/** Caller-directed skills allowlist surface (issue #38). pi: repeatable
|
|
432
459
|
* load flag. claude: per-name "off" overrides via settings JSON.
|
|
433
|
-
* codex
|
|
460
|
+
* codex: per-skill "off" via config-kv array (-c skills.config).
|
|
461
|
+
* muse: null (structural gap - trust/config scoped only). */
|
|
434
462
|
readonly skills: {
|
|
435
463
|
readonly loadFlag: string | null;
|
|
436
|
-
readonly overridesVia: "settings-skilloverrides" | null;
|
|
464
|
+
readonly overridesVia: "settings-skilloverrides" | "config-skills-array" | null;
|
|
437
465
|
} | null;
|
|
438
466
|
readonly tools: {
|
|
439
467
|
readonly includeFlag: string | null;
|
package/src/knowledge/muse.ts
CHANGED
|
@@ -91,6 +91,13 @@ export const museCode: HarnessDescriptor = deepFreeze({
|
|
|
91
91
|
},
|
|
92
92
|
session: false,
|
|
93
93
|
},
|
|
94
|
+
// Escalation provenance transcribed from test/fixtures/phase7-questions/,
|
|
95
|
+
// committed 2026-08-19. `model` is empty because no fixture on that stream
|
|
96
|
+
// records a model id - absence of evidence, not an unset field.
|
|
97
|
+
escalation: {
|
|
98
|
+
supported: true,
|
|
99
|
+
observedOn: { harness: "muse", model: "", version: "0.2.1", date: "2026-08-19" },
|
|
100
|
+
},
|
|
94
101
|
turnOptions: {
|
|
95
102
|
effort: { kind: "effort", render: { kind: "flag-value", flag: "--reasoning-effort" } },
|
|
96
103
|
write: {
|