@dungle-scrubs/harness-cli-normalizer 0.3.0 → 0.4.0
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 +70 -86
- package/dist/cli/args.d.ts +11 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +23 -5
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config.d.ts +25 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +139 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +25 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/inspect.d.ts.map +1 -1
- package/dist/cli/inspect.js +35 -1
- package/dist/cli/inspect.js.map +1 -1
- package/dist/cli/render.d.ts.map +1 -1
- package/dist/cli/render.js +10 -1
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +134 -4
- package/dist/cli/run.js.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/execution/failure.d.ts +21 -4
- package/dist/execution/failure.d.ts.map +1 -1
- package/dist/execution/failure.js +26 -3
- package/dist/execution/failure.js.map +1 -1
- package/dist/execution/stream-turn.d.ts +7 -1
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +43 -20
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/interpretation/argv.d.ts +1 -0
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +13 -19
- package/dist/interpretation/argv.js.map +1 -1
- package/dist/interpretation/hints.d.ts +22 -0
- package/dist/interpretation/hints.d.ts.map +1 -0
- package/dist/interpretation/hints.js +59 -0
- package/dist/interpretation/hints.js.map +1 -0
- package/dist/interpretation/refusal.d.ts +25 -4
- package/dist/interpretation/refusal.d.ts.map +1 -1
- package/dist/interpretation/refusal.js +16 -0
- package/dist/interpretation/refusal.js.map +1 -1
- package/dist/interpretation/resolve-options.d.ts +42 -0
- package/dist/interpretation/resolve-options.d.ts.map +1 -0
- package/dist/interpretation/resolve-options.js +140 -0
- package/dist/interpretation/resolve-options.js.map +1 -0
- package/dist/interpretation/store.js +1 -1
- package/dist/interpretation/support.d.ts +32 -0
- package/dist/interpretation/support.d.ts.map +1 -0
- package/dist/interpretation/support.js +107 -0
- package/dist/interpretation/support.js.map +1 -0
- package/dist/interpretation/tool-selection.d.ts +29 -0
- package/dist/interpretation/tool-selection.d.ts.map +1 -0
- package/dist/interpretation/tool-selection.js +122 -0
- package/dist/interpretation/tool-selection.js.map +1 -0
- package/dist/interpretation/turn-options.d.ts.map +1 -1
- package/dist/interpretation/turn-options.js +4 -0
- package/dist/interpretation/turn-options.js.map +1 -1
- package/dist/knowledge/claude-code.d.ts +2 -2
- package/dist/knowledge/claude-code.d.ts.map +1 -1
- package/dist/knowledge/claude-code.js +37 -4
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +19 -0
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +32 -0
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/muse.d.ts.map +1 -1
- package/dist/knowledge/muse.js +22 -0
- package/dist/knowledge/muse.js.map +1 -1
- package/dist/knowledge/pi.d.ts +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +29 -6
- package/dist/knowledge/pi.js.map +1 -1
- package/dist/knowledge/profile.d.ts +13 -0
- package/dist/knowledge/profile.d.ts.map +1 -0
- package/dist/knowledge/profile.js +31 -0
- package/dist/knowledge/profile.js.map +1 -0
- package/package.json +2 -23
- package/src/cli/args.ts +31 -1
- package/src/cli/config.ts +166 -0
- package/src/cli/index.ts +25 -12
- package/src/cli/inspect.ts +34 -1
- package/src/cli/render.ts +10 -1
- package/src/cli/run.ts +161 -5
- package/src/execution/failure.ts +44 -5
- package/src/execution/stream-turn.ts +43 -14
- package/src/interpretation/argv.ts +14 -19
- package/src/interpretation/hints.ts +99 -0
- package/src/interpretation/refusal.ts +32 -3
- package/src/interpretation/resolve-options.ts +190 -0
- package/src/interpretation/store.ts +1 -1
- package/src/interpretation/support.ts +119 -0
- package/src/interpretation/tool-selection.ts +161 -0
- package/src/interpretation/turn-options.ts +4 -0
- package/src/knowledge/claude-code.ts +37 -4
- package/src/knowledge/codex.ts +19 -0
- package/src/knowledge/descriptor.ts +32 -0
- package/src/knowledge/muse.ts +22 -0
- package/src/knowledge/pi.ts +29 -6
- package/src/knowledge/profile.ts +33 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -8
- package/dist/index.js.map +0 -1
- package/src/index.ts +0 -7
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-harness support derivation: pure functions that answer "which
|
|
3
|
+
* harnesses express this option, and under what native spelling" from the
|
|
4
|
+
* descriptor set. Phase 3 (D7): refusal diagnostics derive support lists at
|
|
5
|
+
* runtime instead of hardcoding them, so a descriptor edit can never leave
|
|
6
|
+
* a refusal message stale. The autonomy refusal in argv.ts was the
|
|
7
|
+
* counter-pattern - a hardcoded flag array that drifts on descriptor
|
|
8
|
+
* change.
|
|
9
|
+
*
|
|
10
|
+
* Interpretation-layer pure function; takes the descriptor set as an
|
|
11
|
+
* argument rather than importing the defaults, so override sets work.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
15
|
+
import type { DescriptorSet } from "../knowledge/overrides.js";
|
|
16
|
+
import type { RefusalOption } from "./refusal.js";
|
|
17
|
+
|
|
18
|
+
export interface SupportEntry {
|
|
19
|
+
readonly harness: string;
|
|
20
|
+
/** The native spelling a caller would use directly on that harness. */
|
|
21
|
+
readonly spelling: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const spellingOf = (h: HarnessDescriptor, option: RefusalOption): string | null => {
|
|
25
|
+
switch (option) {
|
|
26
|
+
case "tools":
|
|
27
|
+
return h.tools.includeFlag;
|
|
28
|
+
case "excludeTools":
|
|
29
|
+
return h.tools.excludeFlag;
|
|
30
|
+
case "autonomy":
|
|
31
|
+
return h.autonomy?.flag ?? null;
|
|
32
|
+
case "effort":
|
|
33
|
+
case "sandbox":
|
|
34
|
+
case "provider":
|
|
35
|
+
case "write":
|
|
36
|
+
case "shell":
|
|
37
|
+
case "maxSteps":
|
|
38
|
+
case "discovery": {
|
|
39
|
+
const spec = h.turnOptions[option];
|
|
40
|
+
if (spec === undefined) return null;
|
|
41
|
+
const rawRender =
|
|
42
|
+
spec.kind === "discovery"
|
|
43
|
+
? (
|
|
44
|
+
Object.values(spec.facets)[0] as
|
|
45
|
+
| { render?: { flag?: string; flags?: string[] } }
|
|
46
|
+
| undefined
|
|
47
|
+
)?.render
|
|
48
|
+
: (spec as { render?: { flag?: string; flags?: string[] } }).render;
|
|
49
|
+
if (rawRender === undefined) return null;
|
|
50
|
+
// flag-value/config-kv carry `flag`; flag-list carries `flags` - the
|
|
51
|
+
// first entry is the spelling a caller would type.
|
|
52
|
+
return rawRender.flag ?? rawRender.flags?.[0] ?? null;
|
|
53
|
+
}
|
|
54
|
+
default:
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Every harness in the set that can express `option`, with its native
|
|
60
|
+
* spelling. Ordered by the set's insertion order (defaults: claude, codex,
|
|
61
|
+
* pi, muse) so output is stable across calls. */
|
|
62
|
+
export const supportedBy = (set: DescriptorSet, option: RefusalOption): readonly SupportEntry[] => {
|
|
63
|
+
const out: SupportEntry[] = [];
|
|
64
|
+
for (const h of Object.values(set)) {
|
|
65
|
+
if (h === undefined) continue;
|
|
66
|
+
const spelling = spellingOf(h, option);
|
|
67
|
+
if (spelling !== null) out.push({ harness: h.name, spelling });
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Reverse lookup for native-spelling recognition (D7 part B): given a raw
|
|
73
|
+
* flag token a caller typed, find the option it belongs to and which
|
|
74
|
+
* harnesses spell it that way. Returns null for tokens no descriptor
|
|
75
|
+
* knows - those keep the plain unknown-flag error. */
|
|
76
|
+
export const recognizeNativeSpelling = (
|
|
77
|
+
set: DescriptorSet,
|
|
78
|
+
flag: string,
|
|
79
|
+
): { readonly option: RefusalOption; readonly entries: readonly SupportEntry[] } | null => {
|
|
80
|
+
const normalized = flag.toLowerCase();
|
|
81
|
+
const candidates: RefusalOption[] = [
|
|
82
|
+
"tools",
|
|
83
|
+
"excludeTools",
|
|
84
|
+
"autonomy",
|
|
85
|
+
"effort",
|
|
86
|
+
"sandbox",
|
|
87
|
+
"provider",
|
|
88
|
+
"write",
|
|
89
|
+
"shell",
|
|
90
|
+
"maxSteps",
|
|
91
|
+
];
|
|
92
|
+
for (const option of candidates) {
|
|
93
|
+
const entries = supportedBy(set, option).filter((e) => e.spelling.toLowerCase() === normalized);
|
|
94
|
+
if (entries.length > 0) return { option, entries };
|
|
95
|
+
}
|
|
96
|
+
// Discovery facets: single-dash spellings (pi -nt/-nc/-ne/-ns) and
|
|
97
|
+
// claude's --setting-sources. Facet spellings map to the facet name so
|
|
98
|
+
// the redirect can name the normalized spelling.
|
|
99
|
+
const FACET_KEYS = ["tools", "instructionFiles", "extensions", "skills"] as const;
|
|
100
|
+
for (const facet of FACET_KEYS) {
|
|
101
|
+
const entries: SupportEntry[] = [];
|
|
102
|
+
for (const h of Object.values(set)) {
|
|
103
|
+
if (h === undefined) continue;
|
|
104
|
+
const spec = h.turnOptions.discovery;
|
|
105
|
+
if (spec === undefined || spec.kind !== "discovery") continue;
|
|
106
|
+
const facetSpec = spec.facets[facet];
|
|
107
|
+
if (facetSpec === undefined) continue;
|
|
108
|
+
const render = facetSpec.render as { flag?: string; flags?: string[] };
|
|
109
|
+
const spelling = render.flag ?? render.flags?.[0];
|
|
110
|
+
if (spelling !== undefined && spelling.toLowerCase() === normalized) {
|
|
111
|
+
entries.push({ harness: h.name, spelling });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (entries.length > 0) {
|
|
115
|
+
return { option: `discovery.${facet}` as RefusalOption, entries };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-selection rendering: turns normalized include/exclude lists into the
|
|
3
|
+
* per-harness argv tokens, per decisions D1-D3 and the Phase 0 evidence.
|
|
4
|
+
*
|
|
5
|
+
* Semantics:
|
|
6
|
+
* - include: exact allowlist over the descriptor's curated names. pi's flag
|
|
7
|
+
* is strict over built-ins, rendered directly. claude's include flag only
|
|
8
|
+
* pre-approves without restricting the visible set, so an exact allowlist
|
|
9
|
+
* on claude renders as a deny-complement (all known minus included).
|
|
10
|
+
* - exclude: the complement of the named tools over all descriptor-known
|
|
11
|
+
* names (D2). pi's native exclude subtracts from its default set (4
|
|
12
|
+
* tools), so a D2 exclude renders as a computed include list there.
|
|
13
|
+
* - mutual exclusion (D1): both flags in one call refuse.
|
|
14
|
+
* - extensible rule (D3): curated names validate and map; unknown
|
|
15
|
+
* clean-selector names pass through and are reported as unmapped so
|
|
16
|
+
* provenance can surface them.
|
|
17
|
+
*/
|
|
18
|
+
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
19
|
+
import { defaultDescriptors } from "../knowledge/overrides.js";
|
|
20
|
+
import { ArgvRefusalError } from "./refusal.js";
|
|
21
|
+
import { supportedBy } from "./support.js";
|
|
22
|
+
|
|
23
|
+
export const TOOL_SELECTOR = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,127}$/;
|
|
24
|
+
|
|
25
|
+
export interface ToolSelection {
|
|
26
|
+
readonly include?: readonly string[];
|
|
27
|
+
readonly exclude?: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RenderedToolSelection {
|
|
31
|
+
readonly tokens: readonly string[];
|
|
32
|
+
readonly unmapped: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const validateNames = (h: HarnessDescriptor, names: readonly string[]): string[] => {
|
|
36
|
+
for (const name of names) {
|
|
37
|
+
if (name.trim() === "" || name.includes(",")) {
|
|
38
|
+
throw new ArgvRefusalError({
|
|
39
|
+
issue: "invalid-tool-grant",
|
|
40
|
+
harness: h.name,
|
|
41
|
+
supported: ["non-empty, comma-free tool names"],
|
|
42
|
+
detail: `tools=${JSON.stringify(names)}`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (!TOOL_SELECTOR.test(name)) {
|
|
46
|
+
throw new ArgvRefusalError({
|
|
47
|
+
issue: "invalid-tool-grant",
|
|
48
|
+
harness: h.name,
|
|
49
|
+
supported: [`must match ${TOOL_SELECTOR.source}`],
|
|
50
|
+
detail: name,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...names];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Curated-name mapping plus extensible pass-through; returns the effective
|
|
58
|
+
* names and which of them the descriptor could not vouch for. */
|
|
59
|
+
const resolveNames = (
|
|
60
|
+
h: HarnessDescriptor,
|
|
61
|
+
names: readonly string[],
|
|
62
|
+
): { mapped: string[]; unmapped: string[] } => {
|
|
63
|
+
const known = new Set(h.tools.builtins.map((t) => t.name));
|
|
64
|
+
const mapped: string[] = [];
|
|
65
|
+
const unmapped: string[] = [];
|
|
66
|
+
for (const name of names) {
|
|
67
|
+
if (known.has(name)) mapped.push(name);
|
|
68
|
+
else unmapped.push(name);
|
|
69
|
+
}
|
|
70
|
+
return { mapped, unmapped };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const renderToolSelection = (
|
|
74
|
+
h: HarnessDescriptor,
|
|
75
|
+
selection: ToolSelection,
|
|
76
|
+
): RenderedToolSelection => {
|
|
77
|
+
const hasInclude = selection.include !== undefined;
|
|
78
|
+
const hasExclude = selection.exclude !== undefined;
|
|
79
|
+
|
|
80
|
+
// D1: mutual exclusivity, structured refusal.
|
|
81
|
+
if (hasInclude && hasExclude) {
|
|
82
|
+
throw new ArgvRefusalError({
|
|
83
|
+
issue: "mutually-exclusive-options",
|
|
84
|
+
harness: h.name,
|
|
85
|
+
option: "tools",
|
|
86
|
+
supported: ["--tools (exact allowlist) or --exclude-tools (complement), never both"],
|
|
87
|
+
detail: "mutual exclusion",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (!hasInclude && !hasExclude) return { tokens: [], unmapped: [] };
|
|
91
|
+
|
|
92
|
+
if (h.tools.includeFlag === null || h.tools.excludeFlag === null) {
|
|
93
|
+
const option = hasInclude ? "tools" : "excludeTools";
|
|
94
|
+
const by = supportedBy(defaultDescriptors(), option);
|
|
95
|
+
throw new ArgvRefusalError({
|
|
96
|
+
issue: "unsupported-option",
|
|
97
|
+
harness: h.name,
|
|
98
|
+
option,
|
|
99
|
+
supported: ["per-tool name lists"],
|
|
100
|
+
supportedBy: by,
|
|
101
|
+
hint:
|
|
102
|
+
h.name === "codex"
|
|
103
|
+
? "nearest control on codex: category switches via config keys (features.shell_tool, web_search) or sandbox modes - see `hcn inspect codex`"
|
|
104
|
+
: "nearest control on muse: category switches (--disable-write, --disable-shell, --disable-web-tools) gate tool execution per session",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (hasInclude) {
|
|
109
|
+
const names = validateNames(h, selection.include!);
|
|
110
|
+
const { mapped, unmapped } = resolveNames(h, names);
|
|
111
|
+
if (!h.tools.includeIsStrictAllowlist) {
|
|
112
|
+
// claude: exact allowlist must reshape the visible set via the deny
|
|
113
|
+
// complement (probe 2b). The include flag also carries the granted
|
|
114
|
+
// names (curated + pass-throughs) so they skip approval prompts;
|
|
115
|
+
// unmapped names ride along - the deny complement is computed over
|
|
116
|
+
// curated names only, so an extension tool in an include is granted,
|
|
117
|
+
// never denied.
|
|
118
|
+
const known = h.tools.builtins.map((t) => t.name);
|
|
119
|
+
const excluded = known.filter((n) => !mapped.includes(n));
|
|
120
|
+
const tokens: string[] = [h.tools.includeFlag!, [...mapped, ...unmapped].join(",")];
|
|
121
|
+
tokens.push(h.tools.excludeFlag!, excluded.join(","));
|
|
122
|
+
return { tokens, unmapped };
|
|
123
|
+
}
|
|
124
|
+
// pi: strict over built-ins, direct include; unmapped (extension/MCP)
|
|
125
|
+
// names ride along in the same list - pi governs them too.
|
|
126
|
+
return { tokens: [h.tools.includeFlag!, [...mapped, ...unmapped].join(",")], unmapped };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// exclude (D2): all descriptor-known names minus the excluded ones.
|
|
130
|
+
const names = validateNames(h, selection.exclude!);
|
|
131
|
+
const { mapped, unmapped: excludedUnmapped } = resolveNames(h, names);
|
|
132
|
+
if (excludedUnmapped.length > 0) {
|
|
133
|
+
// Excluding a name the descriptor does not know: permitted (it may be a
|
|
134
|
+
// runtime-registered tool), rendered natively where the harness's deny
|
|
135
|
+
// list can carry it; on claude only, since its include path consumes
|
|
136
|
+
// unknown names without effect (silent-acceptance hazard).
|
|
137
|
+
if (!h.tools.includeIsStrictAllowlist) {
|
|
138
|
+
const tokens = [h.tools.excludeFlag!, [...mapped, ...excludedUnmapped].join(",")];
|
|
139
|
+
return { tokens, unmapped: excludedUnmapped };
|
|
140
|
+
}
|
|
141
|
+
// pi: a D2 exclude renders as a computed include (its native exclude
|
|
142
|
+
// subtracts from the default set, which cannot express "all minus X"
|
|
143
|
+
// when off-by-default tools are involved). An unknown excluded name
|
|
144
|
+
// cannot be complemented - refuse rather than silently grant it.
|
|
145
|
+
throw new ArgvRefusalError({
|
|
146
|
+
issue: "unknown-tool-name",
|
|
147
|
+
harness: h.name,
|
|
148
|
+
option: "excludeTools",
|
|
149
|
+
supported: [`known tool names: ${h.tools.builtins.map((t) => t.name).join(", ")}`],
|
|
150
|
+
detail: `cannot exclude unknown name(s) ${excludedUnmapped.join(", ")}: the complement cannot be computed`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const known = h.tools.builtins.filter((t) => !mapped.includes(t.name));
|
|
154
|
+
const kept = known.map((t) => t.name);
|
|
155
|
+
if (!h.tools.includeIsStrictAllowlist) {
|
|
156
|
+
// claude: deny list IS the complement expression.
|
|
157
|
+
return { tokens: [h.tools.excludeFlag!, mapped.join(",")], unmapped: [] };
|
|
158
|
+
}
|
|
159
|
+
// pi: computed include over every known name minus the excluded.
|
|
160
|
+
return { tokens: [h.tools.includeFlag!, kept.join(",")], unmapped: [] };
|
|
161
|
+
};
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type { HarnessDescriptor, OptionRender, SpecBase } from "../knowledge/descriptor.js";
|
|
13
13
|
import { DISCOVERY_FACETS, resolveRender, TURN_OPTION_KEYS } from "../knowledge/descriptor.js";
|
|
14
14
|
import type { DiscoveryOptions, TurnOptions } from "./argv.js";
|
|
15
|
+
import { hintFor } from "./hints.js";
|
|
15
16
|
import { ArgvRefusalError } from "./refusal.js";
|
|
16
17
|
import { CLEAN_SELECTOR, resolveModel, validateEffort } from "./vocabulary.js";
|
|
17
18
|
|
|
@@ -62,6 +63,7 @@ export const renderTurnOptions = (
|
|
|
62
63
|
option: "discovery",
|
|
63
64
|
supported: Object.keys(h.turnOptions).length ? Object.keys(h.turnOptions) : ["(none)"],
|
|
64
65
|
detail: String(requested[0]),
|
|
66
|
+
hint: hintFor(h.name, `discovery.${String(requested[0])}`),
|
|
65
67
|
});
|
|
66
68
|
}
|
|
67
69
|
if (spec.kind !== "discovery") {
|
|
@@ -109,6 +111,7 @@ export const renderTurnOptions = (
|
|
|
109
111
|
option: "discovery",
|
|
110
112
|
facet,
|
|
111
113
|
supported: supportedFacets.length ? supportedFacets : ["(none)"],
|
|
114
|
+
hint: hintFor(h.name, `discovery.${facet}`),
|
|
112
115
|
});
|
|
113
116
|
}
|
|
114
117
|
continue;
|
|
@@ -199,6 +202,7 @@ export const renderTurnOptions = (
|
|
|
199
202
|
option: key,
|
|
200
203
|
supported: Object.keys(h.turnOptions).length ? Object.keys(h.turnOptions) : ["(none)"],
|
|
201
204
|
detail: String(raw),
|
|
205
|
+
hint: hintFor(h.name, key),
|
|
202
206
|
});
|
|
203
207
|
}
|
|
204
208
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The claude-code descriptor: facts about the `claude` CLI as data, verified
|
|
3
|
-
* against claude 2.1.
|
|
3
|
+
* against claude 2.1.233 and the 00-chat-substrate spike evidence (A-001,
|
|
4
4
|
* A-002, A-005). No process logic lives here.
|
|
5
5
|
*
|
|
6
|
-
* Discovery: claude 2.1.
|
|
6
|
+
* Discovery: claude 2.1.233 has no isolated instruction-file toggle.
|
|
7
7
|
* `--bare` would stop `CLAUDE.md` discovery, but it also skips hooks, LSP,
|
|
8
8
|
* plugin sync, auto-memory and keychain reads, and forces auth to
|
|
9
9
|
* `ANTHROPIC_API_KEY` / `apiKeyHelper` (OAuth never read) - so a caller
|
|
@@ -21,7 +21,7 @@ import { SHARED_AUTH_MATCHERS, SHARED_LIMIT_MATCHERS } from "./matchers.js";
|
|
|
21
21
|
export const claudeCode: HarnessDescriptor = deepFreeze({
|
|
22
22
|
name: "claude",
|
|
23
23
|
bin: "claude",
|
|
24
|
-
verifiedAgainst: "2.1.
|
|
24
|
+
verifiedAgainst: "2.1.233",
|
|
25
25
|
versionSource: { kind: "npm", package: "@anthropic-ai/claude-code" },
|
|
26
26
|
launch: {
|
|
27
27
|
baseFlags: ["-p"],
|
|
@@ -145,10 +145,43 @@ export const claudeCode: HarnessDescriptor = deepFreeze({
|
|
|
145
145
|
render: { kind: "flag-list", flags: ["--setting-sources", "project"] },
|
|
146
146
|
},
|
|
147
147
|
skills: {
|
|
148
|
+
// Phase 0 (claude-tool-interplay.md probe 4): --disable-slash-commands
|
|
149
|
+
// removes the Skill tool AND all skills listing - verified a full
|
|
150
|
+
// skills-off switch, not just command dispatch. --setting-sources
|
|
151
|
+
// project stays the extensions-facet spelling (settings scope).
|
|
148
152
|
polarity: "disables",
|
|
149
|
-
render: { kind: "flag-list", flags: ["--
|
|
153
|
+
render: { kind: "flag-list", flags: ["--disable-slash-commands"] },
|
|
150
154
|
},
|
|
151
155
|
},
|
|
152
156
|
},
|
|
153
157
|
},
|
|
158
|
+
// Phase 0 fixtures: claude-tool-interplay.md. include is a permission
|
|
159
|
+
// grant (Bash, Edit stay visible under --allowedTools Read); only the
|
|
160
|
+
// disallow flag reshapes the model-visible set. Both flags together
|
|
161
|
+
// compose, deny winning on overlap. Patterns (Bash(git *)) valid in both
|
|
162
|
+
// lists; unknown PATTERN spellings warn on stderr, unknown exact names
|
|
163
|
+
// are the silent-acceptance hazard the curated vocabulary guards.
|
|
164
|
+
tools: {
|
|
165
|
+
includeFlag: "--allowedTools",
|
|
166
|
+
excludeFlag: "--disallowedTools",
|
|
167
|
+
includeIsStrictAllowlist: false,
|
|
168
|
+
composable: true,
|
|
169
|
+
builtins: [
|
|
170
|
+
{ name: "Bash", defaultEnabled: true },
|
|
171
|
+
{ name: "Edit", defaultEnabled: true },
|
|
172
|
+
{ name: "Glob", defaultEnabled: true },
|
|
173
|
+
{ name: "Grep", defaultEnabled: true },
|
|
174
|
+
{ name: "Read", defaultEnabled: true },
|
|
175
|
+
{ name: "Write", defaultEnabled: true },
|
|
176
|
+
{ name: "WebFetch", defaultEnabled: true },
|
|
177
|
+
{ name: "WebSearch", defaultEnabled: true },
|
|
178
|
+
{ name: "Monitor", defaultEnabled: true },
|
|
179
|
+
{ name: "Task", defaultEnabled: true },
|
|
180
|
+
{ name: "Skill", defaultEnabled: true },
|
|
181
|
+
{ name: "NotebookEdit", defaultEnabled: true },
|
|
182
|
+
{ name: "LSP", defaultEnabled: true },
|
|
183
|
+
],
|
|
184
|
+
categories: [],
|
|
185
|
+
denySemantics: "remove-from-set",
|
|
186
|
+
},
|
|
154
187
|
});
|
package/src/knowledge/codex.ts
CHANGED
|
@@ -114,4 +114,23 @@ export const codexCli: HarnessDescriptor = deepFreeze({
|
|
|
114
114
|
resumeRender: null,
|
|
115
115
|
},
|
|
116
116
|
},
|
|
117
|
+
// Phase 0 fixtures: codex-tool-surface.md. No name lists anywhere -
|
|
118
|
+
// not on the CLI, not in config.toml. Control is feature booleans
|
|
119
|
+
// (reachable per-call via -c key=value / --enable/--disable FEATURE),
|
|
120
|
+
// sandbox, and approval policy. MCP servers do have per-tool keys
|
|
121
|
+
// (mcp_servers.<id>.tools.<tool>) but built-ins do not.
|
|
122
|
+
tools: {
|
|
123
|
+
includeFlag: null,
|
|
124
|
+
excludeFlag: null,
|
|
125
|
+
includeIsStrictAllowlist: false,
|
|
126
|
+
composable: false,
|
|
127
|
+
builtins: [],
|
|
128
|
+
categories: [
|
|
129
|
+
{ key: "shell", disableFlag: null, configKey: "features.shell_tool" },
|
|
130
|
+
{ key: "exec", disableFlag: null, configKey: "features.unified_exec" },
|
|
131
|
+
{ key: "web", disableFlag: null, configKey: "web_search" },
|
|
132
|
+
{ key: "view-image", disableFlag: null, configKey: "tools.view_image" },
|
|
133
|
+
],
|
|
134
|
+
denySemantics: "no-lists",
|
|
135
|
+
},
|
|
117
136
|
});
|
|
@@ -340,4 +340,36 @@ export interface HarnessDescriptor {
|
|
|
340
340
|
* harness; a call passing them must refuse. Discovery is a table of
|
|
341
341
|
* per-facet specs rather than a single flag. */
|
|
342
342
|
readonly turnOptions: Readonly<Partial<Record<TurnOptionKey, TurnOptionSpec>>>;
|
|
343
|
+
/** Tool-selection surface, grounded in test/fixtures/phase0/ evidence.
|
|
344
|
+
* `includeFlag`/`excludeFlag` are the per-tool NAME-LIST flags (null when
|
|
345
|
+
* the harness has none - codex/muse). `includeIsStrictAllowlist` records
|
|
346
|
+
* the claude asymmetry: claude's include flag pre-approves without
|
|
347
|
+
* restricting the visible set, so an exact allowlist must render as a
|
|
348
|
+
* disallow-complement there; pi's include IS strict (over built-ins).
|
|
349
|
+
* `composable`: both flags legal at once (pi: exclude subtracts from
|
|
350
|
+
* include). `builtins`: curated names + default-enabled state - grep/find/
|
|
351
|
+
* ls ship off on pi, everything ships on elsewhere. `categories`:
|
|
352
|
+
* non-list switches (muse disable flags, codex config booleans).
|
|
353
|
+
* `denySemantics`: whether a deny removes the tool from the model-visible
|
|
354
|
+
* set (claude/pi), policy-gates execution while the tool stays listed
|
|
355
|
+
* (muse), or there are no lists to deny with (codex). Extension and MCP
|
|
356
|
+
* tools register at runtime and are NEVER enumerated here - name
|
|
357
|
+
* validation is a default, not a refusal authority (same stance as the
|
|
358
|
+
* pi model registry, D-008). */
|
|
359
|
+
readonly tools: {
|
|
360
|
+
readonly includeFlag: string | null;
|
|
361
|
+
readonly excludeFlag: string | null;
|
|
362
|
+
readonly includeIsStrictAllowlist: boolean;
|
|
363
|
+
readonly composable: boolean;
|
|
364
|
+
readonly builtins: ReadonlyArray<{
|
|
365
|
+
readonly name: string;
|
|
366
|
+
readonly defaultEnabled: boolean;
|
|
367
|
+
}>;
|
|
368
|
+
readonly categories: ReadonlyArray<{
|
|
369
|
+
readonly key: "shell" | "write" | "web" | "exec" | "view-image";
|
|
370
|
+
readonly disableFlag: string | null;
|
|
371
|
+
readonly configKey: string | null;
|
|
372
|
+
}>;
|
|
373
|
+
readonly denySemantics: "remove-from-set" | "policy-gate" | "no-lists";
|
|
374
|
+
};
|
|
343
375
|
}
|
package/src/knowledge/muse.ts
CHANGED
|
@@ -14,6 +14,9 @@ export const museCode: HarnessDescriptor = deepFreeze({
|
|
|
14
14
|
name: "muse",
|
|
15
15
|
bin: "muse",
|
|
16
16
|
verifiedAgainst: "0.1.0",
|
|
17
|
+
// No npm package - `hcn check` falls back to `muse --version` locally and
|
|
18
|
+
// is skipped in CI where the binary is absent, so this harness is exempt
|
|
19
|
+
// from automated drift detection (see README Version-pinning and drift).
|
|
17
20
|
versionSource: { kind: "installed" },
|
|
18
21
|
launch: {
|
|
19
22
|
// exec --json emits the payload_type/stream records the runner decodes
|
|
@@ -107,4 +110,23 @@ export const museCode: HarnessDescriptor = deepFreeze({
|
|
|
107
110
|
render: { kind: "flag-value", flag: "--max-model-steps" },
|
|
108
111
|
},
|
|
109
112
|
},
|
|
113
|
+
// Phase 0 fixtures: muse-category-flags.md. No name lists; category
|
|
114
|
+
// switches are enforcement gates - tools stay listed in the catalog but
|
|
115
|
+
// calls are denied per session policy (unlike claude's set removal).
|
|
116
|
+
// Tool names follow muse.<name> (write_file, edit_file, bash, bash_input,
|
|
117
|
+
// add_memory, edit_memory, web_search). --disable-web-tools has no
|
|
118
|
+
// normalized turnOption yet - candidate for a web toggle or passthrough.
|
|
119
|
+
tools: {
|
|
120
|
+
includeFlag: null,
|
|
121
|
+
excludeFlag: null,
|
|
122
|
+
includeIsStrictAllowlist: false,
|
|
123
|
+
composable: false,
|
|
124
|
+
builtins: [],
|
|
125
|
+
categories: [
|
|
126
|
+
{ key: "write", disableFlag: "--disable-write", configKey: null },
|
|
127
|
+
{ key: "shell", disableFlag: "--disable-shell", configKey: null },
|
|
128
|
+
{ key: "web", disableFlag: "--disable-web-tools", configKey: null },
|
|
129
|
+
],
|
|
130
|
+
denySemantics: "policy-gate",
|
|
131
|
+
},
|
|
110
132
|
});
|
package/src/knowledge/pi.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The pi descriptor: facts about the `pi` CLI as data, verified against
|
|
3
|
-
* pi 0.84.
|
|
3
|
+
* pi 0.84.2. Descriptor groundwork only (D-003). The load-bearing scars:
|
|
4
4
|
* pi reads stdin even in -p mode (a backgrounded call without `< /dev/null`
|
|
5
5
|
* hangs forever), it auto-discovers instruction files/skills/extensions
|
|
6
6
|
* unless disabled, and its model registry is runtime-extensible (D-008) -
|
|
@@ -12,11 +12,13 @@ import { SHARED_AUTH_MATCHERS, SHARED_LIMIT_MATCHERS } from "./matchers.js";
|
|
|
12
12
|
export const piCli: HarnessDescriptor = deepFreeze({
|
|
13
13
|
name: "pi",
|
|
14
14
|
bin: "pi",
|
|
15
|
-
verifiedAgainst: "0.84.
|
|
15
|
+
verifiedAgainst: "0.84.2",
|
|
16
16
|
versionSource: { kind: "npm", package: "@earendil-works/pi-coding-agent" },
|
|
17
17
|
launch: {
|
|
18
18
|
// -p --mode json: bare -p prints plain text; --mode json emits the
|
|
19
|
-
// structured v3 records the runner decodes (verified 0.84.
|
|
19
|
+
// structured v3 records the runner decodes (verified 0.84.2; 0.84.2
|
|
20
|
+
// additionally nests a usage object inside message_update alongside
|
|
21
|
+
// assistantMessageEvent - additive, decoder unaffected).
|
|
20
22
|
baseFlags: ["-p", "--mode", "json"],
|
|
21
23
|
subcommands: [],
|
|
22
24
|
promptStyle: "positional",
|
|
@@ -35,14 +37,14 @@ export const piCli: HarnessDescriptor = deepFreeze({
|
|
|
35
37
|
onMissing: "create",
|
|
36
38
|
extraFlags: ["-p", "--mode", "json"],
|
|
37
39
|
},
|
|
38
|
-
// --mode rpc exists on 0.84.
|
|
40
|
+
// --mode rpc exists on 0.84.2 but its session semantics are unverified
|
|
39
41
|
// against a live run; per the truthfulness rule it stays null until a
|
|
40
42
|
// spike proves the contract (the claude slice is the proven vertical -
|
|
41
43
|
// D-003).
|
|
42
44
|
sessionMode: null,
|
|
43
45
|
output: {
|
|
44
46
|
// pi -p prints plain text; --mode json emits structured v3 records
|
|
45
|
-
// INCLUDING assistantMessageEvent text_delta tokens (verified 0.84.
|
|
47
|
+
// INCLUDING assistantMessageEvent text_delta tokens (verified 0.84.2),
|
|
46
48
|
// so this invocation is token-granular, not merely message.
|
|
47
49
|
pins: [{ flags: ["--mode", "json"], granularity: "token" }],
|
|
48
50
|
floor: "none",
|
|
@@ -68,7 +70,7 @@ export const piCli: HarnessDescriptor = deepFreeze({
|
|
|
68
70
|
extensible: true,
|
|
69
71
|
},
|
|
70
72
|
store: {
|
|
71
|
-
// Verified on pi 0.84.
|
|
73
|
+
// Verified on pi 0.84.2: ~/.pi/sessions/<slug>/<ISO-stamp>_<uuid>.jsonl
|
|
72
74
|
// where the slug is the cwd dash-flattened and dash-wrapped, dots
|
|
73
75
|
// preserved (--Users-kevin-dev-x--). The stamp needs a store scan, so
|
|
74
76
|
// the template names the per-cwd directory.
|
|
@@ -116,4 +118,25 @@ export const piCli: HarnessDescriptor = deepFreeze({
|
|
|
116
118
|
},
|
|
117
119
|
},
|
|
118
120
|
},
|
|
121
|
+
// Phase 0 fixtures: pi-both-tool-flags.md. Both list flags legal at once;
|
|
122
|
+
// exclude subtracts from include. --tools is strict over BUILT-INS but
|
|
123
|
+
// does not strip MCP/extension registrations (additive over them);
|
|
124
|
+
// -nbt (built-ins only off) exists but has no normalized spelling yet.
|
|
125
|
+
tools: {
|
|
126
|
+
includeFlag: "--tools",
|
|
127
|
+
excludeFlag: "--exclude-tools",
|
|
128
|
+
includeIsStrictAllowlist: true,
|
|
129
|
+
composable: true,
|
|
130
|
+
builtins: [
|
|
131
|
+
{ name: "read", defaultEnabled: true },
|
|
132
|
+
{ name: "bash", defaultEnabled: true },
|
|
133
|
+
{ name: "edit", defaultEnabled: true },
|
|
134
|
+
{ name: "write", defaultEnabled: true },
|
|
135
|
+
{ name: "grep", defaultEnabled: false },
|
|
136
|
+
{ name: "find", defaultEnabled: false },
|
|
137
|
+
{ name: "ls", defaultEnabled: false },
|
|
138
|
+
],
|
|
139
|
+
categories: [],
|
|
140
|
+
denySemantics: "remove-from-set",
|
|
141
|
+
},
|
|
119
142
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in default turn profile: hcn's curated defaults for a bare run,
|
|
3
|
+
* decided in review (2026-08-18) and applied LAUNCH-ONLY, before user config
|
|
4
|
+
* and args. Every entry here is a ratified dimension; unratified dimensions
|
|
5
|
+
* stay absent and defer to the harness.
|
|
6
|
+
*
|
|
7
|
+
* effort: "medium" - the only value present in all four effort ladders
|
|
8
|
+
* (claude/pi/muse/codex-per-model). Live probe on claude 2.1.233 showed the
|
|
9
|
+
* internal default is above medium and nondeterministic (599-1482 thinking
|
|
10
|
+
* tokens on identical tasks vs 436 at medium); pinning makes bare runs
|
|
11
|
+
* cheaper on claude, uniform everywhere, and knowable from the outside.
|
|
12
|
+
*/
|
|
13
|
+
import { deepFreeze } from "./descriptor.js";
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_TURN_PROFILE = deepFreeze({
|
|
16
|
+
effort: "medium",
|
|
17
|
+
// codex-only (the only harness with a sandbox dimension). Promotes the
|
|
18
|
+
// descriptor's implicit workspace-write default into the visible
|
|
19
|
+
// profile tier. On the other three the dimension is unrenderable and
|
|
20
|
+
// reports as divergence.
|
|
21
|
+
sandbox: "workspace-write",
|
|
22
|
+
// Ratified: discovery fully ON. The harnesses' bare runs already
|
|
23
|
+
// discover instruction files, skills, and extensions; the profile makes
|
|
24
|
+
// that a stated contract. The off-spellings exist per harness
|
|
25
|
+
// (-ns/-nc/-ne on pi, --setting-sources on claude) for callers who want
|
|
26
|
+
// less.
|
|
27
|
+
discovery: { tools: true, instructionFiles: true, extensions: true, skills: true },
|
|
28
|
+
// Ratified: autonomy OFF. No bare run is unattended; --autonomy or a
|
|
29
|
+
// config must say so deliberately.
|
|
30
|
+
autonomy: false,
|
|
31
|
+
} as const);
|
|
32
|
+
|
|
33
|
+
export type ProfileKey = keyof typeof DEFAULT_TURN_PROFILE;
|
package/dist/index.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Public package surface. The three architectural layers remain available as
|
|
3
|
-
* explicit subpath exports for consumers that want the narrowest dependency.
|
|
4
|
-
*/
|
|
5
|
-
export * from "./execution/index.js";
|
|
6
|
-
export * from "./interpretation/index.js";
|
|
7
|
-
export * from "./knowledge/index.js";
|
|
8
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,sBAAsB,CAAC"}
|
package/dist/index.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Public package surface. The three architectural layers remain available as
|
|
3
|
-
* explicit subpath exports for consumers that want the narrowest dependency.
|
|
4
|
-
*/
|
|
5
|
-
export * from "./execution/index.js";
|
|
6
|
-
export * from "./interpretation/index.js";
|
|
7
|
-
export * from "./knowledge/index.js";
|
|
8
|
-
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,sBAAsB,CAAC"}
|
package/src/index.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Public package surface. The three architectural layers remain available as
|
|
3
|
-
* explicit subpath exports for consumers that want the narrowest dependency.
|
|
4
|
-
*/
|
|
5
|
-
export * from "./execution/index.js";
|
|
6
|
-
export * from "./interpretation/index.js";
|
|
7
|
-
export * from "./knowledge/index.js";
|