@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
|
@@ -11,8 +11,10 @@ import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
|
11
11
|
import { defaultDescriptors } from "../knowledge/overrides.js";
|
|
12
12
|
import { DEFAULT_TURN_PROFILE, type ProfileKey } from "../knowledge/profile.js";
|
|
13
13
|
import type { TurnOptions } from "./argv.js";
|
|
14
|
+
import { assertIsolationCombination, ISOLATION_OVERRIDES } from "./isolation.js";
|
|
15
|
+
import type { QuestionMode } from "./question.js";
|
|
14
16
|
import { ArgvRefusalError } from "./refusal.js";
|
|
15
|
-
import type {
|
|
17
|
+
import type { ToolMapConfig } from "./tool-vocabulary.js";
|
|
16
18
|
import { allCanonicalNames, mergeToolMaps, validateCanonicalList } from "./tool-vocabulary.js";
|
|
17
19
|
import { validateAccess } from "./vocabulary.js";
|
|
18
20
|
|
|
@@ -55,6 +57,7 @@ export interface ResolvedOptions {
|
|
|
55
57
|
* resolves to "emit nothing," which every harness can do. Divergence is
|
|
56
58
|
* reserved for dimensions that would emit a flag the harness lacks. */
|
|
57
59
|
const EXPRESSIBLE: Readonly<Record<ProfileKey, (h: HarnessDescriptor) => boolean>> = {
|
|
60
|
+
contextWindow: (h) => h.turnOptions.contextWindow !== undefined,
|
|
58
61
|
effort: (h) => h.turnOptions.effort !== undefined,
|
|
59
62
|
sandbox: (h) => h.turnOptions.sandbox !== undefined,
|
|
60
63
|
discovery: () => true,
|
|
@@ -64,20 +67,71 @@ const EXPRESSIBLE: Readonly<Record<ProfileKey, (h: HarnessDescriptor) => boolean
|
|
|
64
67
|
tools: (h) => h.tools.includeFlag !== null || h.tools.excludeFlag !== null,
|
|
65
68
|
};
|
|
66
69
|
|
|
70
|
+
/** What one parsed config file carries: the turn options it may set, plus
|
|
71
|
+
* the keys that are not turn options - named toolsets, the raw toolMap
|
|
72
|
+
* (harness -> canonical -> native, merged into the one ToolMap shape
|
|
73
|
+
* here), and the wall-clock timeout. */
|
|
74
|
+
export type ConfigTier = Readonly<Omit<Partial<TurnOptions>, "toolMap">> & {
|
|
75
|
+
readonly toolMap?: ToolMapConfig;
|
|
76
|
+
readonly toolsets?: Readonly<Record<string, readonly string[]>>;
|
|
77
|
+
readonly timeout?: number;
|
|
78
|
+
};
|
|
79
|
+
|
|
67
80
|
export interface ConfigTiers {
|
|
68
81
|
/** ~/.config/hcn/config.json (XDG) - machine-wide defaults. */
|
|
69
|
-
readonly user?:
|
|
82
|
+
readonly user?: ConfigTier;
|
|
70
83
|
/** <git-root>/.hcn/config.json - auto-discovered (ratified A), the ALL-
|
|
71
84
|
* OFF tier; its `tools` key is both the default grant and the FLOOR: an
|
|
72
85
|
* arg grant exceeding it refuses, naming both sets (D5). */
|
|
73
|
-
readonly project?:
|
|
86
|
+
readonly project?: ConfigTier;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Where a resolved behaviour value came from; `default` is hcn's own. */
|
|
90
|
+
export type BehaviorTier = "arg" | "project-config" | "user-config" | "default";
|
|
91
|
+
|
|
92
|
+
/** The hcn-owned behaviour instructions a run resolves on every path,
|
|
93
|
+
* launch, resume, and session alike (RFC-02 change 6): question mode,
|
|
94
|
+
* which rides the prompt rather than the argv, and the wall-clock
|
|
95
|
+
* timeout hcn enforces itself. Neither is a turn option, so the
|
|
96
|
+
* launch-only turn-option resolver never sees them. */
|
|
97
|
+
export interface ResolvedBehavior {
|
|
98
|
+
readonly questions: { readonly value: QuestionMode; readonly tier: BehaviorTier };
|
|
99
|
+
readonly timeoutSeconds: { readonly value: number | undefined; readonly tier: BehaviorTier };
|
|
74
100
|
}
|
|
75
101
|
|
|
102
|
+
/** Precedence arg > project > user > default, with the tier the
|
|
103
|
+
* provenance line prints. Timeout 0 is an explicit disable. */
|
|
104
|
+
export const resolveBehavior = (
|
|
105
|
+
args: { readonly questions?: QuestionMode; readonly timeoutSeconds?: number },
|
|
106
|
+
tiers: ConfigTiers,
|
|
107
|
+
): ResolvedBehavior => {
|
|
108
|
+
const pick = <T>(
|
|
109
|
+
arg: T | undefined,
|
|
110
|
+
project: T | undefined,
|
|
111
|
+
user: T | undefined,
|
|
112
|
+
fallback: T,
|
|
113
|
+
): { readonly value: T; readonly tier: BehaviorTier } => {
|
|
114
|
+
if (arg !== undefined) return { value: arg, tier: "arg" };
|
|
115
|
+
if (project !== undefined) return { value: project, tier: "project-config" };
|
|
116
|
+
if (user !== undefined) return { value: user, tier: "user-config" };
|
|
117
|
+
return { value: fallback, tier: "default" };
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
questions: pick(args.questions, tiers.project?.questions, tiers.user?.questions, "ask"),
|
|
121
|
+
timeoutSeconds: pick(
|
|
122
|
+
args.timeoutSeconds,
|
|
123
|
+
tiers.project?.timeout,
|
|
124
|
+
tiers.user?.timeout,
|
|
125
|
+
undefined,
|
|
126
|
+
),
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
76
130
|
/** Merge semantics (gap 1, resolved): config keys are scalars and lists in
|
|
77
131
|
* schema v1 - there is nothing to deep-merge INTO - so precedence is whole-
|
|
78
132
|
* key replacement: arg > project > user > profile. A future nested key
|
|
79
133
|
* (per-harness sections) ships with schema v2 and its own merge rule. */
|
|
80
|
-
const effectiveConfig = (tiers: ConfigTiers):
|
|
134
|
+
const effectiveConfig = (tiers: ConfigTiers): ConfigTier => ({
|
|
81
135
|
...(tiers.user ?? {}),
|
|
82
136
|
...(tiers.project ?? {}),
|
|
83
137
|
});
|
|
@@ -99,10 +153,17 @@ export const resolveEffectiveOptions = (
|
|
|
99
153
|
args: TurnOptions,
|
|
100
154
|
tiers: ConfigTiers = {},
|
|
101
155
|
): ResolvedOptions => {
|
|
156
|
+
assertIsolationCombination(h, args);
|
|
102
157
|
const provenance: ProvenanceEntry[] = [];
|
|
158
|
+
if (args.isolation !== undefined)
|
|
159
|
+
provenance.push({ key: "isolation", value: args.isolation, tier: "arg" });
|
|
103
160
|
const unrenderable: string[] = [];
|
|
104
|
-
const config = effectiveConfig(tiers);
|
|
161
|
+
const config = { ...effectiveConfig(tiers) };
|
|
162
|
+
if (args.isolation !== undefined) {
|
|
163
|
+
for (const key of ISOLATION_OVERRIDES) delete config[key];
|
|
164
|
+
}
|
|
105
165
|
const sourceTier = (key: string): ProvenanceTier | undefined => {
|
|
166
|
+
if (config[key as keyof TurnOptions] === undefined) return undefined;
|
|
106
167
|
if (tiers.project?.[key as keyof TurnOptions] !== undefined) return "project-config";
|
|
107
168
|
if (tiers.user?.[key as keyof TurnOptions] !== undefined) return "user-config";
|
|
108
169
|
return undefined;
|
|
@@ -112,9 +173,9 @@ export const resolveEffectiveOptions = (
|
|
|
112
173
|
// toolset resolves to its list BEFORE the floor check, so a named set
|
|
113
174
|
// within the floor passes and one exceeding it refuses naming the set's
|
|
114
175
|
// members. Project toolsets win name collisions over user toolsets.
|
|
115
|
-
const toolsets = {
|
|
116
|
-
...(
|
|
117
|
-
...(
|
|
176
|
+
const toolsets: Record<string, readonly string[]> = {
|
|
177
|
+
...(tiers.user?.toolsets ?? {}),
|
|
178
|
+
...(tiers.project?.toolsets ?? {}),
|
|
118
179
|
};
|
|
119
180
|
let effectiveArgs = args;
|
|
120
181
|
if (
|
|
@@ -144,17 +205,22 @@ export const resolveEffectiveOptions = (
|
|
|
144
205
|
});
|
|
145
206
|
}
|
|
146
207
|
}
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
208
|
+
// The access preset displaces the turn option its spec claims (codex:
|
|
209
|
+
// sandbox). An explicit value of that option alongside access refuses;
|
|
210
|
+
// the profile default yields silently in the profile loop below. Read
|
|
211
|
+
// from the descriptor, so no harness name appears here.
|
|
212
|
+
const accessSpec = h.turnOptions.access;
|
|
213
|
+
const claimed = accessSpec?.kind === "access" ? accessSpec.claims : undefined;
|
|
214
|
+
if (claimed !== undefined && resolved.access !== undefined) {
|
|
215
|
+
const hasExplicit =
|
|
216
|
+
effectiveArgs[claimed as keyof TurnOptions] !== undefined ||
|
|
217
|
+
sourceTier(claimed) !== undefined;
|
|
218
|
+
if (hasExplicit) {
|
|
153
219
|
throw new ArgvRefusalError({
|
|
154
220
|
issue: "mutually-exclusive-options",
|
|
155
221
|
harness: h.name,
|
|
156
222
|
option: "access",
|
|
157
|
-
supported: [
|
|
223
|
+
supported: [`--access or --${claimed}, not both on ${h.name}`],
|
|
158
224
|
detail: "mutual exclusion",
|
|
159
225
|
});
|
|
160
226
|
}
|
|
@@ -176,20 +242,14 @@ export const resolveEffectiveOptions = (
|
|
|
176
242
|
}
|
|
177
243
|
}
|
|
178
244
|
|
|
179
|
-
// toolMap merge per harness per canonical (project > user)
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
245
|
+
// toolMap merge per harness per canonical (project > user). The merged
|
|
246
|
+
// shape is the one shape past this point (RFC-02 change 8).
|
|
247
|
+
const mergedToolMap = mergeToolMaps({
|
|
248
|
+
user: tiers.user?.toolMap,
|
|
249
|
+
project: tiers.project?.toolMap,
|
|
250
|
+
});
|
|
183
251
|
if (Object.keys(mergedToolMap).length > 0) {
|
|
184
|
-
|
|
185
|
-
const legacy: Record<string, Record<string, string>> = {};
|
|
186
|
-
for (const [harness, per] of Object.entries(mergedToolMap)) {
|
|
187
|
-
legacy[harness] = {};
|
|
188
|
-
for (const [canon, entry] of Object.entries(per as Record<string, { native: string }>)) {
|
|
189
|
-
legacy[harness]![canon] = entry.native;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
resolved.toolMap = legacy as unknown as typeof resolved.toolMap;
|
|
252
|
+
resolved.toolMap = mergedToolMap;
|
|
193
253
|
const harnessMap = mergedToolMap[h.name];
|
|
194
254
|
if (harnessMap) {
|
|
195
255
|
for (const [canonical, entry] of Object.entries(harnessMap)) {
|
|
@@ -214,7 +274,7 @@ export const resolveEffectiveOptions = (
|
|
|
214
274
|
let allCanonical: readonly string[] | undefined;
|
|
215
275
|
const getAllCanonical = (): readonly string[] => {
|
|
216
276
|
if (allCanonical) return allCanonical;
|
|
217
|
-
allCanonical = allCanonicalNames(defaultDescriptors(), mergedToolMap
|
|
277
|
+
allCanonical = allCanonicalNames(defaultDescriptors(), mergedToolMap);
|
|
218
278
|
return allCanonical;
|
|
219
279
|
};
|
|
220
280
|
if (needsCanonical) {
|
|
@@ -236,6 +296,10 @@ export const resolveEffectiveOptions = (
|
|
|
236
296
|
|
|
237
297
|
// Profile is the floor: apply only where nothing above it set the key.
|
|
238
298
|
for (const [key, value] of Object.entries(DEFAULT_TURN_PROFILE)) {
|
|
299
|
+
if (args.isolation !== undefined && ISOLATION_OVERRIDES.some((owned) => owned === key)) {
|
|
300
|
+
provenance.push({ key, value: "disabled (tool-free isolation)", tier: "arg" });
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
239
303
|
const argsSet = effectiveArgs[key as keyof TurnOptions] !== undefined;
|
|
240
304
|
const tier = sourceTier(key);
|
|
241
305
|
if (argsSet) {
|
|
@@ -263,8 +327,8 @@ export const resolveEffectiveOptions = (
|
|
|
263
327
|
provenance.push({ key, value: "none (access preset)", tier: accessTier });
|
|
264
328
|
continue;
|
|
265
329
|
}
|
|
266
|
-
//
|
|
267
|
-
if (key ===
|
|
330
|
+
// The claimed option's profile default yields to a set access preset.
|
|
331
|
+
if (key === claimed && resolved.access !== undefined) {
|
|
268
332
|
provenance.push({ key, value: `${String(value)} (access)`, tier: "harness" });
|
|
269
333
|
continue;
|
|
270
334
|
}
|
|
@@ -280,8 +344,9 @@ export const resolveEffectiveOptions = (
|
|
|
280
344
|
// must not have the profile grant switch them back on (pi reads
|
|
281
345
|
// --tools as an enabling allowlist). The tier that turned tools off
|
|
282
346
|
// owns the skip.
|
|
283
|
-
const toolsOff = (
|
|
284
|
-
o
|
|
347
|
+
const toolsOff = (
|
|
348
|
+
o: { readonly discovery?: TurnOptions["discovery"] } | undefined,
|
|
349
|
+
): boolean => o?.discovery?.tools === false;
|
|
285
350
|
const offTier: ProvenanceTier | undefined = toolsOff(effectiveArgs)
|
|
286
351
|
? "arg"
|
|
287
352
|
: toolsOff(tiers.project)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure validation and encoding for descriptor-declared persistent-session
|
|
3
|
-
* input
|
|
3
|
+
* input, and the decoding of the session records the runner cannot read
|
|
4
|
+
* without harness knowledge. This module owns the wire records and their
|
|
5
|
+
* field names (ADR 0005, RFC-02 change 4); the execution layer branches
|
|
6
|
+
* on the closed kinds it returns and holds no field name of its own.
|
|
4
7
|
*/
|
|
5
8
|
import type {
|
|
6
9
|
HarnessDescriptor,
|
|
@@ -8,7 +11,7 @@ import type {
|
|
|
8
11
|
SessionInputKind,
|
|
9
12
|
} from "../knowledge/descriptor.js";
|
|
10
13
|
import { SESSION_INPUT_KINDS } from "../knowledge/descriptor.js";
|
|
11
|
-
import { asRecord } from "./shape.js";
|
|
14
|
+
import { asRecord, readPath } from "./shape.js";
|
|
12
15
|
|
|
13
16
|
export type SessionInputIssue = "missing-session-input-contract" | "unsupported-session-input-kind";
|
|
14
17
|
|
|
@@ -19,6 +22,11 @@ export class SessionInputRefusalError extends Error {
|
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
/** The marker ids hcn stamps on the rpc commands it writes, so their
|
|
26
|
+
* responses cannot be confused with anything user-visible. */
|
|
27
|
+
export const IDENTITY_PROBE_ID = "hcn-identity";
|
|
28
|
+
export const SEND_ID = "hcn-send";
|
|
29
|
+
|
|
22
30
|
const isSessionInputKind = (value: unknown): value is SessionInputKind =>
|
|
23
31
|
SESSION_INPUT_KINDS.some((kind) => kind === value);
|
|
24
32
|
|
|
@@ -43,8 +51,83 @@ export const encodeSessionInput = (input: SessionInputContract, text: string): s
|
|
|
43
51
|
})}\n`;
|
|
44
52
|
case "pi-rpc-prompt":
|
|
45
53
|
// Verified against pi 0.84.2 rpc (spike fixtures): a prompt command
|
|
46
|
-
// while idle
|
|
47
|
-
//
|
|
48
|
-
|
|
54
|
+
// while idle. hcn keeps no queue (ADR 0007) and writes a send when it
|
|
55
|
+
// arrives; pi refuses a bare prompt mid-run with success:false (spike
|
|
56
|
+
// fixture 05), so a mid-turn send would need streamingBehavior - a
|
|
57
|
+
// pending change, not expressed here yet.
|
|
58
|
+
return `${JSON.stringify({ id: SEND_ID, type: "prompt", message: text })}\n`;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** The record the runner writes at spawn to learn the session id, or null
|
|
63
|
+
* when the harness announces identity on its stream unprompted. pi rpc is
|
|
64
|
+
* identity-silent at startup (spike fixtures); the response echoes the
|
|
65
|
+
* marker id. */
|
|
66
|
+
export const encodeIdentityProbe = (h: HarnessDescriptor): string | null => {
|
|
67
|
+
const mode = h.sessionMode;
|
|
68
|
+
if (mode === null || mode.identityProbe === null) return null;
|
|
69
|
+
switch (mode.input.kind) {
|
|
70
|
+
case "pi-rpc-prompt":
|
|
71
|
+
return `${JSON.stringify({ id: IDENTITY_PROBE_ID, type: mode.identityProbe.command })}\n`;
|
|
72
|
+
case "claude-sdk-user-message":
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** What one parsed stdout record means to a session, as a closed kind. */
|
|
78
|
+
export type SessionRecord =
|
|
79
|
+
/** The identity probe answered with the session id. */
|
|
80
|
+
| { readonly kind: "identity"; readonly sessionId: string }
|
|
81
|
+
/** The identity probe answered without an id - surfaced, never silent. */
|
|
82
|
+
| { readonly kind: "probe-failed"; readonly message: string }
|
|
83
|
+
/** An rpc command hcn wrote was refused by the harness. */
|
|
84
|
+
| { readonly kind: "command-failed"; readonly message: string }
|
|
85
|
+
/** Protocol bookkeeping with nothing to surface. */
|
|
86
|
+
| { readonly kind: "ignored" }
|
|
87
|
+
/** The record that delimits a turn, with the harness's own error flag. */
|
|
88
|
+
| { readonly kind: "turn-end"; readonly isError: boolean }
|
|
89
|
+
/** Anything else: content for the stream decoder. */
|
|
90
|
+
| { readonly kind: "content" };
|
|
91
|
+
|
|
92
|
+
const matchesTurnEnd = (
|
|
93
|
+
record: Record<string, unknown>,
|
|
94
|
+
spec: Readonly<Record<string, string>>,
|
|
95
|
+
): boolean => Object.entries(spec).every(([key, expected]) => record[key] === expected);
|
|
96
|
+
|
|
97
|
+
export const decodeSessionRecord = (
|
|
98
|
+
h: HarnessDescriptor,
|
|
99
|
+
parsed: Record<string, unknown>,
|
|
100
|
+
): SessionRecord => {
|
|
101
|
+
const mode = h.sessionMode;
|
|
102
|
+
if (mode === null) return { kind: "content" };
|
|
103
|
+
if (mode.input.kind === "pi-rpc-prompt" && parsed.type === "response") {
|
|
104
|
+
const probe = mode.identityProbe;
|
|
105
|
+
if (
|
|
106
|
+
probe !== null &&
|
|
107
|
+
parsed.id === IDENTITY_PROBE_ID &&
|
|
108
|
+
parsed.command === probe.command &&
|
|
109
|
+
parsed.success === true
|
|
110
|
+
) {
|
|
111
|
+
const announced = readPath(parsed, probe.responseIdField);
|
|
112
|
+
return typeof announced === "string"
|
|
113
|
+
? { kind: "identity", sessionId: announced }
|
|
114
|
+
: { kind: "probe-failed", message: "identity probe response carried no sessionId" };
|
|
115
|
+
}
|
|
116
|
+
// A failed command is a surfaced error, never a silent drop (spike:
|
|
117
|
+
// mid-stream prompts fail with success:false naming the remedy).
|
|
118
|
+
if (parsed.success === false) {
|
|
119
|
+
return {
|
|
120
|
+
kind: "command-failed",
|
|
121
|
+
message: `rpc command failed: ${JSON.stringify(parsed.command)} - ${JSON.stringify(parsed.error ?? "unknown error")}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return { kind: "ignored" };
|
|
125
|
+
}
|
|
126
|
+
if (matchesTurnEnd(parsed, mode.turnEnd)) {
|
|
127
|
+
// claude's result record carries is_error; pi's agent_settled has no
|
|
128
|
+
// error flag of its own.
|
|
129
|
+
const isError = mode.input.kind === "claude-sdk-user-message" && parsed.is_error === true;
|
|
130
|
+
return { kind: "turn-end", isError };
|
|
49
131
|
}
|
|
132
|
+
return { kind: "content" };
|
|
50
133
|
};
|
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
export const asRecord = (value: unknown): Record<string, unknown> | null =>
|
|
8
8
|
typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
|
|
9
9
|
|
|
10
|
+
export const readPath = (record: Record<string, unknown>, path: string): unknown => {
|
|
11
|
+
let cursor: unknown = record;
|
|
12
|
+
for (const segment of path.split(".")) {
|
|
13
|
+
const inner = asRecord(cursor);
|
|
14
|
+
if (inner === null) return undefined;
|
|
15
|
+
cursor = inner[segment];
|
|
16
|
+
}
|
|
17
|
+
return cursor;
|
|
18
|
+
};
|
|
19
|
+
|
|
10
20
|
/**
|
|
11
21
|
* Split a command line into shell words: whitespace separates, single or
|
|
12
22
|
* double quotes group (so a flag inside quoted prompt text stays part of
|
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Caller-directed skills allowlist rendering (issue #38)
|
|
3
|
-
* agent picks the subset from its own
|
|
4
|
-
* resolved
|
|
2
|
+
* Caller-directed skills allowlist rendering (issue #38), descriptor-driven
|
|
3
|
+
* (RFC-02 change 2). The delegating agent picks the subset from its own
|
|
4
|
+
* registry; the CLI supplies the resolved picks and the registry's known
|
|
5
|
+
* names; this module turns them into per-harness argv tokens by reading
|
|
6
|
+
* the descriptor's `skills` field:
|
|
5
7
|
*
|
|
6
|
-
* - pi:
|
|
7
|
-
* allowlist is exact
|
|
8
|
-
* - claude: no per-skill load flag; the registry
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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.
|
|
8
|
+
* - a load flag (pi): the discovery.skills facet off, then one load per
|
|
9
|
+
* pick - the allowlist is exact, only the caller's picks load.
|
|
10
|
+
* - overrides via settings (claude): no per-skill load flag; the registry
|
|
11
|
+
* is already present, so the allowlist renders as the complement OFF -
|
|
12
|
+
* `--settings '{"skillOverrides":{"<name>":"off",...}}'` for every known
|
|
13
|
+
* skill except the picks.
|
|
14
|
+
* - overrides via a config array (codex): the complement OFF through
|
|
15
|
+
* `-c skills.config=[{path="...", enabled=false}]`. Uses the `path`
|
|
16
|
+
* selector because a skill's frontmatter `name` need not equal its
|
|
17
|
+
* directory basename. The path for skill <n> under root <root> is
|
|
18
|
+
* <root>/<n>/SKILL.md, the root taken from the picks (all share one).
|
|
19
|
+
* - null (muse): refuse, with the support list derived like every other
|
|
20
|
+
* refusal's.
|
|
22
21
|
*/
|
|
23
22
|
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
23
|
+
import { tokensFor } from "../knowledge/descriptor.js";
|
|
24
|
+
import { defaultDescriptors } from "../knowledge/overrides.js";
|
|
24
25
|
import { ArgvRefusalError } from "./refusal.js";
|
|
26
|
+
import { supportedBy } from "./support.js";
|
|
27
|
+
|
|
28
|
+
/** The skills turn option: the caller's resolved picks (absolute paths)
|
|
29
|
+
* and every skill name in the registry they came from. */
|
|
30
|
+
export interface SkillsSelection {
|
|
31
|
+
readonly picks: readonly string[];
|
|
32
|
+
readonly known: readonly string[];
|
|
33
|
+
}
|
|
25
34
|
|
|
26
35
|
export const basenameOf = (p: string): string => {
|
|
27
36
|
const i = p.lastIndexOf("/");
|
|
@@ -33,11 +42,32 @@ const dirnameOf = (p: string): string => {
|
|
|
33
42
|
return i === -1 ? "" : p.slice(0, i);
|
|
34
43
|
};
|
|
35
44
|
|
|
45
|
+
/** The known names that are not picks, in registry order. */
|
|
46
|
+
const complementOf = (skills: SkillsSelection): readonly string[] => {
|
|
47
|
+
const picks = new Set(skills.picks.map(basenameOf));
|
|
48
|
+
return skills.known.filter((name) => !picks.has(name));
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const settingsOverrides = (skills: SkillsSelection): readonly string[] => {
|
|
52
|
+
const offs: Record<string, string> = {};
|
|
53
|
+
for (const name of complementOf(skills)) offs[name] = "off";
|
|
54
|
+
return ["--settings", JSON.stringify({ skillOverrides: offs })];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const configSkillsArray = (skills: SkillsSelection): readonly string[] => {
|
|
58
|
+
const root = skills.picks.length > 0 ? dirnameOf(skills.picks[0] as string) : "";
|
|
59
|
+
const entries = complementOf(skills).map((name) => {
|
|
60
|
+
const path = root ? `${root}/${name}/SKILL.md` : `${name}/SKILL.md`;
|
|
61
|
+
return `{path=${JSON.stringify(path)}, enabled=false}`;
|
|
62
|
+
});
|
|
63
|
+
return entries.length === 0 ? [] : ["-c", `skills.config=[${entries.join(", ")}]`];
|
|
64
|
+
};
|
|
65
|
+
|
|
36
66
|
export const renderSkillsSelection = (
|
|
37
67
|
h: HarnessDescriptor,
|
|
38
|
-
skills:
|
|
68
|
+
skills: SkillsSelection,
|
|
39
69
|
): readonly string[] => {
|
|
40
|
-
if (skills.length === 0) return [];
|
|
70
|
+
if (skills.picks.length === 0) return [];
|
|
41
71
|
|
|
42
72
|
if (h.skills === null) {
|
|
43
73
|
throw new ArgvRefusalError({
|
|
@@ -45,67 +75,33 @@ export const renderSkillsSelection = (
|
|
|
45
75
|
harness: h.name,
|
|
46
76
|
option: "skills",
|
|
47
77
|
supported: ["caller-directed skill sets"],
|
|
48
|
-
supportedBy:
|
|
49
|
-
{ harness: "pi", spelling: "--skill" },
|
|
50
|
-
{ harness: "claude", spelling: "skillOverrides" },
|
|
51
|
-
{ harness: "codex", spelling: "-c skills.config" },
|
|
52
|
-
],
|
|
78
|
+
supportedBy: supportedBy(defaultDescriptors(), "skills"),
|
|
53
79
|
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",
|
|
54
80
|
});
|
|
55
81
|
}
|
|
56
82
|
|
|
57
83
|
if (h.skills.loadFlag !== null) {
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
84
|
+
// Discovery off so ONLY the picks load; the off spelling is the
|
|
85
|
+
// descriptor's own discovery.skills facet render.
|
|
86
|
+
const facet =
|
|
87
|
+
h.turnOptions.discovery?.kind === "discovery"
|
|
88
|
+
? h.turnOptions.discovery.facets.skills
|
|
89
|
+
: undefined;
|
|
90
|
+
const tokens: string[] = facet === undefined ? [] : [...tokensFor(facet.render)];
|
|
91
|
+
for (const path of skills.picks) tokens.push(h.skills.loadFlag, path);
|
|
61
92
|
return tokens;
|
|
62
93
|
}
|
|
63
94
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
knownSkills: readonly string[],
|
|
75
|
-
pickedPaths: readonly string[],
|
|
76
|
-
): string[] => {
|
|
77
|
-
const picks = new Set(pickedPaths.map(basenameOf));
|
|
78
|
-
const offs: Record<string, string> = {};
|
|
79
|
-
for (const name of knownSkills) {
|
|
80
|
-
if (!picks.has(name)) offs[name] = "off";
|
|
81
|
-
}
|
|
82
|
-
const json = JSON.stringify({ skillOverrides: offs });
|
|
83
|
-
return ["--settings", json];
|
|
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}`);
|
|
95
|
+
switch (h.skills.overridesVia) {
|
|
96
|
+
case "settings-skilloverrides":
|
|
97
|
+
return settingsOverrides(skills);
|
|
98
|
+
case "config-skills-array":
|
|
99
|
+
return configSkillsArray(skills);
|
|
100
|
+
case null:
|
|
101
|
+
return [];
|
|
102
|
+
default: {
|
|
103
|
+
const exhaustive: never = h.skills.overridesVia;
|
|
104
|
+
return exhaustive;
|
|
107
105
|
}
|
|
108
106
|
}
|
|
109
|
-
if (entries.length === 0) return [];
|
|
110
|
-
return ["-c", `skills.config=[${entries.join(", ")}]`];
|
|
111
107
|
};
|
|
@@ -27,21 +27,34 @@ const spellingOf = (h: HarnessDescriptor, option: RefusalOption): string | null
|
|
|
27
27
|
switch (option) {
|
|
28
28
|
case "access": {
|
|
29
29
|
const spec = h.turnOptions.access;
|
|
30
|
-
if (spec === undefined) return null;
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
return null;
|
|
30
|
+
if (spec === undefined || spec.kind !== "access") return null;
|
|
31
|
+
const read = spec.renders.read;
|
|
32
|
+
if (read === "tool-preset") return h.tools.includeFlag ?? h.tools.excludeFlag ?? null;
|
|
33
|
+
if (read === null) return null;
|
|
34
|
+
return read.render.kind === "flag-list" ? (read.render.flags[0] ?? null) : read.render.flag;
|
|
36
35
|
}
|
|
37
36
|
case "tools":
|
|
38
37
|
return h.tools.includeFlag;
|
|
39
38
|
case "excludeTools":
|
|
40
39
|
return h.tools.excludeFlag;
|
|
40
|
+
case "skills": {
|
|
41
|
+
if (h.skills === null) return null;
|
|
42
|
+
if (h.skills.loadFlag !== null) return h.skills.loadFlag;
|
|
43
|
+
switch (h.skills.overridesVia) {
|
|
44
|
+
case "settings-skilloverrides":
|
|
45
|
+
return "skillOverrides";
|
|
46
|
+
case "config-skills-array":
|
|
47
|
+
return "-c skills.config";
|
|
48
|
+
default:
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
41
52
|
case "autonomy":
|
|
42
53
|
return h.autonomy?.flag ?? null;
|
|
54
|
+
case "isolation":
|
|
43
55
|
case "effort":
|
|
44
56
|
case "sandbox":
|
|
57
|
+
case "contextWindow":
|
|
45
58
|
case "provider":
|
|
46
59
|
case "write":
|
|
47
60
|
case "shell":
|
|
@@ -112,8 +125,10 @@ export const recognizeNativeSpelling = (
|
|
|
112
125
|
"tools",
|
|
113
126
|
"excludeTools",
|
|
114
127
|
"autonomy",
|
|
128
|
+
"isolation",
|
|
115
129
|
"effort",
|
|
116
130
|
"sandbox",
|
|
131
|
+
"contextWindow",
|
|
117
132
|
"provider",
|
|
118
133
|
"write",
|
|
119
134
|
"shell",
|