@dungle-scrubs/harness-cli-normalizer 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -6
- package/dist/cli/args.d.ts +9 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +36 -26
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/check.d.ts +14 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +10 -3
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/config.d.ts +4 -4
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +8 -6
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/exit-codes.d.ts +0 -1
- package/dist/cli/exit-codes.d.ts.map +1 -1
- package/dist/cli/exit-codes.js +0 -1
- package/dist/cli/exit-codes.js.map +1 -1
- package/dist/cli/help.d.ts +2 -2
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +10 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/inspect.d.ts.map +1 -1
- package/dist/cli/inspect.js +84 -265
- package/dist/cli/inspect.js.map +1 -1
- package/dist/cli/plan-turn.d.ts +68 -0
- package/dist/cli/plan-turn.d.ts.map +1 -0
- package/dist/cli/plan-turn.js +268 -0
- package/dist/cli/plan-turn.js.map +1 -0
- package/dist/cli/refuse.d.ts.map +1 -1
- package/dist/cli/refuse.js +2 -1
- package/dist/cli/refuse.js.map +1 -1
- package/dist/cli/resolve-harness.d.ts.map +1 -1
- package/dist/cli/resolve-harness.js +2 -3
- package/dist/cli/resolve-harness.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +33 -367
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/session-json.d.ts +1 -1
- package/dist/cli/session-json.d.ts.map +1 -1
- package/dist/cli/session-json.js +1 -3
- package/dist/cli/session-json.js.map +1 -1
- package/dist/cli/session.d.ts.map +1 -1
- package/dist/cli/session.js +35 -43
- package/dist/cli/session.js.map +1 -1
- package/dist/cli/skills-root.d.ts +1 -1
- package/dist/cli/skills-root.d.ts.map +1 -1
- package/dist/cli/skills-root.js +1 -1
- package/dist/cli/skills-root.js.map +1 -1
- package/dist/execution/decode.d.ts.map +1 -1
- package/dist/execution/decode.js +11 -36
- package/dist/execution/decode.js.map +1 -1
- package/dist/execution/events.d.ts +4 -1
- package/dist/execution/events.d.ts.map +1 -1
- package/dist/execution/events.js.map +1 -1
- package/dist/execution/failure.d.ts +3 -1
- package/dist/execution/failure.d.ts.map +1 -1
- package/dist/execution/failure.js +14 -11
- package/dist/execution/failure.js.map +1 -1
- package/dist/execution/open-session.d.ts +1 -1
- package/dist/execution/open-session.d.ts.map +1 -1
- package/dist/execution/open-session.js +150 -223
- package/dist/execution/open-session.js.map +1 -1
- package/dist/execution/stream-turn.d.ts +9 -14
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +85 -188
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/execution/supervisor.d.ts +71 -0
- package/dist/execution/supervisor.d.ts.map +1 -0
- package/dist/execution/supervisor.js +127 -0
- package/dist/execution/supervisor.js.map +1 -0
- package/dist/interpretation/argv.d.ts +39 -12
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +28 -20
- package/dist/interpretation/argv.js.map +1 -1
- package/dist/interpretation/content.d.ts +10 -1
- package/dist/interpretation/content.d.ts.map +1 -1
- package/dist/interpretation/content.js +20 -11
- package/dist/interpretation/content.js.map +1 -1
- package/dist/interpretation/hints.d.ts +1 -1
- package/dist/interpretation/hints.d.ts.map +1 -1
- package/dist/interpretation/hints.js +10 -11
- package/dist/interpretation/hints.js.map +1 -1
- package/dist/interpretation/index.d.ts +0 -1
- package/dist/interpretation/index.d.ts.map +1 -1
- package/dist/interpretation/index.js +0 -1
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/isolation.d.ts +10 -0
- package/dist/interpretation/isolation.d.ts.map +1 -0
- package/dist/interpretation/isolation.js +30 -0
- package/dist/interpretation/isolation.js.map +1 -0
- package/dist/interpretation/limits.d.ts +2 -5
- package/dist/interpretation/limits.d.ts.map +1 -1
- package/dist/interpretation/limits.js +8 -62
- package/dist/interpretation/limits.js.map +1 -1
- package/dist/interpretation/question.d.ts +27 -0
- package/dist/interpretation/question.d.ts.map +1 -1
- package/dist/interpretation/question.js +28 -0
- package/dist/interpretation/question.js.map +1 -1
- package/dist/interpretation/refusal.d.ts +10 -5
- package/dist/interpretation/refusal.d.ts.map +1 -1
- package/dist/interpretation/refusal.js +24 -15
- package/dist/interpretation/refusal.js.map +1 -1
- package/dist/interpretation/resolve-options.d.ts +36 -2
- package/dist/interpretation/resolve-options.d.ts.map +1 -1
- package/dist/interpretation/resolve-options.js +53 -22
- package/dist/interpretation/resolve-options.js.map +1 -1
- package/dist/interpretation/session-input.d.ts +44 -1
- package/dist/interpretation/session-input.d.ts.map +1 -1
- package/dist/interpretation/session-input.js +59 -4
- package/dist/interpretation/session-input.js.map +1 -1
- package/dist/interpretation/shape.d.ts +1 -0
- package/dist/interpretation/shape.d.ts.map +1 -1
- package/dist/interpretation/shape.js +10 -0
- package/dist/interpretation/shape.js.map +1 -1
- package/dist/interpretation/skills-selection.d.ts +25 -33
- package/dist/interpretation/skills-selection.d.ts.map +1 -1
- package/dist/interpretation/skills-selection.js +41 -48
- package/dist/interpretation/skills-selection.js.map +1 -1
- package/dist/interpretation/support.d.ts.map +1 -1
- package/dist/interpretation/support.js +25 -8
- package/dist/interpretation/support.js.map +1 -1
- package/dist/interpretation/tool-selection.d.ts +7 -6
- package/dist/interpretation/tool-selection.d.ts.map +1 -1
- package/dist/interpretation/tool-selection.js +54 -116
- package/dist/interpretation/tool-selection.js.map +1 -1
- package/dist/interpretation/tool-vocabulary.d.ts +31 -12
- package/dist/interpretation/tool-vocabulary.d.ts.map +1 -1
- package/dist/interpretation/tool-vocabulary.js +19 -41
- package/dist/interpretation/tool-vocabulary.js.map +1 -1
- package/dist/interpretation/turn-options.d.ts +4 -0
- package/dist/interpretation/turn-options.d.ts.map +1 -1
- package/dist/interpretation/turn-options.js +107 -241
- package/dist/interpretation/turn-options.js.map +1 -1
- package/dist/interpretation/vocabulary.d.ts +8 -6
- package/dist/interpretation/vocabulary.d.ts.map +1 -1
- package/dist/interpretation/vocabulary.js +22 -9
- package/dist/interpretation/vocabulary.js.map +1 -1
- package/dist/knowledge/claude-code.d.ts +3 -1
- package/dist/knowledge/claude-code.d.ts.map +1 -1
- package/dist/knowledge/claude-code.js +17 -4
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +31 -7
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +67 -36
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/descriptor.js +50 -15
- package/dist/knowledge/descriptor.js.map +1 -1
- package/dist/knowledge/matchers.d.ts +12 -0
- package/dist/knowledge/matchers.d.ts.map +1 -1
- package/dist/knowledge/matchers.js +34 -0
- package/dist/knowledge/matchers.js.map +1 -1
- package/dist/knowledge/muse.d.ts.map +1 -1
- package/dist/knowledge/muse.js +13 -6
- package/dist/knowledge/muse.js.map +1 -1
- package/dist/knowledge/overrides.d.ts.map +1 -1
- package/dist/knowledge/overrides.js +9 -20
- package/dist/knowledge/overrides.js.map +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +8 -6
- package/dist/knowledge/pi.js.map +1 -1
- package/dist/knowledge/profile.d.ts +1 -0
- package/dist/knowledge/profile.d.ts.map +1 -1
- package/dist/knowledge/profile.js +1 -0
- package/dist/knowledge/profile.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/args.ts +39 -27
- package/src/cli/check.ts +10 -3
- package/src/cli/config.ts +14 -11
- package/src/cli/exit-codes.ts +0 -2
- package/src/cli/help.ts +10 -2
- package/src/cli/index.ts +3 -1
- package/src/cli/inspect.ts +88 -269
- package/src/cli/plan-turn.ts +350 -0
- package/src/cli/refuse.ts +2 -1
- package/src/cli/resolve-harness.ts +4 -5
- package/src/cli/run.ts +36 -399
- package/src/cli/session-json.ts +1 -6
- package/src/cli/session.ts +51 -44
- package/src/cli/skills-root.ts +1 -4
- package/src/execution/decode.ts +10 -36
- package/src/execution/events.ts +4 -1
- package/src/execution/failure.ts +18 -11
- package/src/execution/open-session.ts +154 -228
- package/src/execution/stream-turn.ts +90 -193
- package/src/execution/supervisor.ts +199 -0
- package/src/interpretation/argv.ts +65 -34
- package/src/interpretation/content.ts +30 -13
- package/src/interpretation/hints.ts +2 -12
- package/src/interpretation/index.ts +0 -1
- package/src/interpretation/isolation.ts +39 -0
- package/src/interpretation/limits.ts +14 -75
- package/src/interpretation/question.ts +46 -0
- package/src/interpretation/refusal.ts +28 -18
- package/src/interpretation/resolve-options.ts +98 -33
- package/src/interpretation/session-input.ts +88 -5
- package/src/interpretation/shape.ts +10 -0
- package/src/interpretation/skills-selection.ts +70 -74
- package/src/interpretation/support.ts +21 -6
- package/src/interpretation/tool-selection.ts +67 -122
- package/src/interpretation/tool-vocabulary.ts +48 -53
- package/src/interpretation/turn-options.ts +125 -220
- package/src/interpretation/vocabulary.ts +15 -10
- package/src/knowledge/claude-code.ts +17 -4
- package/src/knowledge/codex.ts +31 -7
- package/src/knowledge/descriptor.ts +106 -49
- package/src/knowledge/matchers.ts +35 -0
- package/src/knowledge/muse.ts +13 -6
- package/src/knowledge/overrides.ts +17 -32
- package/src/knowledge/pi.ts +8 -6
- package/src/knowledge/profile.ts +1 -0
- package/dist/interpretation/dimensions.d.ts +0 -11
- package/dist/interpretation/dimensions.d.ts.map +0 -1
- package/dist/interpretation/dimensions.js +0 -4
- package/dist/interpretation/dimensions.js.map +0 -1
- package/src/interpretation/dimensions.ts +0 -14
|
@@ -4,24 +4,47 @@
|
|
|
4
4
|
* prompt before tool grants) and the spawn-boundary refusals live here so
|
|
5
5
|
* no caller re-derives them.
|
|
6
6
|
*/
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
AccessValue,
|
|
9
|
+
HarnessDescriptor,
|
|
10
|
+
StreamingGranularity,
|
|
11
|
+
} from "../knowledge/descriptor.js";
|
|
8
12
|
import { defaultDescriptors } from "../knowledge/overrides.js";
|
|
13
|
+
import { assertIsolationCombination } from "./isolation.js";
|
|
9
14
|
import { ArgvRefusalError } from "./refusal.js";
|
|
10
15
|
import { assertUsableSessionId, SESSION_ID_MAX, SessionIdRefusalError } from "./session-id.js";
|
|
11
|
-
import { renderSkillsSelection } from "./skills-selection.js";
|
|
16
|
+
import { renderSkillsSelection, type SkillsSelection } from "./skills-selection.js";
|
|
12
17
|
import { supportedBy } from "./support.js";
|
|
13
18
|
import { renderToolSelection } from "./tool-selection.js";
|
|
19
|
+
import type { ToolMap } from "./tool-vocabulary.js";
|
|
14
20
|
import { renderTurnOptions } from "./turn-options.js";
|
|
15
21
|
import { validateModel } from "./vocabulary.js";
|
|
16
22
|
|
|
17
23
|
export type { RefusalIssue } from "./refusal.js";
|
|
18
24
|
export { ArgvRefusalError, buildRefusalMessage, REFUSAL_ISSUES } from "./refusal.js";
|
|
19
25
|
|
|
20
|
-
/**
|
|
26
|
+
/** A prompt carries its own provenance (RFC-02 change 13): a plain string
|
|
27
|
+
* is an implicit, positional prompt; the object form came from an explicit
|
|
28
|
+
* flag or file, so a leading dash is the caller's intent, not a flag. */
|
|
29
|
+
export type Prompt = string | { readonly text: string; readonly explicit: boolean };
|
|
30
|
+
|
|
31
|
+
/** The one accessor for the prompt's text, for builders, the runner, and
|
|
32
|
+
* redaction alike. */
|
|
33
|
+
export const promptTextOf = (opts: { readonly prompt: Prompt }): string =>
|
|
34
|
+
typeof opts.prompt === "string" ? opts.prompt : opts.prompt.text;
|
|
35
|
+
|
|
36
|
+
/** The same prompt with new text: the composed form keeps its provenance. */
|
|
37
|
+
export const withPromptText = (prompt: Prompt, text: string): Prompt =>
|
|
38
|
+
typeof prompt === "string" ? text : { ...prompt, text };
|
|
39
|
+
|
|
40
|
+
/** One guard for every builder that places a positional prompt: an
|
|
41
|
+
* implicit prompt may not start with '-' (it would be parsed as a flag),
|
|
42
|
+
* while an explicit one - `hcn run --prompt "-bad"` - passes. Selector
|
|
21
43
|
* hygiene (session ids) lives in session-id.ts; model selectors go through
|
|
22
44
|
* validateModel - both refuse, never sanitize. */
|
|
23
|
-
const assertCleanPrompt = (h: HarnessDescriptor, prompt:
|
|
24
|
-
if (prompt
|
|
45
|
+
const assertCleanPrompt = (h: HarnessDescriptor, prompt: Prompt): void => {
|
|
46
|
+
if (typeof prompt !== "string" && prompt.explicit) return;
|
|
47
|
+
if (promptTextOf({ prompt }).startsWith("-")) {
|
|
25
48
|
throw new ArgvRefusalError({
|
|
26
49
|
issue: "prompt-flag-injection",
|
|
27
50
|
harness: h.name,
|
|
@@ -31,19 +54,6 @@ const assertCleanPrompt = (h: HarnessDescriptor, prompt: string): void => {
|
|
|
31
54
|
}
|
|
32
55
|
};
|
|
33
56
|
|
|
34
|
-
/**
|
|
35
|
-
* Variant that allows a leading '-' when the caller explicitly opted in via
|
|
36
|
-
* --prompt / --prompt-file. The positional guard still applies for implicit
|
|
37
|
-
* positional prompts, but an explicit opt-in bypasses it so `hcn run --prompt "-bad"`
|
|
38
|
-
* succeeds while `hcn run "-bad"` refuses. The caller must set
|
|
39
|
-
* `__explicitPrompt: true` on the options object when the prompt came from an
|
|
40
|
-
* explicit flag.
|
|
41
|
-
*/
|
|
42
|
-
const assertCleanPromptMaybe = (h: HarnessDescriptor, prompt: string, explicit?: boolean): void => {
|
|
43
|
-
if (explicit) return;
|
|
44
|
-
assertCleanPrompt(h, prompt);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
57
|
export interface DiscoveryOptions {
|
|
48
58
|
readonly tools?: boolean;
|
|
49
59
|
readonly instructionFiles?: boolean;
|
|
@@ -52,18 +62,20 @@ export interface DiscoveryOptions {
|
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
export interface TurnOptions {
|
|
55
|
-
readonly
|
|
65
|
+
readonly isolation?: "tool-free";
|
|
66
|
+
readonly prompt: Prompt;
|
|
56
67
|
readonly tools?: readonly string[];
|
|
57
68
|
readonly excludeTools?: readonly string[];
|
|
58
|
-
/** Caller-directed skills allowlist: resolved
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* and
|
|
62
|
-
readonly skills?:
|
|
69
|
+
/** Caller-directed skills allowlist: the resolved picks and the
|
|
70
|
+
* registry's known names. Rendered per descriptor by
|
|
71
|
+
* renderSkillsSelection (pi loads each pick with discovery off; claude
|
|
72
|
+
* and codex turn the complement off); muse refuses. */
|
|
73
|
+
readonly skills?: SkillsSelection;
|
|
63
74
|
readonly model?: string;
|
|
64
75
|
readonly autonomy?: boolean;
|
|
65
76
|
readonly effort?: string;
|
|
66
77
|
readonly sandbox?: string;
|
|
78
|
+
readonly contextWindow?: number;
|
|
67
79
|
readonly provider?: string;
|
|
68
80
|
readonly discovery?: DiscoveryOptions;
|
|
69
81
|
readonly write?: boolean;
|
|
@@ -81,11 +93,10 @@ export interface TurnOptions {
|
|
|
81
93
|
* harness argv; the CLI layer turns it into the prompt preamble and
|
|
82
94
|
* arms question-block detection. Undefined means the default: "ask". */
|
|
83
95
|
readonly questions?: import("./question.js").QuestionMode;
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
readonly
|
|
88
|
-
readonly access?: "read" | "write";
|
|
96
|
+
/** The merged toolMap (every harness, native plus tier), the one shape
|
|
97
|
+
* past option resolution (RFC-02 change 8). */
|
|
98
|
+
readonly toolMap?: ToolMap;
|
|
99
|
+
readonly access?: AccessValue;
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
export interface ResumeOptions extends TurnOptions {
|
|
@@ -98,8 +109,8 @@ export type LaunchOptions = TurnOptions;
|
|
|
98
109
|
* validated selections, with the variadic tools flag LAST and fed exactly
|
|
99
110
|
* one joined token so nothing after it can be swallowed as a tool name. */
|
|
100
111
|
const turnTail = (h: HarnessDescriptor, opts: TurnOptions): string[] => {
|
|
101
|
-
|
|
102
|
-
const tail = [opts
|
|
112
|
+
assertCleanPrompt(h, opts.prompt);
|
|
113
|
+
const tail = [promptTextOf(opts), ...h.launch.streamFlags];
|
|
103
114
|
if (opts.model !== undefined) {
|
|
104
115
|
const validated = validateModel(h, opts.model);
|
|
105
116
|
if (!validated.ok) {
|
|
@@ -127,15 +138,14 @@ const turnTail = (h: HarnessDescriptor, opts: TurnOptions): string[] => {
|
|
|
127
138
|
tail.push(h.autonomy.flag);
|
|
128
139
|
}
|
|
129
140
|
if (opts.tools !== undefined || opts.excludeTools !== undefined) {
|
|
130
|
-
const perHarnessMap = opts.toolMap?.[h.name];
|
|
131
141
|
const rendered = renderToolSelection(h, {
|
|
132
142
|
include: opts.tools,
|
|
133
143
|
exclude: opts.excludeTools,
|
|
134
|
-
toolMap:
|
|
144
|
+
toolMap: opts.toolMap,
|
|
135
145
|
});
|
|
136
146
|
tail.push(...rendered.tokens);
|
|
137
147
|
}
|
|
138
|
-
if (opts.skills !== undefined
|
|
148
|
+
if (opts.skills !== undefined) {
|
|
139
149
|
tail.push(...renderSkillsSelection(h, opts.skills));
|
|
140
150
|
}
|
|
141
151
|
return tail;
|
|
@@ -186,6 +196,27 @@ export const buildResumeArgv = (h: HarnessDescriptor, opts: ResumeOptions): stri
|
|
|
186
196
|
];
|
|
187
197
|
};
|
|
188
198
|
|
|
199
|
+
/** What a spawn needs beyond the turn options: the session to resume, if
|
|
200
|
+
* any, and the raw passthrough tail (ADR 0003). */
|
|
201
|
+
export interface SpawnArgvOptions extends TurnOptions {
|
|
202
|
+
readonly resume?: string;
|
|
203
|
+
readonly passthrough?: readonly string[];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The argv a turn spawns: launch or resume per `resume`, then the
|
|
207
|
+
* passthrough tail after a bare separator. One owner, so the CLI's preview
|
|
208
|
+
* and the runner's spawn agree by construction (RFC-02 change 10). */
|
|
209
|
+
export const buildSpawnArgv = (h: HarnessDescriptor, opts: SpawnArgvOptions): string[] => {
|
|
210
|
+
assertIsolationCombination(h, opts);
|
|
211
|
+
const base =
|
|
212
|
+
opts.resume === undefined
|
|
213
|
+
? buildLaunchArgv(h, opts)
|
|
214
|
+
: buildResumeArgv(h, { ...opts, sessionId: opts.resume });
|
|
215
|
+
return opts.passthrough !== undefined && opts.passthrough.length > 0
|
|
216
|
+
? [...base, "--", ...opts.passthrough]
|
|
217
|
+
: base;
|
|
218
|
+
};
|
|
219
|
+
|
|
189
220
|
export interface SessionOptions {
|
|
190
221
|
readonly sessionId: string;
|
|
191
222
|
readonly model?: string;
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* payload-discriminated records - for the flat match/field spec that
|
|
11
11
|
* identity decoding uses.)
|
|
12
12
|
*/
|
|
13
|
-
import type { HarnessName } from "../knowledge/descriptor.js";
|
|
14
|
-
import { asRecord } from "./shape.js";
|
|
13
|
+
import type { HarnessName, LimitCode } from "../knowledge/descriptor.js";
|
|
14
|
+
import { asRecord, readPath as at } from "./shape.js";
|
|
15
15
|
|
|
16
16
|
export type ContentEvent =
|
|
17
17
|
| { readonly kind: "token"; readonly text: string }
|
|
@@ -22,7 +22,16 @@ export type ContentEvent =
|
|
|
22
22
|
* record); the runner turns it into a task failure. Other errors are
|
|
23
23
|
* informational and the turn goes on. */
|
|
24
24
|
| { readonly kind: "error"; readonly message: string; readonly terminal?: boolean }
|
|
25
|
-
| { readonly kind: "budget"; readonly detail: string }
|
|
25
|
+
| { readonly kind: "budget"; readonly detail: string }
|
|
26
|
+
/** A structured limit record on the stream (claude's rate_limit_event);
|
|
27
|
+
* the runner turns it into a limit-class failure. `resetsAt` is epoch
|
|
28
|
+
* milliseconds when the harness reported one. */
|
|
29
|
+
| {
|
|
30
|
+
readonly kind: "limit";
|
|
31
|
+
readonly code: LimitCode;
|
|
32
|
+
readonly detail: string;
|
|
33
|
+
readonly resetsAt?: number;
|
|
34
|
+
};
|
|
26
35
|
|
|
27
36
|
/** Text of an array of `{type:"text", text}` content blocks. */
|
|
28
37
|
const textOfBlocks = (content: unknown): string =>
|
|
@@ -34,16 +43,6 @@ const textOfBlocks = (content: unknown): string =>
|
|
|
34
43
|
.join("")
|
|
35
44
|
: "";
|
|
36
45
|
|
|
37
|
-
const at = (record: Record<string, unknown>, path: string): unknown => {
|
|
38
|
-
let cursor: unknown = record;
|
|
39
|
-
for (const seg of path.split(".")) {
|
|
40
|
-
const inner = asRecord(cursor);
|
|
41
|
-
if (inner === null) return undefined;
|
|
42
|
-
cursor = inner[seg];
|
|
43
|
-
}
|
|
44
|
-
return cursor;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
46
|
const claude = (r: Record<string, unknown>): ContentEvent[] => {
|
|
48
47
|
const events: ContentEvent[] = [];
|
|
49
48
|
if (r.type === "assistant") {
|
|
@@ -75,6 +74,24 @@ const claude = (r: Record<string, unknown>): ContentEvent[] => {
|
|
|
75
74
|
// clean turn. (openSession handles result boundaries itself.)
|
|
76
75
|
const sub = typeof r.subtype === "string" ? r.subtype : "result error";
|
|
77
76
|
events.push({ kind: "error", message: `turn failed: ${sub}`, terminal: true });
|
|
77
|
+
} else if (r.type === "rate_limit_event") {
|
|
78
|
+
// Only "rejected" is a limit; "allowed_warning" still serves the request.
|
|
79
|
+
// overageStatus is a separate billing signal, not a rate limit.
|
|
80
|
+
// resetsAt arrives in seconds; the event carries milliseconds. No wall
|
|
81
|
+
// clock is read.
|
|
82
|
+
const info = asRecord(r.rate_limit_info);
|
|
83
|
+
const status = info?.status;
|
|
84
|
+
if (status === "rejected") {
|
|
85
|
+
const raw = info?.resetsAt;
|
|
86
|
+
const milliseconds = typeof raw === "number" ? raw * 1000 : Number.NaN;
|
|
87
|
+
const resetsAt = Number.isFinite(milliseconds) && milliseconds > 0 ? milliseconds : undefined;
|
|
88
|
+
events.push({
|
|
89
|
+
kind: "limit",
|
|
90
|
+
code: "rate-limit",
|
|
91
|
+
detail: `rate_limit_event status=${String(status)}`,
|
|
92
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
78
95
|
}
|
|
79
96
|
return events;
|
|
80
97
|
};
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* stay-on-harness suggestion first. Wording is locked verbatim; unit tests
|
|
7
7
|
* pin every string.
|
|
8
8
|
*/
|
|
9
|
-
import type
|
|
9
|
+
import { deepFreeze, type HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
10
10
|
import type { RefusalOption } from "./refusal.js";
|
|
11
11
|
|
|
12
|
-
const HINTS: Readonly<Record<string, Readonly<Record<string, string>>>> =
|
|
12
|
+
const HINTS: Readonly<Record<string, Readonly<Record<string, string>>>> = deepFreeze({
|
|
13
13
|
claude: {
|
|
14
14
|
sandbox:
|
|
15
15
|
"claude has no sandbox modes; approximate with a per-tool allowlist (--tools Read,Bash) or --disallowedTools to keep tools out, and run untrusted work in a disposable directory or container",
|
|
@@ -84,16 +84,6 @@ const HINTS: Readonly<Record<string, Readonly<Record<string, string>>>> = deepFr
|
|
|
84
84
|
},
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
/** deepFreeze without importing the descriptor's (which carries extra
|
|
88
|
-
* machinery); structure is plain JSON so Object.freeze all the way down. */
|
|
89
|
-
function deepFreezeSafe<T>(value: T): T {
|
|
90
|
-
if (typeof value === "object" && value !== null) {
|
|
91
|
-
for (const v of Object.values(value as Record<string, unknown>)) deepFreezeSafe(v);
|
|
92
|
-
Object.freeze(value);
|
|
93
|
-
}
|
|
94
|
-
return value;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
87
|
/** The confirmed hint for a refused option on a harness, or undefined. */
|
|
98
88
|
export const hintFor = (harness: string, option: RefusalOption): string | undefined =>
|
|
99
89
|
HINTS[harness]?.[option];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
2
|
+
import type { TurnOptions } from "./argv.js";
|
|
3
|
+
import { ArgvRefusalError } from "./refusal.js";
|
|
4
|
+
|
|
5
|
+
/** Isolation owns these dimensions for the whole invocation. Config defaults
|
|
6
|
+
* yield to it; explicit competing selections refuse before registry reads. */
|
|
7
|
+
export const ISOLATION_OVERRIDES = [
|
|
8
|
+
"tools",
|
|
9
|
+
"excludeTools",
|
|
10
|
+
"skills",
|
|
11
|
+
"access",
|
|
12
|
+
"autonomy",
|
|
13
|
+
"discovery",
|
|
14
|
+
] as const;
|
|
15
|
+
export const assertIsolationCombination = (
|
|
16
|
+
h: HarnessDescriptor,
|
|
17
|
+
opts: Partial<TurnOptions> & {
|
|
18
|
+
readonly skillNames?: readonly string[];
|
|
19
|
+
readonly passthrough?: readonly string[];
|
|
20
|
+
},
|
|
21
|
+
): void => {
|
|
22
|
+
if (opts.isolation === undefined) return;
|
|
23
|
+
const conflict =
|
|
24
|
+
ISOLATION_OVERRIDES.find((key) =>
|
|
25
|
+
key === "autonomy" ? opts[key] === true : opts[key] !== undefined,
|
|
26
|
+
) ??
|
|
27
|
+
(opts.skillNames !== undefined ? "skills" : undefined) ??
|
|
28
|
+
(opts.passthrough?.length ? "native passthrough" : undefined);
|
|
29
|
+
if (conflict === undefined) return;
|
|
30
|
+
throw new ArgvRefusalError({
|
|
31
|
+
issue: "mutually-exclusive-options",
|
|
32
|
+
harness: h.name,
|
|
33
|
+
option: "isolation",
|
|
34
|
+
detail: conflict,
|
|
35
|
+
supported: [
|
|
36
|
+
"tool-free without explicit tools, skills, access, autonomy, discovery, or native passthrough",
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
};
|
|
@@ -25,18 +25,15 @@ import type {
|
|
|
25
25
|
LimitMatcher,
|
|
26
26
|
PhraseMatcher,
|
|
27
27
|
} from "../knowledge/descriptor.js";
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
37
|
-
* compiler or the scanner. */
|
|
38
|
-
const MAX_PATTERN_LENGTH = 200;
|
|
39
|
-
const MAX_MATCHERS_PER_KIND = 64;
|
|
28
|
+
import {
|
|
29
|
+
compileMatcher,
|
|
30
|
+
MAX_MATCHERS_PER_KIND,
|
|
31
|
+
SHARED_TRANSPORT_MATCHERS,
|
|
32
|
+
SHARED_UNAVAILABLE_MATCHERS,
|
|
33
|
+
} from "../knowledge/matchers.js";
|
|
34
|
+
|
|
35
|
+
/** The pattern bounds live with the matchers in the knowledge layer
|
|
36
|
+
* (compileMatcher); this is the per-line input window. */
|
|
40
37
|
const WINDOW = 4096;
|
|
41
38
|
|
|
42
39
|
// WeakMap cache: same matcher array instance reuses identical RegExp objects
|
|
@@ -49,24 +46,6 @@ const authCache = new WeakMap<
|
|
|
49
46
|
ReadonlyArray<readonly [RegExp, AuthFailureKind]>
|
|
50
47
|
>();
|
|
51
48
|
|
|
52
|
-
const validateAndCompile = (pattern: string, flags: string | undefined): RegExp => {
|
|
53
|
-
const f = flags ?? "i";
|
|
54
|
-
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
55
|
-
throw new Error(`pattern over ${MAX_PATTERN_LENGTH} characters`);
|
|
56
|
-
}
|
|
57
|
-
if (f.includes("g") || f.includes("y")) {
|
|
58
|
-
throw new Error(`flags must not contain g or y (got ${JSON.stringify(f)})`);
|
|
59
|
-
}
|
|
60
|
-
for (const ch of f) {
|
|
61
|
-
if (!"imsu".includes(ch)) throw new Error(`flag ${JSON.stringify(ch)} outside imsu`);
|
|
62
|
-
}
|
|
63
|
-
try {
|
|
64
|
-
return new RegExp(pattern, f);
|
|
65
|
-
} catch (e) {
|
|
66
|
-
throw new Error(`uncompilable pattern ${JSON.stringify(pattern)}: ${(e as Error).message}`);
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
49
|
export const compileLimitMatchers = (
|
|
71
50
|
matchers: ReadonlyArray<LimitMatcher>,
|
|
72
51
|
): ReadonlyArray<readonly [RegExp, LimitCode]> => {
|
|
@@ -75,7 +54,7 @@ export const compileLimitMatchers = (
|
|
|
75
54
|
if (matchers.length > MAX_MATCHERS_PER_KIND) {
|
|
76
55
|
throw new Error(`more than ${MAX_MATCHERS_PER_KIND} matchers per harness per kind`);
|
|
77
56
|
}
|
|
78
|
-
const compiled = matchers.map((m) => [
|
|
57
|
+
const compiled = matchers.map((m) => [compileMatcher(m.pattern, m.flags), m.code] as const);
|
|
79
58
|
limitCache.set(matchers, compiled);
|
|
80
59
|
return compiled;
|
|
81
60
|
};
|
|
@@ -88,27 +67,11 @@ export const compileAuthMatchers = (
|
|
|
88
67
|
if (matchers.length > MAX_MATCHERS_PER_KIND) {
|
|
89
68
|
throw new Error(`more than ${MAX_MATCHERS_PER_KIND} matchers per harness per kind`);
|
|
90
69
|
}
|
|
91
|
-
const compiled = matchers.map((m) => [
|
|
70
|
+
const compiled = matchers.map((m) => [compileMatcher(m.pattern, m.flags), m.kind] as const);
|
|
92
71
|
authCache.set(matchers, compiled);
|
|
93
72
|
return compiled;
|
|
94
73
|
};
|
|
95
74
|
|
|
96
|
-
// Generic alias for tests that call compileMatchers directly
|
|
97
|
-
export const compileMatchers = <T extends LimitMatcher | AuthMatcher>(
|
|
98
|
-
matchers: ReadonlyArray<T>,
|
|
99
|
-
): ReadonlyArray<readonly [RegExp, unknown]> => {
|
|
100
|
-
// Dispatch based on first element's shape - limit has code, auth has kind
|
|
101
|
-
if (matchers.length === 0) return [];
|
|
102
|
-
const first = matchers[0] as unknown as Record<string, unknown>;
|
|
103
|
-
if ("code" in first)
|
|
104
|
-
return compileLimitMatchers(
|
|
105
|
-
matchers as unknown as ReadonlyArray<LimitMatcher>,
|
|
106
|
-
) as unknown as ReadonlyArray<readonly [RegExp, unknown]>;
|
|
107
|
-
return compileAuthMatchers(
|
|
108
|
-
matchers as unknown as ReadonlyArray<AuthMatcher>,
|
|
109
|
-
) as unknown as ReadonlyArray<readonly [RegExp, unknown]>;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
75
|
const scanLine = <Code>(
|
|
113
76
|
line: string,
|
|
114
77
|
matchers: ReadonlyArray<readonly [RegExp, Code]>,
|
|
@@ -121,40 +84,16 @@ const scanLine = <Code>(
|
|
|
121
84
|
return null;
|
|
122
85
|
};
|
|
123
86
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
matchers: ReadonlyArray<readonly [RegExp, Code]>,
|
|
127
|
-
): Code | null => {
|
|
128
|
-
let end = output.length;
|
|
129
|
-
let scanned = 0;
|
|
130
|
-
while (end > 0 && scanned < BATCH_SCAN_MAX_LINES) {
|
|
131
|
-
const start = output.lastIndexOf("\n", end - 1);
|
|
132
|
-
const line = output.slice(start + 1, end).trim();
|
|
133
|
-
end = start;
|
|
134
|
-
if (line === "") continue;
|
|
135
|
-
scanned++;
|
|
136
|
-
const code = scanLine(line, matchers);
|
|
137
|
-
if (code !== null) return code;
|
|
138
|
-
}
|
|
139
|
-
return null;
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
/** Per-line entry point for streaming readers: O(1) per line, no rescans. */
|
|
87
|
+
/** Per-line entry point for streaming readers: O(1) per line, no rescans.
|
|
88
|
+
* Both runners feed lines as they arrive; there is no batch form. */
|
|
143
89
|
export const detectLimitInLine = (h: HarnessDescriptor, line: string): LimitCode | null =>
|
|
144
90
|
scanLine(line.trim(), compileLimitMatchers(h.limitMatchers));
|
|
145
91
|
|
|
146
|
-
/** Batch convenience over a turn's tail, bounded and bottom-up. */
|
|
147
|
-
export const detectLimit = (h: HarnessDescriptor, output: string): LimitCode | null =>
|
|
148
|
-
scanTail(output, compileLimitMatchers(h.limitMatchers));
|
|
149
|
-
|
|
150
92
|
export const detectAuthFailureInLine = (
|
|
151
93
|
h: HarnessDescriptor,
|
|
152
94
|
line: string,
|
|
153
95
|
): AuthFailureKind | null => scanLine(line.trim(), compileAuthMatchers(h.authMatchers));
|
|
154
96
|
|
|
155
|
-
export const detectAuthFailure = (h: HarnessDescriptor, output: string): AuthFailureKind | null =>
|
|
156
|
-
scanTail(output, compileAuthMatchers(h.authMatchers));
|
|
157
|
-
|
|
158
97
|
const phraseCache = new WeakMap<ReadonlyArray<PhraseMatcher>, ReadonlyArray<RegExp>>();
|
|
159
98
|
|
|
160
99
|
const compilePhraseMatchers = (matchers: ReadonlyArray<PhraseMatcher>): ReadonlyArray<RegExp> => {
|
|
@@ -163,7 +102,7 @@ const compilePhraseMatchers = (matchers: ReadonlyArray<PhraseMatcher>): Readonly
|
|
|
163
102
|
if (matchers.length > MAX_MATCHERS_PER_KIND) {
|
|
164
103
|
throw new Error(`more than ${MAX_MATCHERS_PER_KIND} matchers per harness per kind`);
|
|
165
104
|
}
|
|
166
|
-
const compiled = matchers.map((m) =>
|
|
105
|
+
const compiled = matchers.map((m) => compileMatcher(m.pattern, m.flags));
|
|
167
106
|
phraseCache.set(matchers, compiled);
|
|
168
107
|
return compiled;
|
|
169
108
|
};
|
|
@@ -47,6 +47,12 @@ 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
|
+
/** The wrapper hcn composes around the user's answer to an escalated
|
|
51
|
+
* question, so a consumer never re-derives it (RFC-01; the README quotes
|
|
52
|
+
* the exact text). One owner for both session surfaces. */
|
|
53
|
+
export const composeAnswer = (question: string, answer: string): string =>
|
|
54
|
+
`The user answered the question: "${question}" with: ${answer}. Continue accordingly.`;
|
|
55
|
+
|
|
50
56
|
export type QuestionMode = "ask" | "assume" | "none";
|
|
51
57
|
|
|
52
58
|
export const QUESTION_MODES: readonly QuestionMode[] = ["ask", "assume", "none"] as const;
|
|
@@ -152,6 +158,46 @@ const parseBlock = (body: string): QuestionDetection => {
|
|
|
152
158
|
};
|
|
153
159
|
};
|
|
154
160
|
|
|
161
|
+
/** What a turn's close found, recorded on `done` (RFC-01). */
|
|
162
|
+
export type QuestionDetectionKind = "block" | "malformed" | "none";
|
|
163
|
+
|
|
164
|
+
/** The typed question event a detected block becomes: the fields ARE the
|
|
165
|
+
* question; prose renders from them downstream. */
|
|
166
|
+
export interface QuestionEvent {
|
|
167
|
+
readonly kind: "question";
|
|
168
|
+
readonly question: string;
|
|
169
|
+
readonly options: readonly string[];
|
|
170
|
+
readonly recommended?: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The one place a detected block becomes a question event (RFC-02
|
|
174
|
+
* change 5). Both runners call it at turn close with the last assistant
|
|
175
|
+
* message: detection is armed only in `ask` mode; a malformed block names
|
|
176
|
+
* its malformation and is never a silent no-op. */
|
|
177
|
+
export const questionEventOf = (
|
|
178
|
+
lastAssistantText: string | null,
|
|
179
|
+
mode: QuestionMode,
|
|
180
|
+
):
|
|
181
|
+
| { readonly detection: "none" }
|
|
182
|
+
| { readonly detection: "malformed"; readonly malformed: string }
|
|
183
|
+
| { readonly detection: "block"; readonly event: QuestionEvent } => {
|
|
184
|
+
if (mode !== "ask" || lastAssistantText === null) return { detection: "none" };
|
|
185
|
+
const detected = detectQuestionBlock(lastAssistantText);
|
|
186
|
+
if (detected === null) return { detection: "none" };
|
|
187
|
+
if ("malformed" in detected) return { detection: "malformed", malformed: detected.malformed };
|
|
188
|
+
return {
|
|
189
|
+
detection: "block",
|
|
190
|
+
event: {
|
|
191
|
+
kind: "question",
|
|
192
|
+
question: detected.block.question,
|
|
193
|
+
options: detected.block.options,
|
|
194
|
+
...(detected.block.recommended !== undefined
|
|
195
|
+
? { recommended: detected.block.recommended }
|
|
196
|
+
: {}),
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
155
201
|
/** Detect the hcn-question block in a message text. The LAST block wins
|
|
156
202
|
* (the protocol makes the block the turn's final content; a corrected
|
|
157
203
|
* re-emit supersedes an earlier one). An empty candidate body (a bare
|
|
@@ -36,14 +36,18 @@ export type RefusalOption =
|
|
|
36
36
|
| "skills"
|
|
37
37
|
| "autonomy"
|
|
38
38
|
| "questions"
|
|
39
|
+
| "timeout"
|
|
39
40
|
| `discovery.${string}`;
|
|
40
41
|
|
|
41
42
|
/** One helper builds the message from the structured fields so message and
|
|
42
43
|
* fields cannot drift. Every message names an alternative, not only a
|
|
43
|
-
* negation, so an agent can pivot without reading the descriptor.
|
|
44
|
+
* negation, so an agent can pivot without reading the descriptor. A
|
|
45
|
+
* refusal raised before a harness is chosen (argument parsing, config)
|
|
46
|
+
* passes no harness, and the message carries no harness fragment (RFC-02
|
|
47
|
+
* change 11) instead of naming one the caller never picked. */
|
|
44
48
|
export const buildRefusalMessage = (
|
|
45
49
|
issue: RefusalIssue,
|
|
46
|
-
harness: HarnessName,
|
|
50
|
+
harness: HarnessName | undefined,
|
|
47
51
|
option?: RefusalOption,
|
|
48
52
|
facet?: DiscoveryFacet,
|
|
49
53
|
supported: readonly string[] = [],
|
|
@@ -58,42 +62,46 @@ export const buildRefusalMessage = (
|
|
|
58
62
|
? ` facet ${JSON.stringify(facet)}`
|
|
59
63
|
: "";
|
|
60
64
|
const detailSuffix = detail ? ` (${detail})` : "";
|
|
65
|
+
// The harness fragments: present only when a harness was chosen.
|
|
66
|
+
const who = harness ?? "the harness";
|
|
67
|
+
const onHarness = harness === undefined ? "" : ` on ${harness}`;
|
|
68
|
+
const forHarness = harness === undefined ? "" : ` for ${harness}`;
|
|
61
69
|
switch (issue) {
|
|
62
70
|
case "unsupported-option":
|
|
63
|
-
return `${
|
|
71
|
+
return `${who} cannot express${optionPart}${detailSuffix}; ${supportedStr} - drop the option or route this work to a harness that supports it`;
|
|
64
72
|
case "unsupported-option-facet":
|
|
65
|
-
return `${
|
|
73
|
+
return `${who} cannot express discovery${facetSuffix}${detailSuffix}; ${supportedStr} - drop the facet or route this work to a harness that supports it`;
|
|
66
74
|
case "unsupported-on-resume": {
|
|
67
75
|
const name = option
|
|
68
76
|
? JSON.stringify(option) + (facet ? `:${facet}` : "")
|
|
69
77
|
: facet
|
|
70
78
|
? JSON.stringify(facet)
|
|
71
79
|
: "option";
|
|
72
|
-
return `${name} cannot be expressed on resume
|
|
80
|
+
return `${name} cannot be expressed on resume${forHarness}${detailSuffix}; ${supportedStr} - re-launch instead of resuming or drop the option`;
|
|
73
81
|
}
|
|
74
82
|
case "invalid-option-value":
|
|
75
|
-
return `invalid value for${optionPart}
|
|
83
|
+
return `invalid value for${optionPart}${onHarness}${detailSuffix}; ${supportedStr} - use a supported value instead`;
|
|
76
84
|
case "unknown-effort":
|
|
77
|
-
return `unknown effort
|
|
85
|
+
return `unknown effort${forHarness}${detail ? ` ${JSON.stringify(detail)}` : ""}; ${supportedStr} - use one of the ladder values`;
|
|
78
86
|
case "unknown-model":
|
|
79
|
-
return `unknown ${harness} model${detail ? ` ${JSON.stringify(detail)}` : ""}; ${supportedStr} - use one of the supported models`;
|
|
87
|
+
return `unknown ${harness === undefined ? "" : `${harness} `}model${detail ? ` ${JSON.stringify(detail)}` : ""}; ${supportedStr} - use one of the supported models`;
|
|
80
88
|
case "invalid-env":
|
|
81
|
-
return `invalid env key or value
|
|
89
|
+
return `invalid env key or value${forHarness}${detailSuffix}; ${supportedStr} - keys must match ^[A-Za-z_][A-Za-z0-9_]*$ and contain no NUL`;
|
|
82
90
|
case "invalid-tool-grant":
|
|
83
|
-
return `tool grant
|
|
91
|
+
return `tool grant${forHarness} contains an empty entry or a comma; a blank tool flag value grants nothing detectable, and a comma inside one name silently splits the grant; ${supportedStr} - provide comma-free, non-empty tool names as separate entries`;
|
|
84
92
|
case "unknown-tool-name":
|
|
85
|
-
return `${
|
|
93
|
+
return `${who} cannot compute a tool complement around an unknown name${detailSuffix}; ${supportedStr} - exclude only curated names, or pass the unknown name through an include list instead`;
|
|
86
94
|
case "mutually-exclusive-options":
|
|
87
|
-
return `${harness} cannot combine${optionPart}${detailSuffix}; ${supportedStr} - pass exactly one of them`;
|
|
95
|
+
return `${harness === undefined ? "cannot combine" : `${harness} cannot combine`}${optionPart}${detailSuffix}; ${supportedStr} - pass exactly one of them`;
|
|
88
96
|
case "prompt-flag-injection":
|
|
89
|
-
return `positional prompt may not start with '-'; it would be parsed as a flag by ${
|
|
97
|
+
return `positional prompt may not start with '-'; it would be parsed as a flag by ${who}${detailSuffix}; ${supportedStr} - remove leading '-' or prefix with a space`;
|
|
90
98
|
case "no-autonomy-mode":
|
|
91
|
-
return `${
|
|
99
|
+
return `${who} has no unattended-run flag; ${supportedStr} - drop autonomy or route to a supporting harness (claude --dangerously-skip-permissions, codex/muse --yolo)`;
|
|
92
100
|
case "no-session-mode":
|
|
93
|
-
return `${
|
|
101
|
+
return `${who} declares no persistent headless session mode; ${supportedStr} - use hcn run --resume <id>`;
|
|
94
102
|
default: {
|
|
95
103
|
const exhaustive: never = issue;
|
|
96
|
-
return `${exhaustive as string}
|
|
104
|
+
return `${exhaustive as string}${forHarness}${optionPart}${detailSuffix}; ${supportedStr}`;
|
|
97
105
|
}
|
|
98
106
|
}
|
|
99
107
|
};
|
|
@@ -101,7 +109,9 @@ export const buildRefusalMessage = (
|
|
|
101
109
|
/** Raised when launch options would corrupt or subvert the spawned argv. */
|
|
102
110
|
export class ArgvRefusalError extends Error {
|
|
103
111
|
readonly issue: RefusalIssue;
|
|
104
|
-
|
|
112
|
+
/** The harness the refusal is about; absent when it was raised before
|
|
113
|
+
* one was chosen (argument parsing, config loading). */
|
|
114
|
+
readonly harness?: HarnessName;
|
|
105
115
|
readonly option?: RefusalOption;
|
|
106
116
|
readonly facet?: DiscoveryFacet;
|
|
107
117
|
readonly supported: readonly string[];
|
|
@@ -115,7 +125,7 @@ export class ArgvRefusalError extends Error {
|
|
|
115
125
|
readonly hint?: string;
|
|
116
126
|
constructor(args: {
|
|
117
127
|
readonly issue: RefusalIssue;
|
|
118
|
-
readonly harness
|
|
128
|
+
readonly harness?: HarnessName;
|
|
119
129
|
readonly option?: RefusalOption;
|
|
120
130
|
readonly facet?: DiscoveryFacet;
|
|
121
131
|
readonly supported?: readonly string[];
|