@gaunt-sloth/core 2.0.0-alpha.34 → 2.0.0-alpha.36
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/dist/config/configDiscovery.d.ts +79 -0
- package/dist/config/configDiscovery.js +80 -0
- package/dist/config/configDiscovery.js.map +1 -0
- package/dist/config/loader.d.ts +6 -5
- package/dist/config/loader.js +229 -92
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +3 -2
- package/dist/config/schema.js +57 -9
- package/dist/config/schema.js.map +1 -1
- package/dist/config/shell-policy.d.ts +58 -26
- package/dist/config/shell-policy.js +60 -27
- package/dist/config/shell-policy.js.map +1 -1
- package/dist/config/tool-descriptions.d.ts +6 -6
- package/dist/config/tool-descriptions.js +7 -7
- package/dist/config/tool-descriptions.js.map +1 -1
- package/dist/config/types.d.ts +34 -31
- package/dist/config/types.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/core/GthAbstractAgent.d.ts +11 -13
- package/dist/core/GthAbstractAgent.js +9 -11
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +17 -33
- package/dist/core/GthAgentRunner.js +31 -41
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/GthLangChainAgent.js +25 -34
- package/dist/core/GthLangChainAgent.js.map +1 -1
- package/dist/core/approvals/toolAnnotationSources.d.ts +4 -4
- package/dist/core/approvals/toolAnnotationSources.js +8 -8
- package/dist/core/debugCapture.d.ts +2 -3
- package/dist/core/debugCapture.js +1 -2
- package/dist/core/debugCapture.js.map +1 -1
- package/dist/core/gthLeanAgentFactory.d.ts +4 -4
- package/dist/core/gthLeanAgentFactory.js +4 -4
- package/dist/core/reasoningBlocks.d.ts +7 -2
- package/dist/core/reasoningBlocks.js +7 -2
- package/dist/core/reasoningBlocks.js.map +1 -1
- package/dist/core/shell/ShellCommandFailedError.d.ts +3 -4
- package/dist/core/shell/ShellCommandFailedError.js +3 -4
- package/dist/core/shell/ShellCommandFailedError.js.map +1 -1
- package/dist/core/shell/approvalStop.d.ts +68 -29
- package/dist/core/shell/approvalStop.js +178 -28
- package/dist/core/shell/approvalStop.js.map +1 -1
- package/dist/core/shell/hardline.d.ts +22 -0
- package/dist/core/shell/hardline.js +81 -0
- package/dist/core/shell/hardline.js.map +1 -1
- package/dist/core/shell/negotiation.d.ts +81 -2
- package/dist/core/shell/negotiation.js +144 -11
- package/dist/core/shell/negotiation.js.map +1 -1
- package/dist/core/shell/rater.d.ts +7 -69
- package/dist/core/shell/rater.js +5 -24
- package/dist/core/shell/rater.js.map +1 -1
- package/dist/core/shell/raterVocabulary.d.ts +119 -0
- package/dist/core/shell/raterVocabulary.js +114 -0
- package/dist/core/shell/raterVocabulary.js.map +1 -0
- package/dist/core/types.d.ts +17 -3
- package/dist/providers/anthropic.js +12 -0
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/configurationPassthrough.d.ts +107 -0
- package/dist/providers/configurationPassthrough.js +148 -0
- package/dist/providers/configurationPassthrough.js.map +1 -0
- package/dist/providers/geminiThinking.d.ts +8 -0
- package/dist/providers/geminiThinking.js +33 -13
- package/dist/providers/geminiThinking.js.map +1 -1
- package/dist/providers/google-genai.js +11 -0
- package/dist/providers/google-genai.js.map +1 -1
- package/dist/providers/groq.js +12 -0
- package/dist/providers/groq.js.map +1 -1
- package/dist/providers/ollama.js +12 -0
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openrouter.d.ts +23 -0
- package/dist/providers/openrouter.js +71 -4
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/vertexai.js +12 -0
- package/dist/providers/vertexai.js.map +1 -1
- package/dist/providers/xai.js +20 -0
- package/dist/providers/xai.js.map +1 -1
- package/dist/runtime/conversation.d.ts +2 -2
- package/dist/runtime/conversation.js +19 -4
- package/dist/runtime/conversation.js.map +1 -1
- package/dist/runtime/singleShot.d.ts +2 -3
- package/dist/runtime/singleShot.js +22 -7
- package/dist/runtime/singleShot.js.map +1 -1
- package/dist/utils/systemPromptNotes.d.ts +17 -20
- package/dist/utils/systemPromptNotes.js +22 -24
- package/dist/utils/systemPromptNotes.js.map +1 -1
- package/dist/utils/untrustedText.d.ts +24 -4
- package/dist/utils/untrustedText.js +25 -4
- package/dist/utils/untrustedText.js.map +1 -1
- package/package.json +4 -4
- package/schema/gsloth-config.schema.json +0 -1
package/dist/config/loader.js
CHANGED
|
@@ -10,9 +10,10 @@ import { displayDebug, displayError, displayInfo, displayWarning, setConsoleLeve
|
|
|
10
10
|
import { findApprovalsGrammarIssues, findApprovalsRaterProfiles, findDeprecatedConfigIssues, findUnknownTopLevelKeys, formatConfigValidationError, formatDeprecatedConfigIssues, isRecordConfig, rawGthConfigSchema, unresolvedRaterProfileMessage, validateRawGthConfig, } from '#src/config/schema.js';
|
|
11
11
|
import { parseJsonc } from '#src/config/jsonc.js';
|
|
12
12
|
import { isMissingProviderKeyError, MissingProviderKeyError } from '#src/config/providerKeys.js';
|
|
13
|
+
import { ConfigDiscoveryError, isConfigDiscoveryError } from '#src/config/configDiscovery.js';
|
|
13
14
|
import { getGslothConfigReadPath, importExternalFile } from '#src/utils/fileUtils.js';
|
|
14
15
|
import { getGlobalGslothConfigReadPath } from '#src/utils/globalConfigUtils.js';
|
|
15
|
-
import { env,
|
|
16
|
+
import { env, getCurrentWorkDir, isStdoutTTY, isTTY, setProjectDir, setUseColour, } from '#src/utils/systemUtils.js';
|
|
16
17
|
import { resolveUseColour } from '#src/config/colour.js';
|
|
17
18
|
import { resolveUseMouse } from '#src/config/mouse.js';
|
|
18
19
|
import { existsSync, readFileSync } from 'node:fs';
|
|
@@ -28,7 +29,7 @@ import { DEFAULT_CONFIG } from '#src/config/defaults.js';
|
|
|
28
29
|
* 1. Deprecated-shape reject (GS2-28): a removed pre-2.0 shape — a top-level command key
|
|
29
30
|
* or a deprecated `*Provider*` name (root + per-command), detected by
|
|
30
31
|
* {@link findDeprecatedConfigIssues} — is a HARD error naming the canonical replacement +
|
|
31
|
-
* migration path
|
|
32
|
+
* migration path. 2.0 dropped back-compat coercion, so these fail rather than
|
|
32
33
|
* remap. Runs FIRST so a deprecated name never merely surfaces as an unknown-key warning.
|
|
33
34
|
* 1b. Approvals rule-grammar reject (EXT-71): a bare string in `allow`/`deny`/`escalate`, or a
|
|
34
35
|
* configured `mcpServers` key named `*`, detected by {@link findApprovalsGrammarIssues} — a
|
|
@@ -36,13 +37,25 @@ import { DEFAULT_CONFIG } from '#src/config/defaults.js';
|
|
|
36
37
|
* user wrote) is the only one they see.
|
|
37
38
|
* 2. Unknown top-level keys: warn (do NOT fail) so likely typos are surfaced while
|
|
38
39
|
* forward-compatible / extension keys still pass through untouched.
|
|
39
|
-
* 3. Schema parse: on a genuine type mismatch on a known field,
|
|
40
|
-
*
|
|
41
|
-
* behaviour). Validation is shape-only — the loose schema preserves unknown keys,
|
|
40
|
+
* 3. Schema parse: on a genuine type mismatch on a known field, a friendly, path-scoped
|
|
41
|
+
* error. Validation is shape-only — the loose schema preserves unknown keys,
|
|
42
42
|
* so the original `raw` is returned unchanged on success.
|
|
43
43
|
*
|
|
44
|
+
* CFG-36 / CFG-47 — every hard failure above RAISES a {@link ConfigDiscoveryError} rather than
|
|
45
|
+
* printing and calling `exit(1)`, and so does every other "config present and unusable" site in this
|
|
46
|
+
* file. Config loading is a library operation: the caller chooses the exit code (the CLI's top-level
|
|
47
|
+
* guard prints the message and exits 1; `gth eval` classifies it as a harness error and exits 2).
|
|
48
|
+
* **This file no longer calls `exit` at all** — that is the invariant, and it is easier to keep than
|
|
49
|
+
* a list of which sites do. Because these throw, every `catch` between here and a top level must
|
|
50
|
+
* re-raise them rather than fall through to another format or treat the layer as absent — see the
|
|
51
|
+
* {@link isConfigDiscoveryError} re-raises in {@link loadGlobalRawConfig}, {@link initConfig},
|
|
52
|
+
* {@link tryModuleConfig} and {@link tryJsonConfig}. Swallowing one would silently downgrade a hard
|
|
53
|
+
* config error to a different (or absent) config, which is the false-green this change exists to
|
|
54
|
+
* prevent.
|
|
55
|
+
*
|
|
44
56
|
* @param raw The freshly loaded config layer (read-only here).
|
|
45
57
|
* @param sourceLabel Human-readable source name for messages (e.g. the filename).
|
|
58
|
+
* @throws ConfigDiscoveryError when the layer carries a hard configuration error.
|
|
46
59
|
*/
|
|
47
60
|
function validateRawConfigLayer(raw, sourceLabel) {
|
|
48
61
|
// Only an object config can carry deprecated/unknown keys; a null/array/primitive config skips
|
|
@@ -51,11 +64,7 @@ function validateRawConfigLayer(raw, sourceLabel) {
|
|
|
51
64
|
if (isRecordConfig(raw)) {
|
|
52
65
|
const deprecatedIssues = findDeprecatedConfigIssues(raw);
|
|
53
66
|
if (deprecatedIssues.length > 0) {
|
|
54
|
-
|
|
55
|
-
exit(1);
|
|
56
|
-
// Unreachable past exit(1) in production; keeps the mocked-exit test path from falling
|
|
57
|
-
// through into the schema parse below.
|
|
58
|
-
return raw;
|
|
67
|
+
throw new ConfigDiscoveryError(`Invalid configuration in ${sourceLabel}:\n${formatDeprecatedConfigIssues(deprecatedIssues)}`, { sourceLabel });
|
|
59
68
|
}
|
|
60
69
|
// EXT-71 — the rule-grammar errors that must be seen BEFORE the schema parse: a bare string in
|
|
61
70
|
// a rule list (whose message shows the object form of that same string) and a reserved `*`
|
|
@@ -63,9 +72,7 @@ function validateRawConfigLayer(raw, sourceLabel) {
|
|
|
63
72
|
// validate` too, so the validator can never green-light a config a real run refuses.
|
|
64
73
|
const grammarIssues = findApprovalsGrammarIssues(raw);
|
|
65
74
|
if (grammarIssues.length > 0) {
|
|
66
|
-
|
|
67
|
-
exit(1);
|
|
68
|
-
return raw;
|
|
75
|
+
throw new ConfigDiscoveryError(`Invalid configuration in ${sourceLabel}:\n${formatDeprecatedConfigIssues(grammarIssues)}`, { sourceLabel });
|
|
69
76
|
}
|
|
70
77
|
const unknownKeys = findUnknownTopLevelKeys(raw);
|
|
71
78
|
if (unknownKeys.length > 0) {
|
|
@@ -76,11 +83,7 @@ function validateRawConfigLayer(raw, sourceLabel) {
|
|
|
76
83
|
}
|
|
77
84
|
const result = rawGthConfigSchema.safeParse(raw);
|
|
78
85
|
if (!result.success) {
|
|
79
|
-
|
|
80
|
-
exit(1);
|
|
81
|
-
// Unreachable past exit(1) in production; in specs exit() is mocked, so returning here keeps
|
|
82
|
-
// a shape-invalid config from falling through into the profile check below.
|
|
83
|
-
return raw;
|
|
86
|
+
throw new ConfigDiscoveryError(`Invalid configuration in ${sourceLabel}:\n${formatConfigValidationError(result.error)}`, { sourceLabel });
|
|
84
87
|
}
|
|
85
88
|
// CFG-26 — `approvals.rater` STRICT resolution (GS2-62): a named profile that does not
|
|
86
89
|
// resolve to a real profile config is a hard error, never a silent fallback to the main model.
|
|
@@ -89,10 +92,8 @@ function validateRawConfigLayer(raw, sourceLabel) {
|
|
|
89
92
|
if (isRecordConfig(raw)) {
|
|
90
93
|
for (const ref of findApprovalsRaterProfiles(raw)) {
|
|
91
94
|
if (!resolveIdentityProfileConfigPath(ref.profile)) {
|
|
92
|
-
|
|
93
|
-
` - ${ref.path}: ${unresolvedRaterProfileMessage(ref)}
|
|
94
|
-
exit(1);
|
|
95
|
-
return raw;
|
|
95
|
+
throw new ConfigDiscoveryError(`Invalid configuration in ${sourceLabel}:\n` +
|
|
96
|
+
` - ${ref.path}: ${unresolvedRaterProfileMessage(ref)}`, { sourceLabel, identityProfile: ref.profile });
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
}
|
|
@@ -240,11 +241,12 @@ const RAW_CONFIG_VALIDATION_OPTIONS = {
|
|
|
240
241
|
* config happens to be present / a global config exists," a distinction the loader's fall-through
|
|
241
242
|
* deliberately blurs.
|
|
242
243
|
*
|
|
243
|
-
* PURE PREDICATE — never throws, never calls `exit
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
* blank/whitespace-only name counts
|
|
244
|
+
* PURE PREDICATE — never throws, never calls `exit`, so it can be asked the question without
|
|
245
|
+
* committing to an outcome. {@link initConfig} uses it to enforce that an explicitly-named profile
|
|
246
|
+
* really exists (raising a catchable {@link ConfigDiscoveryError} when it does not), and callers
|
|
247
|
+
* that want to CLASSIFY rather than fail — BATCH-12's identity matrix checks every declared identity
|
|
248
|
+
* up front so one message can name them all — ask it directly. A blank/whitespace-only name counts
|
|
249
|
+
* as "no profile" → `undefined`.
|
|
248
250
|
*
|
|
249
251
|
* @param identityProfile The explicitly-requested identity profile name.
|
|
250
252
|
* @returns The resolved profile config path, or `undefined` when the profile has no config.
|
|
@@ -265,6 +267,31 @@ export function resolveIdentityProfileConfigPath(identityProfile) {
|
|
|
265
267
|
}
|
|
266
268
|
return undefined;
|
|
267
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* CFG-36 — the ONE statement of "an explicitly-named identity profile that does not exist", shared
|
|
272
|
+
* by the run path ({@link initConfig}, which raises) and the read path ({@link validateConfig},
|
|
273
|
+
* which records a not-ok layer). Single-sourced deliberately: GS2-29's invariant is that
|
|
274
|
+
* `gth config validate` can never green-light a config a real run refuses, and two copies of this
|
|
275
|
+
* rule is exactly how that invariant rots.
|
|
276
|
+
*
|
|
277
|
+
* Returns the offending profile name, or `undefined` when there is nothing to complain about —
|
|
278
|
+
* no profile named (a blank/whitespace name counts as none, keeping the CFG-8 no-profile path
|
|
279
|
+
* untouched), an explicit `--config` path (which names the file to load and bypasses discovery),
|
|
280
|
+
* or a profile that resolves to its own config.
|
|
281
|
+
*/
|
|
282
|
+
function findUnresolvedExplicitProfile(commandLineConfigOverrides) {
|
|
283
|
+
const profile = commandLineConfigOverrides.identityProfile?.trim();
|
|
284
|
+
if (!profile || commandLineConfigOverrides.customConfigPath) {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
return resolveIdentityProfileConfigPath(profile) ? undefined : profile;
|
|
288
|
+
}
|
|
289
|
+
/** The message both paths report for {@link findUnresolvedExplicitProfile}'s failure. */
|
|
290
|
+
function identityProfileNotFoundMessage(profile) {
|
|
291
|
+
return (`identity profile "${profile}" not found: no config file in ` +
|
|
292
|
+
`${GSLOTH_DIR}/${GSLOTH_SETTINGS_DIR}/${profile}/ ` +
|
|
293
|
+
`(checked ${PROJECT_CONFIG_FORMATS.join(', ')})`);
|
|
294
|
+
}
|
|
268
295
|
/**
|
|
269
296
|
* Loads the global gsloth config (if present) from the global `~/.gsloth` folder.
|
|
270
297
|
*
|
|
@@ -292,6 +319,13 @@ export async function loadGlobalRawConfig() {
|
|
|
292
319
|
return validateRawConfigLayer(parsed, `${filename} (global)`);
|
|
293
320
|
}
|
|
294
321
|
catch (e) {
|
|
322
|
+
// CFG-36 — this catch exists to treat an UNREADABLE global as absent. A global that read
|
|
323
|
+
// fine and is MALFORMED is a hard configuration error (it used to `exit(1)` from inside
|
|
324
|
+
// the validator); swallowing it here would silently downgrade that to "ignoring it" and
|
|
325
|
+
// run under a different config — the exact false-green the throw was introduced to avoid.
|
|
326
|
+
if (isConfigDiscoveryError(e)) {
|
|
327
|
+
throw e;
|
|
328
|
+
}
|
|
295
329
|
displayDebug(e instanceof Error ? e : String(e));
|
|
296
330
|
displayWarning(`Failed to read global config from ${jsonPath}, ignoring it.`);
|
|
297
331
|
return undefined;
|
|
@@ -308,6 +342,10 @@ export async function loadGlobalRawConfig() {
|
|
|
308
342
|
return validateRawConfigLayer(configured, `${filename} (global)`);
|
|
309
343
|
}
|
|
310
344
|
catch (e) {
|
|
345
|
+
// CFG-36 — see the JSON branch above: a malformed global is a hard error, not an absent one.
|
|
346
|
+
if (isConfigDiscoveryError(e)) {
|
|
347
|
+
throw e;
|
|
348
|
+
}
|
|
311
349
|
displayDebug(e instanceof Error ? e : String(e));
|
|
312
350
|
displayWarning(`Failed to read global config from ${modulePath}, ignoring it.`);
|
|
313
351
|
return undefined;
|
|
@@ -339,7 +377,8 @@ const MAX_EXTENDS_CHAIN_DEPTH = 50;
|
|
|
339
377
|
* cycle, a missing base, an over-deep chain, or an unreadable base. It carries the SAME clear,
|
|
340
378
|
* user-facing message the run path prints, so the two consumers can translate one shared failure
|
|
341
379
|
* into their own convention WITHOUT the traversal being forked or the checks duplicated:
|
|
342
|
-
* - the run path ({@link resolveConfigExtends}) →
|
|
380
|
+
* - the run path ({@link resolveConfigExtends}) → re-raised as a {@link ConfigDiscoveryError}, so
|
|
381
|
+
* the caller classifies it and chooses the exit code (CFG-36),
|
|
343
382
|
* - the read path ({@link validateConfig}) → a `not-ok` layer with this message (collect, never
|
|
344
383
|
* `exit`), so `gth config validate` mirrors what a real run would hit (GS2-29 invariant).
|
|
345
384
|
*/
|
|
@@ -383,15 +422,28 @@ export async function resolveConfigExtends(rawConfig, profileLabel) {
|
|
|
383
422
|
return await composeExtends(rawConfig, profileLabel);
|
|
384
423
|
}
|
|
385
424
|
catch (e) {
|
|
386
|
-
// GS2-73 — the traversal
|
|
387
|
-
//
|
|
388
|
-
//
|
|
425
|
+
// GS2-73 — the traversal RAISES a {@link ConfigExtendsError} rather than exiting inline, so the
|
|
426
|
+
// read side ({@link validateConfig}) can report the same failure without terminating.
|
|
427
|
+
//
|
|
428
|
+
// CFG-36 — the RUN path re-raises it as a {@link ConfigDiscoveryError} instead of printing and
|
|
429
|
+
// calling `exit(1)`. A profile whose `extends` base is missing (or forms a cycle) is a bad
|
|
430
|
+
// profile exactly as a profile with no config at all is, and both must be classifiable by the
|
|
431
|
+
// caller: exiting here from inside a library collapses `gth eval`'s harness-error (exit 2) and
|
|
432
|
+
// product-failure (exit 1) contract onto the same code, which is the collapse this node exists
|
|
433
|
+
// to remove. The CLI's top-level guard prints the same message and exits 1, so what a person at
|
|
434
|
+
// a terminal sees is unchanged.
|
|
435
|
+
//
|
|
436
|
+
// The message already names the profile, the base and the cycle; the optional detail fields are
|
|
437
|
+
// deliberately left unset rather than guessed at. `identityProfile` means "the profile that did
|
|
438
|
+
// not resolve", which for a missing base referenced from another profile is the BASE, not the
|
|
439
|
+
// `profileLabel` in hand here — a wrong value would be worse than an absent one.
|
|
440
|
+
//
|
|
441
|
+
// CFG-47 — the original travels as `cause`. Re-raising by message alone made the
|
|
442
|
+
// {@link ConfigExtendsError} (and any stack under it) unrecoverable from the wrapper, so a
|
|
443
|
+
// consumer that wanted the underlying failure had only the rendered string. The in-repo sibling
|
|
444
|
+
// ({@link MissingProviderKeyError} in {@link tryJsonConfig}) already carries it.
|
|
389
445
|
if (e instanceof ConfigExtendsError) {
|
|
390
|
-
|
|
391
|
-
exit(1);
|
|
392
|
-
// Unreachable past exit(1) in production; LOAD-BEARING under the specs' mocked exit so the
|
|
393
|
-
// caller never observes a silently-composed config after a cycle/missing base.
|
|
394
|
-
throw new Error('Unexpected error occurred.');
|
|
446
|
+
throw new ConfigDiscoveryError(e.message, {}, { cause: e });
|
|
395
447
|
}
|
|
396
448
|
throw e;
|
|
397
449
|
}
|
|
@@ -400,7 +452,7 @@ export async function resolveConfigExtends(rawConfig, profileLabel) {
|
|
|
400
452
|
* GS2-73 — seed the `extends` chain from the selected profile's own name and run the throwing
|
|
401
453
|
* traversal ({@link resolveExtendsChain}). Shared by BOTH consumers so the walk and its
|
|
402
454
|
* cycle/missing-base checks live in ONE place: the run-path {@link resolveConfigExtends} (which
|
|
403
|
-
*
|
|
455
|
+
* re-raises a {@link ConfigExtendsError} as a {@link ConfigDiscoveryError}) and the read-path
|
|
404
456
|
* {@link validateConfig} (which records it as a not-ok layer). Propagates the typed error to its
|
|
405
457
|
* caller; the caller owns the reporting convention.
|
|
406
458
|
*/
|
|
@@ -554,11 +606,12 @@ async function readProjectConfiguredTui(commandLineConfigOverrides) {
|
|
|
554
606
|
// profile whose `tui` is inherited while the run composes it, which is precisely the
|
|
555
607
|
// reader-vs-run divergence this seam exists to prevent.
|
|
556
608
|
//
|
|
557
|
-
// Deliberately NOT inside the try above:
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
//
|
|
609
|
+
// Deliberately NOT inside the try above: a cycle, a missing base and a MALFORMED base all raise a
|
|
610
|
+
// {@link ConfigDiscoveryError} that must reach the top level to be reported. The try above exists
|
|
611
|
+
// to treat an unreadable config as "no configured tui"; extending it over these would swallow a
|
|
612
|
+
// hard configuration error and let the surface be selected from a config the run itself refuses
|
|
613
|
+
// to load. This reader runs BEFORE the TUI/readline choice, so the error surfaces here rather than
|
|
614
|
+
// through `createTuiSession` — either way it is reported once, by the CLI's top-level guard.
|
|
562
615
|
if (typeof raw.extends === 'string') {
|
|
563
616
|
raw = await resolveConfigExtends(raw, commandLineConfigOverrides.identityProfile);
|
|
564
617
|
}
|
|
@@ -571,7 +624,13 @@ async function readProjectConfiguredTui(commandLineConfigOverrides) {
|
|
|
571
624
|
export async function initConfig(commandLineConfigOverrides) {
|
|
572
625
|
if (commandLineConfigOverrides.customConfigPath &&
|
|
573
626
|
!existsSync(commandLineConfigOverrides.customConfigPath)) {
|
|
574
|
-
|
|
627
|
+
// CFG-47 — a {@link ConfigDiscoveryError}, not a plain `Error`. An explicitly named `-c` path
|
|
628
|
+
// that is not there is the same "this configuration cannot be used" failure as a named profile
|
|
629
|
+
// with no config, and it reaches the same two consumers: the CLI's top-level guard (prints the
|
|
630
|
+
// message, exits 1) and `gth eval` (harness error, exit 2). As a plain Error it was invisible to
|
|
631
|
+
// BOTH — it fell through the guard to the crash handler, so `gth -c <missing path> code` printed
|
|
632
|
+
// a false "TUI unavailable … falling back to the readline session" and then a crash snapshot.
|
|
633
|
+
throw new ConfigDiscoveryError(`Provided manual config "${commandLineConfigOverrides.customConfigPath}" does not exist`, { sourceLabel: commandLineConfigOverrides.customConfigPath });
|
|
575
634
|
}
|
|
576
635
|
// Clear the project root BEFORE discovery. Discovery and detection must resolve against cwd,
|
|
577
636
|
// and the up-tree walk itself goes through getGslothConfigReadPath -> getProjectDir(); clearing
|
|
@@ -583,6 +642,26 @@ export async function initConfig(commandLineConfigOverrides) {
|
|
|
583
642
|
// from cwd to the stop boundary (see findProjectConfigPath). Detection and loading share this
|
|
584
643
|
// resolver, and the discovered dir becomes the base for the per-format cascade below.
|
|
585
644
|
const discovered = findProjectConfigPath(commandLineConfigOverrides);
|
|
645
|
+
// GS2-62 / CFG-36 — an EXPLICITLY named identity profile (`-i <name>` / eval `--judge <name>`)
|
|
646
|
+
// must resolve to its OWN config; it must never silently run under some OTHER config. That is a
|
|
647
|
+
// false-green trap: `gth -i typo …` would run under the wrong model while appearing to use the
|
|
648
|
+
// named profile — in an authorization/eval context, hiding a real misconfiguration.
|
|
649
|
+
//
|
|
650
|
+
// Checked with the STRICT resolver, NOT with `discovered`. `findProjectConfigPath` deliberately
|
|
651
|
+
// falls back to a plain `<dir>/<config>` when the named profile has no config of its own (see its
|
|
652
|
+
// note, and the "Case C" spec in config.uptree.spec.ts), so gating on `!discovered` only catches
|
|
653
|
+
// the case where NO config exists anywhere — a project that has a plain config would sail past it
|
|
654
|
+
// and load that instead. `resolveIdentityProfileConfigPath` never falls through, which is what
|
|
655
|
+
// makes this check see the case the discovery gate cannot.
|
|
656
|
+
//
|
|
657
|
+
// The rule itself lives in findUnresolvedExplicitProfile, shared with `validateConfig` so the
|
|
658
|
+
// validator can never green-light a profile a run refuses (GS2-29).
|
|
659
|
+
const explicitProfile = findUnresolvedExplicitProfile(commandLineConfigOverrides);
|
|
660
|
+
if (explicitProfile) {
|
|
661
|
+
throw new ConfigDiscoveryError(identityProfileNotFoundMessage(explicitProfile), {
|
|
662
|
+
identityProfile: explicitProfile,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
586
665
|
const baseDir = discovered?.dir ?? getCurrentWorkDir();
|
|
587
666
|
// Set the project root for post-config, project-relative artifact resolution (guidelines,
|
|
588
667
|
// prompts, .gsloth-settings, outputs). up-tree and --config both set it here; a global-only /
|
|
@@ -602,24 +681,10 @@ export async function initConfig(commandLineConfigOverrides) {
|
|
|
602
681
|
// standalone global config (loaded alone) before erroring. Project config still takes
|
|
603
682
|
// precedence: this branch only runs when there is no project file to apply the global under.
|
|
604
683
|
if (!discovered) {
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
// loudly instead. Gated on a non-empty identityProfile, so the CFG-8 no-profile global fallback
|
|
610
|
-
// just below is UNTOUCHED — a run with no profile still loads the global exactly as before.
|
|
611
|
-
const explicitProfile = commandLineConfigOverrides.identityProfile?.trim();
|
|
612
|
-
if (explicitProfile) {
|
|
613
|
-
displayError(`identity profile "${explicitProfile}" not found: no config file in ` +
|
|
614
|
-
`${GSLOTH_DIR}/${GSLOTH_SETTINGS_DIR}/${explicitProfile}/ ` +
|
|
615
|
-
`(checked ${PROJECT_CONFIG_FORMATS.join(', ')})`);
|
|
616
|
-
exit(1);
|
|
617
|
-
// Unreachable past exit(1) in production. In specs exit() is mocked to a no-op, so this throw
|
|
618
|
-
// is LOAD-BEARING: without it execution would fall through into loadGlobalRawConfig() below
|
|
619
|
-
// and the test would observe the global silently loaded — masking the very regression this
|
|
620
|
-
// guard fixes. Matches the loader's existing post-exit sentinel-throw pattern.
|
|
621
|
-
throw new Error('Unexpected error occurred.');
|
|
622
|
-
}
|
|
684
|
+
// The explicitly-named-profile guard that used to sit here now runs BEFORE discovery is
|
|
685
|
+
// consulted at all (see above), because gating it on `!discovered` missed the case where a
|
|
686
|
+
// plain project config exists. A run with no profile reaches the global fallback below exactly
|
|
687
|
+
// as before.
|
|
623
688
|
const globalRawConfig = await loadGlobalRawConfig();
|
|
624
689
|
if (globalRawConfig) {
|
|
625
690
|
if (globalRawConfig.llm &&
|
|
@@ -628,10 +693,11 @@ export async function initConfig(commandLineConfigOverrides) {
|
|
|
628
693
|
// Route the global config through the same path the project JSON uses.
|
|
629
694
|
return await tryJsonConfig(globalRawConfig, commandLineConfigOverrides);
|
|
630
695
|
}
|
|
631
|
-
|
|
632
|
-
exit
|
|
633
|
-
//
|
|
634
|
-
|
|
696
|
+
// CFG-47 — a global config that read fine and does not define `llm.type` is "config present
|
|
697
|
+
// and unusable", the same class CFG-36 converted: raise it, let the caller choose the exit
|
|
698
|
+
// code. The message is the one this branch has always printed, so the CLI's top-level guard
|
|
699
|
+
// reproduces the previous output exactly.
|
|
700
|
+
throw new ConfigDiscoveryError('Global configuration found but it is not in valid format. Should at least define llm.type', { sourceLabel: `${USER_PROJECT_CONFIG_JSON} (global)` });
|
|
635
701
|
}
|
|
636
702
|
}
|
|
637
703
|
// Try loading the JSON/JSONC config file first (GS2-69 — an explicit `-c foo.jsonc` takes
|
|
@@ -656,12 +722,12 @@ export async function initConfig(commandLineConfigOverrides) {
|
|
|
656
722
|
return await tryJsonConfig(jsonConfig, commandLineConfigOverrides);
|
|
657
723
|
}
|
|
658
724
|
else {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
//
|
|
662
|
-
//
|
|
725
|
+
// CFG-47 — same class, same treatment. Deliberately a ConfigDiscoveryError and not a plain
|
|
726
|
+
// one: the catch below re-raises this class and swallows everything else into the next
|
|
727
|
+
// FORMAT, so a plain throw here would silently fall through to the module loader and end in
|
|
728
|
+
// the terminal "No configuration file found" — hiding the real, clearly-worded problem.
|
|
663
729
|
// noinspection ExceptionCaughtLocallyJS
|
|
664
|
-
throw new
|
|
730
|
+
throw new ConfigDiscoveryError(`${jsonConfigPath} is not in valid format. Should at least define llm.type`, { sourceLabel: jsonConfigName });
|
|
665
731
|
}
|
|
666
732
|
}
|
|
667
733
|
catch (e) {
|
|
@@ -669,10 +735,17 @@ export async function initConfig(commandLineConfigOverrides) {
|
|
|
669
735
|
// failure. This catch exists to move on to the next config FORMAT when the JSON layer could
|
|
670
736
|
// not be read; a config that read fine and named a provider we have no key for is not a
|
|
671
737
|
// read failure, and falling through would end in the terminal "No configuration file found"
|
|
672
|
-
//
|
|
738
|
+
// failure — which blames a missing config for what is actually a missing key, burying the
|
|
739
|
+
// clearly-worded message this branch already has.
|
|
673
740
|
if (isMissingProviderKeyError(e)) {
|
|
674
741
|
throw e;
|
|
675
742
|
}
|
|
743
|
+
// CFG-36 — same reasoning for a MALFORMED config: the config read fine and is invalid, which
|
|
744
|
+
// is a hard error the user must see. Falling through to the next FORMAT would end in the
|
|
745
|
+
// terminal "No configuration file found" failure, hiding the real (clearly-worded) problem.
|
|
746
|
+
if (isConfigDiscoveryError(e)) {
|
|
747
|
+
throw e;
|
|
748
|
+
}
|
|
676
749
|
displayDebug(e instanceof Error ? e : String(e));
|
|
677
750
|
displayError(`Failed to read config from ${jsonConfigName}, will try other formats.`);
|
|
678
751
|
// Continue to try other formats
|
|
@@ -726,15 +799,24 @@ async function tryModuleConfig(format, commandLineConfigOverrides, baseDir) {
|
|
|
726
799
|
return await mergeConfig(mergedWithGlobal, commandLineConfigOverrides);
|
|
727
800
|
}
|
|
728
801
|
catch (e) {
|
|
802
|
+
// CFG-36 — a config that read fine and is MALFORMED (or names an unresolvable profile) is a
|
|
803
|
+
// hard error, not a reason to try the next format. Re-raise before the fall-through, exactly
|
|
804
|
+
// as the JSON branch in initConfig does.
|
|
805
|
+
if (isConfigDiscoveryError(e)) {
|
|
806
|
+
throw e;
|
|
807
|
+
}
|
|
729
808
|
displayDebug(e instanceof Error ? e : String(e));
|
|
730
809
|
if (nextFormat) {
|
|
731
810
|
displayError(`Failed to read config from ${filename}, will try other formats.`);
|
|
732
811
|
// Continue to try other formats
|
|
733
812
|
return await tryModuleConfig(nextFormat, commandLineConfigOverrides, baseDir);
|
|
734
813
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
814
|
+
// CFG-47 — the last format in the chain failed to read. The config IS present and cannot be
|
|
815
|
+
// used, so this raises like the rest of the class. Both lines are kept in the one message:
|
|
816
|
+
// the first names the file that failed, the second is the advice, and the guard prints them
|
|
817
|
+
// together exactly as the two `displayError` calls did.
|
|
818
|
+
throw new ConfigDiscoveryError(`Failed to read config from ${filename}.\n` +
|
|
819
|
+
`No valid configuration found. Please create a valid configuration file.`, { sourceLabel: filename });
|
|
738
820
|
}
|
|
739
821
|
}
|
|
740
822
|
else if (nextFormat) {
|
|
@@ -742,14 +824,19 @@ async function tryModuleConfig(format, commandLineConfigOverrides, baseDir) {
|
|
|
742
824
|
return await tryModuleConfig(nextFormat, commandLineConfigOverrides, baseDir);
|
|
743
825
|
}
|
|
744
826
|
else {
|
|
745
|
-
// No config files found
|
|
746
|
-
|
|
827
|
+
// No config files found.
|
|
828
|
+
//
|
|
829
|
+
// CFG-47 — the terminal "nothing to load" exit, raised rather than exited. It is the same class
|
|
830
|
+
// for the same reason as the rest: the caller must classify it. `gth eval` in particular needs
|
|
831
|
+
// "the harness has no config" to be a harness error (exit 2), not the exit 1 that means the SUT
|
|
832
|
+
// ran and failed. The CLI's top-level guard prints this message and exits 1, so a person at a
|
|
833
|
+
// terminal sees exactly what they saw before — and `startSession` still runs the first-run
|
|
834
|
+
// dialog ahead of this on an interactive TTY with no config anywhere (CFG-10), so the ordinary
|
|
835
|
+
// unconfigured path never reaches here.
|
|
836
|
+
throw new ConfigDiscoveryError('No configuration file found. Please create one of: ' +
|
|
747
837
|
`${USER_PROJECT_CONFIG_JSON}, ${USER_PROJECT_CONFIG_JS}, or ${USER_PROJECT_CONFIG_MJS} ` +
|
|
748
838
|
'in your project directory.');
|
|
749
|
-
exit(1);
|
|
750
839
|
}
|
|
751
|
-
// This throw is unreachable due to exit(1) above, but satisfies TS type analysis and prevents tests from exiting
|
|
752
|
-
throw new Error('Unexpected error occurred.');
|
|
753
840
|
}
|
|
754
841
|
/**
|
|
755
842
|
* Process JSON LLM config by creating the appropriate LLM instance
|
|
@@ -763,8 +850,12 @@ export async function tryJsonConfig(jsonConfig, commandLineConfigOverrides) {
|
|
|
763
850
|
// Get the type of LLM (e.g. 'vertexai', 'anthropic') - this should exist
|
|
764
851
|
const llmType = jsonConfig.llm.type;
|
|
765
852
|
if (!llmType) {
|
|
766
|
-
|
|
767
|
-
|
|
853
|
+
// CFG-47 — same class, raised rather than exited. Note this throw is caught by THIS
|
|
854
|
+
// function's own catch below, which re-raises the class before its "Error processing LLM
|
|
855
|
+
// config" wrapper; without that re-raise the clear message here would be re-worded and then
|
|
856
|
+
// exited on anyway.
|
|
857
|
+
// noinspection ExceptionCaughtLocallyJS
|
|
858
|
+
throw new ConfigDiscoveryError('LLM type not specified in config.');
|
|
768
859
|
}
|
|
769
860
|
// Get the configuration for the specific LLM type
|
|
770
861
|
const llmConfig = jsonConfig.llm;
|
|
@@ -791,19 +882,35 @@ export async function tryJsonConfig(jsonConfig, commandLineConfigOverrides) {
|
|
|
791
882
|
}
|
|
792
883
|
}
|
|
793
884
|
else {
|
|
794
|
-
|
|
795
|
-
|
|
885
|
+
// CFG-47 — the node called this site the one that is "genuinely a different case". Measured,
|
|
886
|
+
// it is not: `#src/providers/<type>.js` resolves against the SAME directory that holds five
|
|
887
|
+
// real modules with no `processJsonConfig` export (modelCatalog, modelDiscovery,
|
|
888
|
+
// geminiThinking, geminiSchemaSanitizer, configurationPassthrough), so `llm.type:
|
|
889
|
+
// "modelCatalog"` reaches here from an ordinary config file — verified by resolving that
|
|
890
|
+
// specifier at runtime. It is therefore user-reachable "config present and unusable" like
|
|
891
|
+
// the rest, and is converted with them. Kept on the warning wording it has always had.
|
|
892
|
+
// noinspection ExceptionCaughtLocallyJS
|
|
893
|
+
throw new ConfigDiscoveryError(`Config module for ${llmType} does not have processJsonConfig function.`);
|
|
796
894
|
}
|
|
797
895
|
}
|
|
798
896
|
else {
|
|
799
|
-
|
|
800
|
-
|
|
897
|
+
// noinspection ExceptionCaughtLocallyJS
|
|
898
|
+
throw new ConfigDiscoveryError('No LLM configuration found in config.');
|
|
801
899
|
}
|
|
802
900
|
}
|
|
803
901
|
catch (e) {
|
|
902
|
+
// CFG-47 — the four sites above raise INTO this catch, so it must re-raise the class before its
|
|
903
|
+
// own wrapper reaches them. Without this, a clear "LLM type not specified in config." would be
|
|
904
|
+
// re-worded as "Error processing LLM config: …" and exited on anyway — the local catch would
|
|
905
|
+
// undo the conversion three lines after it happened. Same shape as the re-raises in
|
|
906
|
+
// `initConfig` and `tryModuleConfig`.
|
|
907
|
+
if (isConfigDiscoveryError(e)) {
|
|
908
|
+
throw e;
|
|
909
|
+
}
|
|
804
910
|
if (e instanceof Error && e.message.includes('Cannot find module')) {
|
|
805
|
-
|
|
806
|
-
exit
|
|
911
|
+
// CFG-47 — a configured `llm.type` we have no provider module for is a config error, not a
|
|
912
|
+
// process exit. `{ cause: e }` keeps the resolver's own failure reachable.
|
|
913
|
+
throw new ConfigDiscoveryError(`LLM type '${jsonConfig.llm.type}' not supported.`, {}, { cause: e });
|
|
807
914
|
}
|
|
808
915
|
else {
|
|
809
916
|
const message = `Error processing LLM config: ${e instanceof Error ? e.message : String(e)}`;
|
|
@@ -825,12 +932,13 @@ export async function tryJsonConfig(jsonConfig, commandLineConfigOverrides) {
|
|
|
825
932
|
if (missingKey) {
|
|
826
933
|
throw new MissingProviderKeyError(message, missingKey, { cause: e });
|
|
827
934
|
}
|
|
828
|
-
|
|
829
|
-
|
|
935
|
+
// CFG-47 — and when it is NOT a missing key, raise too. This site's immediate neighbour three
|
|
936
|
+
// lines up already threw, so the file disagreed with itself about whether a provider that
|
|
937
|
+
// could not be built terminates the process; it does not. The message is unchanged and
|
|
938
|
+
// `{ cause: e }` keeps the provider's own error reachable, as the missing-key branch does.
|
|
939
|
+
throw new ConfigDiscoveryError(message, {}, { cause: e });
|
|
830
940
|
}
|
|
831
941
|
}
|
|
832
|
-
// This throw is unreachable due to exit(1) above, but satisfies TS type analysis and prevents tests from exiting
|
|
833
|
-
throw new Error('Unexpected error occurred.');
|
|
834
942
|
}
|
|
835
943
|
/**
|
|
836
944
|
* Config array fields whose values ADD UP across merge layers (global → project): the
|
|
@@ -1260,6 +1368,31 @@ async function loadGlobalRawConfigUnvalidated() {
|
|
|
1260
1368
|
*/
|
|
1261
1369
|
export async function validateConfig(commandLineConfigOverrides) {
|
|
1262
1370
|
const layers = [];
|
|
1371
|
+
// CFG-36 — mirror the run's STRICT named-profile rule before anything is read. `initConfig`
|
|
1372
|
+
// refuses outright when an explicitly-named profile has no config of its own, so a validator that
|
|
1373
|
+
// walked on would report OK for a config the run rejects — and it would do so in the ordinary
|
|
1374
|
+
// case, because discovery falls back to a plain project config for an unresolved profile. That is
|
|
1375
|
+
// the GS2-29 divergence in its purest form: `gth config validate -i typo` green-lighting a run
|
|
1376
|
+
// that cannot start. Reported as a not-ok layer rather than thrown, because the read side
|
|
1377
|
+
// COLLECTS and never terminates; returned immediately because a run gets no further either.
|
|
1378
|
+
const unresolvedProfile = findUnresolvedExplicitProfile(commandLineConfigOverrides);
|
|
1379
|
+
if (unresolvedProfile) {
|
|
1380
|
+
return {
|
|
1381
|
+
// `found: true` so the caller renders THIS message. `found: false` means "nothing to
|
|
1382
|
+
// validate, run `gth init`", which would both discard the real diagnosis and misdirect a user
|
|
1383
|
+
// whose actual problem is a mistyped `-i`.
|
|
1384
|
+
found: true,
|
|
1385
|
+
ok: false,
|
|
1386
|
+
layers: [
|
|
1387
|
+
{
|
|
1388
|
+
sourceLabel: `${GSLOTH_DIR}/${GSLOTH_SETTINGS_DIR}/${unresolvedProfile}/`,
|
|
1389
|
+
ok: false,
|
|
1390
|
+
warnings: [],
|
|
1391
|
+
errorMessage: identityProfileNotFoundMessage(unresolvedProfile),
|
|
1392
|
+
},
|
|
1393
|
+
],
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1263
1396
|
// Project layer first (run order): initConfig validates the discovered project config before
|
|
1264
1397
|
// applying the global base. A parse failure here propagates (surfaced by the caller).
|
|
1265
1398
|
const discovered = findProjectConfigPath(commandLineConfigOverrides);
|
|
@@ -1282,7 +1415,11 @@ export async function validateConfig(commandLineConfigOverrides) {
|
|
|
1282
1415
|
await composeExtends(raw, commandLineConfigOverrides.identityProfile);
|
|
1283
1416
|
}
|
|
1284
1417
|
catch (e) {
|
|
1285
|
-
|
|
1418
|
+
// CFG-36 — a MALFORMED base layer now raises a ConfigDiscoveryError from
|
|
1419
|
+
// `validateRawConfigLayer` instead of exiting the process. Record it as a not-ok layer
|
|
1420
|
+
// alongside the traversal's own failures: the read side collects, it never terminates, so
|
|
1421
|
+
// `gth config validate` reports the broken base rather than dying on it.
|
|
1422
|
+
if (e instanceof ConfigExtendsError || isConfigDiscoveryError(e)) {
|
|
1286
1423
|
layer.ok = false;
|
|
1287
1424
|
layer.errorMessage = e.message;
|
|
1288
1425
|
}
|