@fro.bot/systematic 2.10.0 → 2.12.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/dist/index.js CHANGED
@@ -1,18 +1,19 @@
1
1
  // @bun
2
2
  import {
3
+ AgentOverlaySchema,
4
+ CategoryOverlaySchema,
5
+ assertSourceCategoryModelDefaults,
3
6
  convertFileWithCache,
4
7
  extractAgentFrontmatter,
5
8
  extractCommandFrontmatter,
6
9
  findAgentsInDir,
7
10
  findCommandsInDir,
8
11
  findSkillsInDir,
9
- isAgentMode,
10
- isPermissionSetting,
11
12
  isRecord,
12
13
  loadConfig,
13
14
  loadConfigWithSources,
14
15
  parseFrontmatter
15
- } from "./index-mfy9dbdx.js";
16
+ } from "./index-g602hys2.js";
16
17
 
17
18
  // src/index.ts
18
19
  import fs4 from "fs";
@@ -78,28 +79,16 @@ ${toolMapping}
78
79
 
79
80
  // src/lib/agent-overlays.ts
80
81
  import fs2 from "fs";
82
+ import os2 from "os";
81
83
  import path2 from "path";
82
84
  var SOURCE_CATEGORY_MODEL_DEFAULTS = {
83
- design: "openai/gpt-5.5",
84
- docs: "openai/gpt-5.4-mini",
85
- "document-review": "anthropic/claude-opus-4.7",
86
- research: "openai/gpt-5.5",
87
- review: "anthropic/claude-opus-4.7",
88
- workflow: "openai/gpt-5.4-mini"
85
+ design: ["openai/gpt-5.5", "anthropic/claude-opus-4.7"],
86
+ docs: ["openai/gpt-5.4-mini", "anthropic/claude-haiku-4-5"],
87
+ "document-review": ["anthropic/claude-opus-4.7", "openai/gpt-5.5"],
88
+ research: ["openai/gpt-5.5", "anthropic/claude-opus-4.7"],
89
+ review: ["anthropic/claude-opus-4.7", "openai/gpt-5.5"],
90
+ workflow: ["openai/gpt-5.4-mini", "anthropic/claude-haiku-4-5"]
89
91
  };
90
- var ALLOWED_OVERLAY_FIELDS = new Set([
91
- "model",
92
- "variant",
93
- "temperature",
94
- "top_p",
95
- "permission",
96
- "mode",
97
- "color",
98
- "steps",
99
- "hidden",
100
- "disable",
101
- "skills"
102
- ]);
103
92
  function buildBundledAgentInventory(agentsDir, disabledAgents) {
104
93
  const categories = readCategoryDirs(agentsDir);
105
94
  const agentsByQualifiedId = {};
@@ -166,10 +155,51 @@ function inferBuiltInTemperature(name, description) {
166
155
  }
167
156
  return 0.3;
168
157
  }
169
- function getSourceCategoryModel(category) {
158
+ function getAuthenticatedProviders(rootDirOverride) {
159
+ const xdgDataHome = process.env.XDG_DATA_HOME?.trim();
160
+ const rootDir = rootDirOverride || (xdgDataHome && path2.isAbsolute(xdgDataHome) ? xdgDataHome : path2.join(os2.homedir(), ".local/share"));
161
+ const authPath = path2.join(rootDir, "opencode", "auth.json");
162
+ let raw;
163
+ try {
164
+ raw = fs2.readFileSync(authPath, "utf8");
165
+ } catch (err) {
166
+ if (isSystemError(err) && err.code === "ENOENT") {
167
+ return new Set;
168
+ }
169
+ console.warn(`[systematic] auth.json unreadable at ${authPath}; ignoring`);
170
+ return new Set;
171
+ }
172
+ let parsed;
173
+ try {
174
+ parsed = JSON.parse(raw);
175
+ } catch {
176
+ console.warn(`[systematic] auth.json malformed at ${authPath}; ignoring`);
177
+ return new Set;
178
+ }
179
+ if (!isRecord(parsed)) {
180
+ console.warn(`[systematic] auth.json malformed at ${authPath}; ignoring`);
181
+ return new Set;
182
+ }
183
+ return new Set(Object.keys(parsed));
184
+ }
185
+ function getSourceCategoryModel(category, authedProviders) {
170
186
  if (!category)
171
187
  return;
172
- return SOURCE_CATEGORY_MODEL_DEFAULTS[category];
188
+ const candidates = SOURCE_CATEGORY_MODEL_DEFAULTS[category];
189
+ if (!candidates || candidates.length === 0)
190
+ return;
191
+ if (!authedProviders || authedProviders.size === 0)
192
+ return candidates[0];
193
+ for (const entry of candidates) {
194
+ const slashIndex = entry.indexOf("/");
195
+ if (slashIndex <= 0)
196
+ continue;
197
+ const providerId = entry.slice(0, slashIndex);
198
+ if (authedProviders.has(providerId)) {
199
+ return entry;
200
+ }
201
+ }
202
+ return candidates[0];
173
203
  }
174
204
  function assertSourceCategoryModelCoverage(categories) {
175
205
  validateSourceCategoryModelDefaults();
@@ -179,9 +209,7 @@ function assertSourceCategoryModelCoverage(categories) {
179
209
  }
180
210
  }
181
211
  function validateSourceCategoryModelDefaults(defaults = SOURCE_CATEGORY_MODEL_DEFAULTS) {
182
- for (const [category, model] of Object.entries(defaults)) {
183
- validateModel("source category model defaults", `source category model defaults.${category}`, model);
184
- }
212
+ assertSourceCategoryModelDefaults(defaults);
185
213
  }
186
214
  function validateExactAgentOverlays(inventory, overlays, nativeAgents, enabledSkills) {
187
215
  const result = [];
@@ -231,150 +259,35 @@ function validateCategoryOverlays(inventory, overlays, enabledSkills) {
231
259
  }
232
260
  return result;
233
261
  }
234
- function validateOverlayFields(overlay, targetType, enabledSkills) {
235
- if (Object.hasOwn(overlay.value, "skills") && hasPermissionSkill(overlay.value.permission)) {
236
- throwConfigError(overlay.sourcePath, overlay.keyPath, "cannot set both skills and permission.skill in the same overlay object");
237
- }
238
- for (const [field, value] of Object.entries(overlay.value)) {
239
- const keyPath = `${overlay.keyPath}.${field}`;
240
- if (!ALLOWED_OVERLAY_FIELDS.has(field)) {
241
- throwConfigError(overlay.sourcePath, keyPath, `unsupported agent overlay field "${field}"`);
242
- }
243
- if (targetType === "category" && field === "disable") {
244
- throwConfigError(overlay.sourcePath, keyPath, "disable is only valid for exact agent overlays");
245
- }
246
- validateOverlayFieldValue(overlay.sourcePath, keyPath, field, value, enabledSkills);
247
- }
248
- }
249
262
  function hasPermissionSkill(permission) {
250
263
  return isRecord(permission) && Object.hasOwn(permission, "skill");
251
264
  }
252
- function validateOverlayFieldValue(sourcePath, keyPath, field, value, enabledSkills) {
253
- switch (field) {
254
- case "model":
255
- if (value === null)
256
- return;
257
- validateModel(sourcePath, keyPath, value);
258
- return;
259
- case "variant":
260
- validateNonEmptyString(sourcePath, keyPath, value);
261
- return;
262
- case "temperature":
263
- validateTemperature(sourcePath, keyPath, value);
264
- return;
265
- case "top_p":
266
- validateTopP(sourcePath, keyPath, value);
267
- return;
268
- case "permission":
269
- validatePermission(sourcePath, keyPath, value);
270
- return;
271
- case "mode":
272
- validateMode(sourcePath, keyPath, value);
273
- return;
274
- case "color":
275
- validateColor(sourcePath, keyPath, value);
276
- return;
277
- case "steps":
278
- validatePositiveInteger(sourcePath, keyPath, value);
279
- return;
280
- case "hidden":
281
- case "disable":
282
- validateBoolean(sourcePath, keyPath, value);
283
- return;
284
- case "skills":
285
- validateSkills(sourcePath, keyPath, value, enabledSkills);
286
- return;
287
- }
288
- }
289
- function validateModel(sourcePath, keyPath, value) {
290
- if (typeof value !== "string") {
291
- throwConfigError(sourcePath, keyPath, "must be a provider/model string");
292
- }
293
- if (value !== value.trim() || /\s/.test(value)) {
294
- throwConfigError(sourcePath, keyPath, "must be a provider/model string");
295
- }
296
- const slashIndex = value.indexOf("/");
297
- if (value === "" || slashIndex <= 0 || slashIndex === value.length - 1) {
298
- throwConfigError(sourcePath, keyPath, "must be a provider/model string");
299
- }
300
- }
301
- function validateNonEmptyString(sourcePath, keyPath, value) {
302
- if (typeof value !== "string" || value === "" || value !== value.trim()) {
303
- throwConfigError(sourcePath, keyPath, "must be a non-empty string");
304
- }
305
- }
306
- function validateTemperature(sourcePath, keyPath, value) {
307
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
308
- throwConfigError(sourcePath, keyPath, "must be a non-negative finite number");
309
- }
310
- }
311
- function validateTopP(sourcePath, keyPath, value) {
312
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
313
- throwConfigError(sourcePath, keyPath, "must be a number from 0 to 1");
314
- }
315
- }
316
- function validatePositiveInteger(sourcePath, keyPath, value) {
317
- if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
318
- throwConfigError(sourcePath, keyPath, "must be a positive integer");
319
- }
320
- }
321
- function validateBoolean(sourcePath, keyPath, value) {
322
- if (typeof value !== "boolean") {
323
- throwConfigError(sourcePath, keyPath, "must be a boolean");
324
- }
325
- }
326
- function validateMode(sourcePath, keyPath, value) {
327
- if (!isAgentMode(value)) {
328
- throwConfigError(sourcePath, keyPath, "must be one of: subagent, primary, all");
265
+ function validateOverlayFields(overlay, targetType, enabledSkills) {
266
+ if (Object.hasOwn(overlay.value, "skills") && hasPermissionSkill(overlay.value.permission)) {
267
+ throwConfigError(overlay.sourcePath, overlay.keyPath, "cannot set both skills and permission.skill in the same overlay object");
329
268
  }
269
+ const result = parseOverlayShape(overlay, targetType);
270
+ validateOverlaySkills(overlay, result.skills, enabledSkills);
330
271
  }
331
- function validateColor(sourcePath, keyPath, value) {
332
- if (typeof value !== "string" || value !== value.trim() || !isOpenCodeColor(value)) {
333
- throwConfigError(sourcePath, keyPath, "must be an OpenCode-compatible color string");
272
+ function parseOverlayShape(overlay, targetType) {
273
+ const schema = targetType === "agent" ? AgentOverlaySchema : CategoryOverlaySchema;
274
+ const result = schema.safeParse(overlay.value);
275
+ if (!result.success) {
276
+ throwOverlaySchemaError(overlay, result.error.issues[0]);
334
277
  }
278
+ return { skills: result.data.skills };
335
279
  }
336
- function isOpenCodeColor(value) {
337
- if (value === "")
338
- return false;
339
- if (/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value))
340
- return true;
341
- return /^[a-zA-Z][a-zA-Z0-9-]*$/.test(value);
280
+ function throwOverlaySchemaError(overlay, issue) {
281
+ const zodPath = issue.code === "unrecognized_keys" ? issue.message.match(/"([^"]+)"/)?.[1] ?? issue.path.join(".") : issue.path.join(".");
282
+ const fullPath = zodPath ? `${overlay.keyPath}.${zodPath}` : overlay.keyPath;
283
+ throwConfigError(overlay.sourcePath, fullPath, issue.message);
342
284
  }
343
- function validateSkills(sourcePath, keyPath, value, enabledSkills) {
344
- if (!Array.isArray(value) || !value.every((skill) => typeof skill === "string" && skill !== "" && skill === skill.trim())) {
345
- throwConfigError(sourcePath, keyPath, "must be an array of non-empty strings");
346
- }
347
- if (!enabledSkills)
285
+ function validateOverlaySkills(overlay, skills, enabledSkills) {
286
+ if (!enabledSkills || !skills)
348
287
  return;
349
- for (const skill of value) {
288
+ for (const skill of skills) {
350
289
  if (!enabledSkills.has(skill)) {
351
- throwConfigError(sourcePath, keyPath, `unknown or disabled skill "${skill}"`);
352
- }
353
- }
354
- }
355
- function validatePermission(sourcePath, keyPath, value) {
356
- if (!isRecord(value)) {
357
- throwConfigError(sourcePath, keyPath, "must be an object");
358
- }
359
- for (const [toolKey, rule] of Object.entries(value)) {
360
- if (toolKey.trim() === "") {
361
- throwConfigError(sourcePath, `${keyPath}.${toolKey}`, "must use a non-empty tool key");
362
- }
363
- validatePermissionRule(sourcePath, `${keyPath}.${toolKey}`, rule);
364
- }
365
- }
366
- function validatePermissionRule(sourcePath, keyPath, value) {
367
- if (isPermissionSetting(value))
368
- return;
369
- if (!isRecord(value)) {
370
- throwConfigError(sourcePath, keyPath, "must be ask, allow, deny, or an object of pattern rules");
371
- }
372
- for (const [pattern, setting] of Object.entries(value)) {
373
- if (pattern.trim() === "") {
374
- throwConfigError(sourcePath, `${keyPath}.${pattern}`, "must use a non-empty permission pattern");
375
- }
376
- if (!isPermissionSetting(setting)) {
377
- throwConfigError(sourcePath, `${keyPath}.${pattern}`, "must be ask, allow, or deny");
290
+ throwConfigError(overlay.sourcePath, `${overlay.keyPath}.skills`, `unknown or disabled skill "${skill}"`);
378
291
  }
379
292
  }
380
293
  }
@@ -398,6 +311,9 @@ function validAgentKeys(inventory) {
398
311
  function throwConfigError(sourcePath, keyPath, message) {
399
312
  throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath} ${message}`);
400
313
  }
314
+ function isSystemError(err) {
315
+ return typeof err === "object" && err !== null && "code" in err && typeof err.code === "string";
316
+ }
401
317
 
402
318
  // src/lib/skill-loader.ts
403
319
  import path3 from "path";
@@ -567,7 +483,7 @@ function loadSkillAsCommand(loaded) {
567
483
  config.subtask = loaded.subtask;
568
484
  return config;
569
485
  }
570
- function collectAgents(dir, disabledAgents, nativeAgents, overlays) {
486
+ function collectAgents(dir, disabledAgents, nativeAgents, overlays, authedProviders) {
571
487
  const agents = {};
572
488
  const agentList = findAgentsInDir(dir);
573
489
  const disabledSet = new Set(disabledAgents);
@@ -582,12 +498,12 @@ function collectAgents(dir, disabledAgents, nativeAgents, overlays) {
582
498
  continue;
583
499
  const config = loadAgentAsConfig(agentInfo);
584
500
  if (config) {
585
- agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays);
501
+ agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays, authedProviders);
586
502
  }
587
503
  }
588
504
  return agents;
589
505
  }
590
- function applyAgentOverlays(config, agentInfo, overlays) {
506
+ function applyAgentOverlays(config, agentInfo, overlays, authedProviders) {
591
507
  const id = agentInfo.category ? `${agentInfo.category}/${agentInfo.name}` : agentInfo.name;
592
508
  const categoryOverlay = agentInfo.category ? overlays.categoriesByKey.get(agentInfo.category) : undefined;
593
509
  const exactOverlay = overlays.agentsByTargetId.get(id);
@@ -599,7 +515,7 @@ function applyAgentOverlays(config, agentInfo, overlays) {
599
515
  }
600
516
  result.temperature = inferBuiltInTemperature(agentInfo.name, result.description);
601
517
  if (agentInfo.category) {
602
- const sourceModel = getSourceCategoryModel(agentInfo.category);
518
+ const sourceModel = getSourceCategoryModel(agentInfo.category, authedProviders);
603
519
  if (sourceModel) {
604
520
  result.model = sourceModel;
605
521
  }
@@ -729,6 +645,7 @@ function collectEnabledSkillNames(dir, disabledSkills) {
729
645
  }
730
646
  function createConfigHandler(deps) {
731
647
  const { directory, bundledSkillsDir, bundledAgentsDir, bundledCommandsDir } = deps;
648
+ const readAuthProviders = deps.getAuthenticatedProviders ?? getAuthenticatedProviders;
732
649
  return async (config) => {
733
650
  const { config: systematicConfig, overlays } = loadConfigWithSources(directory);
734
651
  const existingAgents = { ...config.agent ?? {} };
@@ -744,7 +661,8 @@ function createConfigHandler(deps) {
744
661
  enabledSkills: enabledSkillNames
745
662
  });
746
663
  const resolvedOverlays = resolveAgentOverlaySet(validatedOverlays);
747
- const bundledAgents = collectAgents(bundledAgentsDir, systematicConfig.disabled_agents, existingAgents, resolvedOverlays);
664
+ const authedProviders = readAuthProviders();
665
+ const bundledAgents = collectAgents(bundledAgentsDir, systematicConfig.disabled_agents, existingAgents, resolvedOverlays, authedProviders);
748
666
  const bundledCommands = collectCommands(bundledCommandsDir, systematicConfig.disabled_commands);
749
667
  config.agent = {
750
668
  ...bundledAgents,
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Canonical OpenCode-accepted agent color tokens.
3
+ *
4
+ * These values are validated by OpenCode's `/config` HttpApi schema;
5
+ * any other value is rejected and surfaces to the user as
6
+ * `400: (empty response body)` on TUI launch.
7
+ *
8
+ * See anomalyco/opencode commits 2793502db / 96a534d8c for the
9
+ * server-side schema (PR #346 / v2.9.2 history).
10
+ */
11
+ export declare const OPENCODE_AGENT_COLOR_TOKENS: readonly ["primary", "secondary", "accent", "success", "warning", "error", "info"];
12
+ /**
13
+ * Check whether a value is a valid agent color — either a hex literal
14
+ * (`#RRGGBB`) or a named token from `OPENCODE_AGENT_COLOR_TOKENS`.
15
+ */
16
+ export declare function isValidAgentColor(value: string): boolean;
@@ -42,6 +42,27 @@ export declare function buildBundledAgentInventory(agentsDir: string, disabledAg
42
42
  export declare function validateAgentOverlays({ inventory, overlays, nativeAgents, enabledSkills, }: ValidateAgentOverlaysOptions): ValidatedAgentOverlays;
43
43
  export declare function resolveAgentOverlaySet(overlays: ValidatedAgentOverlays): ResolvedAgentOverlaySet;
44
44
  export declare function inferBuiltInTemperature(name: string, description?: string): number;
45
- export declare function getSourceCategoryModel(category: string | undefined): string | undefined;
45
+ /**
46
+ * Read which providers are authenticated from OpenCode's auth.json.
47
+ *
48
+ * Reads only top-level keys (provider IDs). Nested values are NEVER
49
+ * inspected, logged, persisted, or transmitted. This is a hard contract:
50
+ * the auth file holds API keys and OAuth tokens, and Systematic must
51
+ * never expose them via stderr, telemetry, or any other channel.
52
+ *
53
+ * Intended for one invocation per plugin config(cfg) cycle. Repeated
54
+ * calls trigger repeated file reads and, on malformed input, repeated
55
+ * stderr diagnostics.
56
+ *
57
+ * @param rootDirOverride - Optional path override for tests. When
58
+ * non-empty, the auth file is resolved as
59
+ * `path.join(rootDirOverride, 'opencode', 'auth.json')`. When
60
+ * omitted, resolution follows XDG_DATA_HOME -> ~/.local/share
61
+ * convention.
62
+ * @returns A readonly set of authenticated provider IDs (empty set on
63
+ * any failure).
64
+ */
65
+ export declare function getAuthenticatedProviders(rootDirOverride?: string): ReadonlySet<string>;
66
+ export declare function getSourceCategoryModel(category: string | undefined, authedProviders?: ReadonlySet<string>): string | undefined;
46
67
  export declare function assertSourceCategoryModelCoverage(categories: string[]): void;
47
68
  export declare function validateSourceCategoryModelDefaults(defaults?: Record<string, unknown>): void;
@@ -4,6 +4,8 @@ export interface ConfigHandlerDeps {
4
4
  bundledSkillsDir: string;
5
5
  bundledAgentsDir: string;
6
6
  bundledCommandsDir: string;
7
+ /** Override for authenticated provider reader; for testing. */
8
+ getAuthenticatedProviders?: (rootDirOverride?: string) => ReadonlySet<string>;
7
9
  }
8
10
  export declare function toTitleCase(name: string): string;
9
11
  export declare function formatAgentDescription(name: string, description: string | undefined): string;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Zod schema for the user-facing `systematic.json` / `systematic.jsonc` config.
3
+ *
4
+ * ## Zod 4 API notes (verified against zod@4.4.3 during implementation)
5
+ *
6
+ * - `z.toJSONSchema(schema, { target: 'draft-7' })` produces draft-07 JSON Schema.
7
+ * - `.default(value)` DOES round-trip into JSON Schema `default`.
8
+ * - `.meta({ description, examples })` attaches documentation metadata visible in
9
+ * JSON Schema output and via `schema.description`.
10
+ * - `z.object().strict()` rejects unknown keys with `unrecognized_keys` issues that
11
+ * include the offending key names and the path to the containing object.
12
+ * - `z.record(keySchema, valueSchema)` is the canonical 2-arg form (Zod 4 types
13
+ * require both key and value schemas). A single-arg overload works at runtime
14
+ * but is not reflected in the type declarations for this Zod 4 minor.
15
+ */
16
+ import { z } from 'zod';
17
+ export declare const AgentOverlaySchema: z.ZodObject<{
18
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
19
+ variant: z.ZodOptional<z.ZodString>;
20
+ temperature: z.ZodOptional<z.ZodNumber>;
21
+ top_p: z.ZodOptional<z.ZodNumber>;
22
+ mode: z.ZodOptional<z.ZodEnum<{
23
+ subagent: "subagent";
24
+ primary: "primary";
25
+ all: "all";
26
+ }>>;
27
+ color: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
28
+ primary: "primary";
29
+ secondary: "secondary";
30
+ accent: "accent";
31
+ success: "success";
32
+ warning: "warning";
33
+ error: "error";
34
+ info: "info";
35
+ }>, z.ZodString]>>;
36
+ steps: z.ZodOptional<z.ZodNumber>;
37
+ hidden: z.ZodOptional<z.ZodBoolean>;
38
+ disable: z.ZodOptional<z.ZodBoolean>;
39
+ skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
40
+ permission: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
41
+ ask: "ask";
42
+ allow: "allow";
43
+ deny: "deny";
44
+ }>, z.ZodRecord<z.ZodString, z.ZodEnum<{
45
+ ask: "ask";
46
+ allow: "allow";
47
+ deny: "deny";
48
+ }>>]>>>;
49
+ }, z.core.$strict>;
50
+ export declare const CategoryOverlaySchema: z.ZodObject<{
51
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
52
+ variant: z.ZodOptional<z.ZodString>;
53
+ temperature: z.ZodOptional<z.ZodNumber>;
54
+ top_p: z.ZodOptional<z.ZodNumber>;
55
+ mode: z.ZodOptional<z.ZodEnum<{
56
+ subagent: "subagent";
57
+ primary: "primary";
58
+ all: "all";
59
+ }>>;
60
+ color: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
61
+ primary: "primary";
62
+ secondary: "secondary";
63
+ accent: "accent";
64
+ success: "success";
65
+ warning: "warning";
66
+ error: "error";
67
+ info: "info";
68
+ }>, z.ZodString]>>;
69
+ steps: z.ZodOptional<z.ZodNumber>;
70
+ hidden: z.ZodOptional<z.ZodBoolean>;
71
+ skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
72
+ permission: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
73
+ ask: "ask";
74
+ allow: "allow";
75
+ deny: "deny";
76
+ }>, z.ZodRecord<z.ZodString, z.ZodEnum<{
77
+ ask: "ask";
78
+ allow: "allow";
79
+ deny: "deny";
80
+ }>>]>>>;
81
+ }, z.core.$strict>;
82
+ export declare const BootstrapSchema: z.ZodObject<{
83
+ enabled: z.ZodDefault<z.ZodBoolean>;
84
+ file: z.ZodOptional<z.ZodString>;
85
+ }, z.core.$strict>;
86
+ export declare const SystematicConfigSchema: z.ZodObject<{
87
+ $schema: z.ZodOptional<z.ZodString>;
88
+ agents: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
89
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
90
+ variant: z.ZodOptional<z.ZodString>;
91
+ temperature: z.ZodOptional<z.ZodNumber>;
92
+ top_p: z.ZodOptional<z.ZodNumber>;
93
+ mode: z.ZodOptional<z.ZodEnum<{
94
+ subagent: "subagent";
95
+ primary: "primary";
96
+ all: "all";
97
+ }>>;
98
+ color: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
99
+ primary: "primary";
100
+ secondary: "secondary";
101
+ accent: "accent";
102
+ success: "success";
103
+ warning: "warning";
104
+ error: "error";
105
+ info: "info";
106
+ }>, z.ZodString]>>;
107
+ steps: z.ZodOptional<z.ZodNumber>;
108
+ hidden: z.ZodOptional<z.ZodBoolean>;
109
+ disable: z.ZodOptional<z.ZodBoolean>;
110
+ skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
111
+ permission: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
112
+ ask: "ask";
113
+ allow: "allow";
114
+ deny: "deny";
115
+ }>, z.ZodRecord<z.ZodString, z.ZodEnum<{
116
+ ask: "ask";
117
+ allow: "allow";
118
+ deny: "deny";
119
+ }>>]>>>;
120
+ }, z.core.$strict>>>;
121
+ categories: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
122
+ model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
123
+ variant: z.ZodOptional<z.ZodString>;
124
+ temperature: z.ZodOptional<z.ZodNumber>;
125
+ top_p: z.ZodOptional<z.ZodNumber>;
126
+ mode: z.ZodOptional<z.ZodEnum<{
127
+ subagent: "subagent";
128
+ primary: "primary";
129
+ all: "all";
130
+ }>>;
131
+ color: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
132
+ primary: "primary";
133
+ secondary: "secondary";
134
+ accent: "accent";
135
+ success: "success";
136
+ warning: "warning";
137
+ error: "error";
138
+ info: "info";
139
+ }>, z.ZodString]>>;
140
+ steps: z.ZodOptional<z.ZodNumber>;
141
+ hidden: z.ZodOptional<z.ZodBoolean>;
142
+ skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
143
+ permission: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
144
+ ask: "ask";
145
+ allow: "allow";
146
+ deny: "deny";
147
+ }>, z.ZodRecord<z.ZodString, z.ZodEnum<{
148
+ ask: "ask";
149
+ allow: "allow";
150
+ deny: "deny";
151
+ }>>]>>>;
152
+ }, z.core.$strict>>>;
153
+ disabled_skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
154
+ disabled_agents: z.ZodDefault<z.ZodArray<z.ZodString>>;
155
+ disabled_commands: z.ZodDefault<z.ZodArray<z.ZodString>>;
156
+ bootstrap: z.ZodDefault<z.ZodObject<{
157
+ enabled: z.ZodDefault<z.ZodBoolean>;
158
+ file: z.ZodOptional<z.ZodString>;
159
+ }, z.core.$strict>>;
160
+ }, z.core.$strict>;
161
+ export interface ValidationResult {
162
+ success: boolean;
163
+ data?: z.infer<typeof SystematicConfigSchema>;
164
+ errors?: readonly z.ZodIssue[];
165
+ }
166
+ export declare function validateConfig(input: unknown): ValidationResult;
167
+ export declare function assertSourceCategoryModelDefaults(defaults: Record<string, unknown>): void;
168
+ /**
169
+ * Overlay fields that require a project-or-higher trust source.
170
+ *
171
+ * This list is co-located with the schema definitions above so that any
172
+ * future field additions that need trust protection are added here at the
173
+ * same time. The regression tests in tests/unit/config-schema.test.ts
174
+ * assert that this list agrees with every field tagged `.meta({ trust:
175
+ * 'project-or-higher' })` in AgentOverlaySchema — preventing silent drift.
176
+ *
177
+ * Matches the hand-coded `SECURITY_OVERLAY_FIELDS` set in `src/lib/config.ts`.
178
+ */
179
+ export declare const SECURITY_OVERLAY_FIELDS: readonly string[];
@@ -1,3 +1,4 @@
1
+ import type { z } from 'zod';
1
2
  export interface BootstrapConfig {
2
3
  enabled: boolean;
3
4
  file?: string;
@@ -26,6 +27,26 @@ export interface SystematicConfig {
26
27
  categories?: OverlayConfigMap;
27
28
  }
28
29
  export declare const DEFAULT_CONFIG: SystematicConfig;
30
+ interface RawSystematicConfig extends Omit<Partial<SystematicConfig>, 'agents' | 'categories'> {
31
+ agents?: unknown;
32
+ categories?: unknown;
33
+ }
34
+ interface ConfigSource {
35
+ path: string;
36
+ config: RawSystematicConfig;
37
+ trust: 'user' | 'project' | 'custom';
38
+ }
39
+ /**
40
+ * Type guard for errors thrown by the top-level config schema validator.
41
+ * Use this to distinguish schema validation failures from JSONC parse errors
42
+ * or file read errors.
43
+ */
44
+ export declare function isConfigSchemaError(err: unknown): err is Error & {
45
+ _tag: 'ConfigSchemaError';
46
+ filePath: string;
47
+ trust: ConfigSource['trust'];
48
+ issues: readonly z.core.$ZodIssue[];
49
+ };
29
50
  export declare function loadConfig(projectDir: string): SystematicConfig;
30
51
  export declare function loadConfigWithSources(projectDir: string): SourceAwareConfigResult;
31
52
  export declare function getConfigPaths(projectDir: string): {
@@ -36,3 +57,4 @@ export declare function getConfigPaths(projectDir: string): {
36
57
  userDir: string;
37
58
  projectDir: string;
38
59
  };
60
+ export {};