@fro.bot/systematic 2.12.2 → 2.13.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 CHANGED
@@ -338,7 +338,7 @@ Systematic separates config-source precedence from overlay precedence. Config fi
338
338
 
339
339
  Source category model defaults are primary model choices only — they are not fallback chains. Systematic does not support `fallback_models`, inherited retry semantics, runtime fallback behavior, or fallback to the parent model when a source model is unavailable. Explicit and source model IDs are structurally validated and may still fail at OpenCode runtime if the provider or model is unavailable.
340
340
 
341
- Source category model defaults are now ordered preference arrays per category rather than single strings. At plugin load, Systematic reads OpenCode's authentication state from `auth.json` and selects the first array entry whose provider is authenticated. For example, the `review` category defaults to `['anthropic/claude-opus-4.7', 'openai/gpt-5.5']` — a user authenticated only to OpenAI receives `openai/gpt-5.5` (first match), while a user authenticated to Anthropic (or both) receives the more preferred `anthropic/claude-opus-4.7`. If no array entry's provider is authenticated, the first entry is used as the default. The arrays are an ordered preference list, not a runtime fallback chain — `fallback_models` is still not supported.
341
+ Source category model defaults are now ordered preference arrays per category rather than single strings. At plugin load, Systematic reads OpenCode's authentication state from `auth.json` and selects the first array entry whose provider is authenticated. For example, the `review` category defaults to `['anthropic/claude-sonnet-4-6', 'openai/gpt-5.3-codex']` — a user authenticated only to OpenAI receives `openai/gpt-5.5` (first match), while a user authenticated to Anthropic (or both) receives the more preferred `anthropic/claude-sonnet-4-6`. If no array entry's provider is authenticated, the first entry is used as the default. The arrays are an ordered preference list, not a runtime fallback chain — `fallback_models` is still not supported.
342
342
 
343
343
  If you want to restore OpenCode parent-model inheritance for a bundled agent or category (opting out of the source default), set `"model": null` in high-trust user or `$OPENCODE_CONFIG_DIR/systematic.json` config. Project config cannot use `model: null` — project config cannot set, erase, or shadow `model` at any value.
344
344
 
@@ -346,11 +346,11 @@ The source defaults are:
346
346
 
347
347
  | Category | Default `model` | Rationale |
348
348
  |----------|-----------------|-----------|
349
- | `design` | `openai/gpt-5.5` | High-judgment UX/product/design work benefits from a strong general reasoning model. |
350
- | `docs` | `openai/gpt-5.4-mini` | Documentation and summarization should start cheaper/faster. |
351
- | `document-review` | `anthropic/claude-opus-4.7` | Requirements and plan critique benefit from strongest nuanced reasoning. |
352
- | `research` | `openai/gpt-5.5` | Tool-heavy synthesis and source evaluation benefit from a strong general reasoning model. |
353
- | `review` | `anthropic/claude-opus-4.7` | Code/security/adversarial review benefits from strongest reasoning. |
349
+ | `design` | `github-copilot/gemini-3.1-pro-preview` | High-judgment UX/product/design work benefits from a strong general reasoning model. |
350
+ | `docs` | `github-copilot/gemini-3.1-pro-preview` | Documentation and summarization should start cheaper/faster. |
351
+ | `document-review` | `anthropic/claude-opus-4-7` | Requirements and plan critique benefit from strongest nuanced reasoning. |
352
+ | `research` | `openai/gpt-5.4-mini` | Tool-heavy synthesis and source evaluation benefit from a strong general reasoning model. |
353
+ | `review` | `anthropic/claude-sonnet-4-6` | Code/security/adversarial review benefits from strongest reasoning. |
354
354
  | `workflow` | `openai/gpt-5.4-mini` | Orchestration and bounded implementation should default cheaper/faster. |
355
355
 
356
356
  These defaults are owned by Systematic code and emitted for bundled agents in each category when no stronger high-trust exact or category `model` override exists. Uncategorized bundled agents receive no source default and continue inheriting the parent OpenCode model. Native OpenCode agents with the same emitted key are full replacements and receive no Systematic source model default.
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  findCommandsInDir,
7
7
  findSkillsInDir,
8
8
  getConfigPaths
9
- } from "./index-b4ht76qd.js";
9
+ } from "./index-4qhjf4tb.js";
10
10
 
11
11
  // src/cli.ts
12
12
  import fs from "fs";
@@ -12324,7 +12324,7 @@ function finalize(ctx, schema) {
12324
12324
  result.$schema = "http://json-schema.org/draft-07/schema#";
12325
12325
  } else if (ctx.target === "draft-04") {
12326
12326
  result.$schema = "http://json-schema.org/draft-04/schema#";
12327
- } else if (ctx.target === "openapi-3.0") {} else {}
12327
+ } else if (ctx.target === "openapi-3.0") {}
12328
12328
  if (ctx.external?.uri) {
12329
12329
  const id = ctx.external.registry.get(schema)?.id;
12330
12330
  if (!id)
@@ -12568,7 +12568,7 @@ var literalProcessor = (schema, ctx, json, _params) => {
12568
12568
  if (val === undefined) {
12569
12569
  if (ctx.unrepresentable === "throw") {
12570
12570
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
12571
- } else {}
12571
+ }
12572
12572
  } else if (typeof val === "bigint") {
12573
12573
  if (ctx.unrepresentable === "throw") {
12574
12574
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -15163,7 +15163,7 @@ var modelSchema = exports_external.string().min(1).regex(MODEL_FORMAT_REGEX, MOD
15163
15163
  description: "Model identifier in provider/model format, or null to inherit parent model",
15164
15164
  examples: ["anthropic/claude-sonnet-4", null]
15165
15165
  });
15166
- var variantSchema = exports_external.string().min(1).regex(/^\S+$/, "must be a non-empty string without whitespace").meta({
15166
+ var variantSchema = exports_external.string().min(1).max(128, "variant must be at most 128 characters").regex(/^\S+$/, "must be a non-empty string without whitespace").meta({
15167
15167
  description: "Model variant identifier",
15168
15168
  examples: ["v2", "extended"]
15169
15169
  });
@@ -15224,7 +15224,7 @@ var AgentOverlaySchema = exports_external.object({
15224
15224
  description: "Per-agent configuration overlay",
15225
15225
  examples: [
15226
15226
  {
15227
- model: "anthropic/claude-opus-4.7",
15227
+ model: "anthropic/claude-opus-4-7",
15228
15228
  temperature: 0.1,
15229
15229
  mode: "subagent"
15230
15230
  }
@@ -15243,7 +15243,7 @@ var CategoryOverlaySchema = exports_external.object({
15243
15243
  permission: trustProtected(permissionSchema).optional()
15244
15244
  }).strict().meta({
15245
15245
  description: "Per-category configuration overlay (same fields as agent minus disable)",
15246
- examples: [{ model: "anthropic/claude-opus-4.7", temperature: 0.1 }]
15246
+ examples: [{ model: "anthropic/claude-opus-4-7", temperature: 0.1 }]
15247
15247
  });
15248
15248
  var BootstrapSchema = exports_external.object({
15249
15249
  enabled: exports_external.boolean().default(true).meta({
@@ -15271,7 +15271,7 @@ var SystematicConfigSchema = exports_external.object({
15271
15271
  }),
15272
15272
  categories: exports_external.record(exports_external.string(), CategoryOverlaySchema).default({}).meta({
15273
15273
  description: "Per-category configuration overlays keyed by category name",
15274
- examples: [{ review: { model: "anthropic/claude-opus-4.7" } }, {}]
15274
+ examples: [{ review: { model: "anthropic/claude-opus-4-7" } }, {}]
15275
15275
  }),
15276
15276
  disabled_skills: exports_external.array(exports_external.string()).default([]).meta({
15277
15277
  description: "Array of skill names to disable globally",
@@ -15296,13 +15296,6 @@ var SystematicConfigSchema = exports_external.object({
15296
15296
  description: "Systematic user configuration file (systematic.json / systematic.jsonc)",
15297
15297
  examples: [{ disabled_skills: ["ce:plan"], bootstrap: { enabled: false } }]
15298
15298
  });
15299
- var SourceCategoryModelDefaultsSchema = exports_external.record(exports_external.string(), exports_external.array(exports_external.string().min(1).regex(MODEL_FORMAT_REGEX, MODEL_FORMAT_MESSAGE)).min(1)).meta({
15300
- description: "Validates source category model defaults shape",
15301
- examples: [{ design: ["openai/gpt-5.5", "anthropic/claude-opus-4.7"] }]
15302
- });
15303
- function assertSourceCategoryModelDefaults(defaults) {
15304
- SourceCategoryModelDefaultsSchema.parse(defaults);
15305
- }
15306
15299
  var SECURITY_OVERLAY_FIELDS = [
15307
15300
  "model",
15308
15301
  "variant",
@@ -16101,4 +16094,4 @@ function findSkillsInDir(dir, maxDepth = 3) {
16101
16094
  return skills;
16102
16095
  }
16103
16096
 
16104
- export { parseFrontmatter, AgentOverlaySchema, CategoryOverlaySchema, assertSourceCategoryModelDefaults, loadConfig, loadConfigWithSources, getConfigPaths, isRecord2 as isRecord, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, convertContent, convertFileWithCache, findSkillsInDir };
16097
+ export { parseFrontmatter, exports_external, AgentOverlaySchema, CategoryOverlaySchema, loadConfig, loadConfigWithSources, getConfigPaths, isRecord2 as isRecord, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, convertContent, convertFileWithCache, findSkillsInDir };
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  AgentOverlaySchema,
4
4
  CategoryOverlaySchema,
5
- assertSourceCategoryModelDefaults,
6
5
  convertFileWithCache,
6
+ exports_external,
7
7
  extractAgentFrontmatter,
8
8
  extractCommandFrontmatter,
9
9
  findAgentsInDir,
@@ -13,12 +13,12 @@ import {
13
13
  loadConfig,
14
14
  loadConfigWithSources,
15
15
  parseFrontmatter
16
- } from "./index-b4ht76qd.js";
16
+ } from "./index-4qhjf4tb.js";
17
17
 
18
18
  // src/index.ts
19
- import fs4 from "fs";
20
- import path5 from "path";
21
- import { fileURLToPath } from "url";
19
+ import fs5 from "fs";
20
+ import path7 from "path";
21
+ import { fileURLToPath as fileURLToPath2 } from "url";
22
22
 
23
23
  // src/lib/bootstrap.ts
24
24
  import fs from "fs";
@@ -107,30 +107,232 @@ ${toolMapping}
107
107
 
108
108
  // src/lib/agent-overlays.ts
109
109
  import fs2 from "fs";
110
- import os2 from "os";
111
- import path2 from "path";
110
+ import path3 from "path";
111
+
112
+ // src/lib/source-model-defaults.ts
113
+ import * as path2 from "path";
114
+ import { fileURLToPath } from "url";
115
+ var __filename2 = fileURLToPath(import.meta.url);
116
+ var __dirname2 = path2.dirname(__filename2);
117
+ var packageRoot = path2.resolve(__dirname2, "..", "..");
118
+ var bundledAgentsDir = path2.join(packageRoot, "agents");
119
+ var ProviderID = exports_external.union([
120
+ exports_external.literal("vercel"),
121
+ exports_external.literal("opencode"),
122
+ exports_external.literal("github-copilot"),
123
+ exports_external.literal("opencode-go"),
124
+ exports_external.literal("openai"),
125
+ exports_external.literal("anthropic"),
126
+ exports_external.literal("google")
127
+ ]);
128
+ var variantSchema = exports_external.string().min(1, "variant must be a non-empty string").max(128, "variant must be at most 128 characters").regex(/^\S+$/, "variant must not contain whitespace");
129
+ var tableCellSchema = exports_external.string().min(1, "must be a non-empty string").regex(/^[^|\r\n]+$/, "must not contain pipe (|) or newline characters \u2014 these break Markdown table generation");
130
+ var ModelEntrySchema = exports_external.object({
131
+ model: exports_external.string().min(1, "model must be a non-empty string"),
132
+ variant: variantSchema.optional()
133
+ }).strict();
134
+ var ProviderEntrySchema = exports_external.object({
135
+ provider: ProviderID,
136
+ models: exports_external.array(ModelEntrySchema).min(1, "models must be non-empty \u2014 every provider entry must list at least one model")
137
+ }).strict().refine((entry) => {
138
+ const seen = new Set;
139
+ for (const m of entry.models) {
140
+ const key = `${m.model}::${m.variant ?? ""}`;
141
+ if (seen.has(key))
142
+ return false;
143
+ seen.add(key);
144
+ }
145
+ return true;
146
+ }, {
147
+ message: "duplicate (model, variant) pair within a provider entry \u2014 each model+variant combination must be unique"
148
+ });
149
+ var CategoryDefaultSchema = exports_external.object({
150
+ rationale: tableCellSchema,
151
+ whenToOverride: tableCellSchema.optional(),
152
+ providers: exports_external.array(ProviderEntrySchema).min(1, "providers must be non-empty \u2014 every category must list at least one provider")
153
+ }).strict().refine((cat) => {
154
+ const seen = new Set;
155
+ for (const p of cat.providers) {
156
+ if (seen.has(p.provider))
157
+ return false;
158
+ seen.add(p.provider);
159
+ }
160
+ return true;
161
+ }, {
162
+ message: "duplicate provider ID within a category \u2014 each provider must appear at most once per category"
163
+ });
164
+ var SourceCategoryDefaultsSchema = exports_external.record(exports_external.string(), CategoryDefaultSchema);
112
165
  var SOURCE_CATEGORY_MODEL_DEFAULTS = {
113
- design: ["openai/gpt-5.5", "anthropic/claude-opus-4.7"],
114
- docs: ["openai/gpt-5.4-mini", "anthropic/claude-haiku-4-5"],
115
- "document-review": ["anthropic/claude-opus-4.7", "openai/gpt-5.5"],
116
- research: ["openai/gpt-5.5", "anthropic/claude-opus-4.7"],
117
- review: ["anthropic/claude-opus-4.7", "openai/gpt-5.5"],
118
- workflow: ["openai/gpt-5.4-mini", "anthropic/claude-haiku-4-5"]
166
+ design: {
167
+ rationale: "High-judgment UX, product, and design work benefits from a strong general reasoning model with broad creative capability.",
168
+ whenToOverride: "Override to a faster/cheaper model when design tasks are primarily templating or low-stakes layout work.",
169
+ providers: [
170
+ {
171
+ provider: "github-copilot",
172
+ models: [{ model: "gemini-3.1-pro-preview" }]
173
+ },
174
+ {
175
+ provider: "openai",
176
+ models: [{ model: "gpt-5.5", variant: "high" }]
177
+ },
178
+ {
179
+ provider: "anthropic",
180
+ models: [{ model: "claude-opus-4-7", variant: "max" }]
181
+ },
182
+ {
183
+ provider: "vercel",
184
+ models: [{ model: "v0-1.5-md" }]
185
+ }
186
+ ]
187
+ },
188
+ docs: {
189
+ rationale: "Documentation and summarization tasks should start cheaper and faster; quality is sufficient at mid-tier models.",
190
+ providers: [
191
+ {
192
+ provider: "github-copilot",
193
+ models: [{ model: "gemini-3.1-pro-preview" }]
194
+ },
195
+ {
196
+ provider: "openai",
197
+ models: [{ model: "gpt-5.4-mini" }]
198
+ },
199
+ {
200
+ provider: "anthropic",
201
+ models: [{ model: "claude-haiku-4-5" }]
202
+ },
203
+ {
204
+ provider: "opencode",
205
+ models: [{ model: "claude-haiku-4-5" }]
206
+ }
207
+ ]
208
+ },
209
+ "document-review": {
210
+ rationale: "Requirements and plan critique benefit from the strongest nuanced reasoning to surface non-obvious gaps and contradictions.",
211
+ providers: [
212
+ {
213
+ provider: "anthropic",
214
+ models: [{ model: "claude-opus-4-7", variant: "max" }]
215
+ },
216
+ {
217
+ provider: "openai",
218
+ models: [{ model: "gpt-5.5", variant: "high" }]
219
+ },
220
+ {
221
+ provider: "github-copilot",
222
+ models: [{ model: "gemini-3.1-pro-preview" }]
223
+ },
224
+ {
225
+ provider: "opencode",
226
+ models: [{ model: "claude-sonnet-4-6" }]
227
+ }
228
+ ]
229
+ },
230
+ research: {
231
+ rationale: "Tool-heavy synthesis and source evaluation benefit from a strong general reasoning model with broad knowledge.",
232
+ providers: [
233
+ {
234
+ provider: "openai",
235
+ models: [{ model: "gpt-5.4-mini" }]
236
+ },
237
+ {
238
+ provider: "anthropic",
239
+ models: [{ model: "claude-sonnet-4-6" }]
240
+ },
241
+ {
242
+ provider: "github-copilot",
243
+ models: [{ model: "gemini-3.1-pro-preview" }]
244
+ },
245
+ {
246
+ provider: "opencode",
247
+ models: [{ model: "claude-sonnet-4-6" }]
248
+ }
249
+ ]
250
+ },
251
+ review: {
252
+ rationale: "Code, security, and adversarial review benefits from the strongest reasoning to catch subtle bugs and security issues.",
253
+ whenToOverride: "Override to a faster model when review tasks are primarily style or formatting checks rather than correctness or security analysis.",
254
+ providers: [
255
+ {
256
+ provider: "anthropic",
257
+ models: [{ model: "claude-sonnet-4-6" }]
258
+ },
259
+ {
260
+ provider: "openai",
261
+ models: [{ model: "gpt-5.3-codex" }]
262
+ },
263
+ {
264
+ provider: "github-copilot",
265
+ models: [{ model: "gemini-3.1-pro-preview" }]
266
+ },
267
+ {
268
+ provider: "opencode",
269
+ models: [{ model: "claude-sonnet-4-6" }]
270
+ }
271
+ ]
272
+ },
273
+ workflow: {
274
+ rationale: "Orchestration and bounded implementation tasks should default cheaper and faster; strong reasoning is rarely needed for routing decisions.",
275
+ providers: [
276
+ {
277
+ provider: "openai",
278
+ models: [{ model: "gpt-5.4-mini" }]
279
+ },
280
+ {
281
+ provider: "anthropic",
282
+ models: [{ model: "claude-sonnet-4-6" }]
283
+ },
284
+ {
285
+ provider: "opencode",
286
+ models: [{ model: "claude-haiku-4-5" }]
287
+ },
288
+ {
289
+ provider: "opencode-go",
290
+ models: [{ model: "claude-haiku-4-5" }]
291
+ }
292
+ ]
293
+ }
119
294
  };
295
+ function resolveSourceModel(category, availabilitySet) {
296
+ const categoryDefault = SOURCE_CATEGORY_MODEL_DEFAULTS[category];
297
+ if (!categoryDefault) {
298
+ throw new Error(`resolveSourceModel: unknown category "${category}". Valid categories: ${Object.keys(SOURCE_CATEGORY_MODEL_DEFAULTS).join(", ")}`);
299
+ }
300
+ for (const providerEntry of categoryDefault.providers) {
301
+ for (const modelEntry of providerEntry.models) {
302
+ const key = `${providerEntry.provider}/${modelEntry.model}`;
303
+ if (availabilitySet.has(key)) {
304
+ return {
305
+ provider: providerEntry.provider,
306
+ model: modelEntry.model,
307
+ variant: modelEntry.variant
308
+ };
309
+ }
310
+ }
311
+ }
312
+ const firstProvider = categoryDefault.providers[0];
313
+ const firstModel = firstProvider.models[0];
314
+ return {
315
+ provider: firstProvider.provider,
316
+ model: firstModel.model,
317
+ variant: firstModel.variant
318
+ };
319
+ }
320
+
321
+ // src/lib/agent-overlays.ts
120
322
  function buildBundledAgentInventory(agentsDir, disabledAgents) {
121
323
  const categories = readCategoryDirs(agentsDir);
122
324
  const agentsByQualifiedId = {};
123
325
  const stemCategories = new Map;
124
326
  const disabledSet = new Set(disabledAgents);
125
327
  for (const category of categories) {
126
- for (const fileName of readMarkdownFiles(path2.join(agentsDir, category))) {
328
+ for (const fileName of readMarkdownFiles(path3.join(agentsDir, category))) {
127
329
  const key = fileName.replace(/\.md$/, "");
128
330
  const id = `${category}/${key}`;
129
331
  agentsByQualifiedId[id] = {
130
332
  id,
131
333
  key,
132
334
  category,
133
- file: path2.join(agentsDir, category, fileName),
335
+ file: path3.join(agentsDir, category, fileName),
134
336
  disabled: disabledSet.has(key) || disabledSet.has(id)
135
337
  };
136
338
  const existing = stemCategories.get(key) ?? [];
@@ -183,62 +385,12 @@ function inferBuiltInTemperature(name, description) {
183
385
  }
184
386
  return 0.3;
185
387
  }
186
- function getAuthenticatedProviders(rootDirOverride) {
187
- const xdgDataHome = process.env.XDG_DATA_HOME?.trim();
188
- const rootDir = rootDirOverride || (xdgDataHome && path2.isAbsolute(xdgDataHome) ? xdgDataHome : path2.join(os2.homedir(), ".local/share"));
189
- const authPath = path2.join(rootDir, "opencode", "auth.json");
190
- let raw;
191
- try {
192
- raw = fs2.readFileSync(authPath, "utf8");
193
- } catch (err) {
194
- if (isSystemError(err) && err.code === "ENOENT") {
195
- return new Set;
196
- }
197
- console.warn(`[systematic] auth.json unreadable at ${authPath}; ignoring`);
198
- return new Set;
199
- }
200
- let parsed;
201
- try {
202
- parsed = JSON.parse(raw);
203
- } catch {
204
- console.warn(`[systematic] auth.json malformed at ${authPath}; ignoring`);
205
- return new Set;
206
- }
207
- if (!isRecord(parsed)) {
208
- console.warn(`[systematic] auth.json malformed at ${authPath}; ignoring`);
209
- return new Set;
210
- }
211
- return new Set(Object.keys(parsed));
212
- }
213
- function getSourceCategoryModel(category, authedProviders) {
214
- if (!category)
215
- return;
216
- const candidates = SOURCE_CATEGORY_MODEL_DEFAULTS[category];
217
- if (!candidates || candidates.length === 0)
218
- return;
219
- if (!authedProviders || authedProviders.size === 0)
220
- return candidates[0];
221
- for (const entry of candidates) {
222
- const slashIndex = entry.indexOf("/");
223
- if (slashIndex <= 0)
224
- continue;
225
- const providerId = entry.slice(0, slashIndex);
226
- if (authedProviders.has(providerId)) {
227
- return entry;
228
- }
229
- }
230
- return candidates[0];
231
- }
232
388
  function assertSourceCategoryModelCoverage(categories) {
233
- validateSourceCategoryModelDefaults();
234
389
  const missingCategories = categories.filter((category) => !Object.hasOwn(SOURCE_CATEGORY_MODEL_DEFAULTS, category));
235
390
  if (missingCategories.length > 0) {
236
391
  throw new Error(`Source category model defaults missing intentional coverage for: ${missingCategories.join(", ")}`);
237
392
  }
238
393
  }
239
- function validateSourceCategoryModelDefaults(defaults = SOURCE_CATEGORY_MODEL_DEFAULTS) {
240
- assertSourceCategoryModelDefaults(defaults);
241
- }
242
394
  function validateExactAgentOverlays(inventory, overlays, nativeAgents, enabledSkills) {
243
395
  const result = [];
244
396
  const seenTargets = new Map;
@@ -339,12 +491,158 @@ function validAgentKeys(inventory) {
339
491
  function throwConfigError(sourcePath, keyPath, message) {
340
492
  throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath} ${message}`);
341
493
  }
342
- function isSystemError(err) {
343
- return typeof err === "object" && err !== null && "code" in err && typeof err.code === "string";
494
+
495
+ // src/lib/model-availability.ts
496
+ import { createHash } from "crypto";
497
+ import fs3 from "fs";
498
+ import os2 from "os";
499
+ import path4 from "path";
500
+ function emptyAvailability() {
501
+ return { status: "unknown", models: new Set };
502
+ }
503
+ var MAX_CACHE_FILE_BYTES = 16 * 1024 * 1024;
504
+ var DEFAULT_API_TIMEOUT_MS = 1500;
505
+ var MODELS_JSON_FILENAME = "models.json";
506
+ function resolveCacheDir() {
507
+ const xdgCacheHome = process.env.XDG_CACHE_HOME?.trim();
508
+ const cacheBase = xdgCacheHome && path4.isAbsolute(xdgCacheHome) ? xdgCacheHome : path4.join(os2.homedir(), ".cache");
509
+ return path4.join(cacheBase, "opencode");
510
+ }
511
+ function fastHash(input) {
512
+ return createHash("sha1").update(input).digest("hex");
513
+ }
514
+ function isProviderRecord(value) {
515
+ if (!isRecord(value))
516
+ return false;
517
+ for (const entry of Object.values(value)) {
518
+ if (!isRecord(entry))
519
+ return false;
520
+ if (!isRecord(entry.models))
521
+ return false;
522
+ }
523
+ return true;
524
+ }
525
+ function readModelsFromCache(filePath) {
526
+ let fd;
527
+ try {
528
+ fd = fs3.openSync(filePath, "r");
529
+ } catch {
530
+ return null;
531
+ }
532
+ let raw;
533
+ try {
534
+ let stat;
535
+ try {
536
+ stat = fs3.fstatSync(fd);
537
+ } catch {
538
+ return null;
539
+ }
540
+ if (!stat.isFile())
541
+ return null;
542
+ if (stat.size === 0)
543
+ return null;
544
+ if (stat.size > MAX_CACHE_FILE_BYTES) {
545
+ console.warn(`[systematic] models.json at ${filePath} is ${stat.size} bytes (>${MAX_CACHE_FILE_BYTES}); treating as cache miss.`);
546
+ return null;
547
+ }
548
+ const buffer = Buffer.alloc(stat.size);
549
+ let bytesRead;
550
+ try {
551
+ bytesRead = fs3.readSync(fd, buffer, 0, stat.size, 0);
552
+ } catch {
553
+ return null;
554
+ }
555
+ if (bytesRead !== stat.size)
556
+ return null;
557
+ raw = buffer.toString("utf8");
558
+ } finally {
559
+ try {
560
+ fs3.closeSync(fd);
561
+ } catch {}
562
+ }
563
+ if (raw.trim().length === 0)
564
+ return null;
565
+ let parsed;
566
+ try {
567
+ parsed = JSON.parse(raw);
568
+ } catch {
569
+ return null;
570
+ }
571
+ if (!isProviderRecord(parsed)) {
572
+ console.warn(`[systematic] models.json schema mismatch at ${filePath}; treating as cache miss. Upstream shape may have drifted.`);
573
+ return null;
574
+ }
575
+ const result = new Set;
576
+ for (const [providerId, providerData] of Object.entries(parsed)) {
577
+ for (const modelId of Object.keys(providerData.models)) {
578
+ result.add(`${providerId}/${modelId}`);
579
+ }
580
+ }
581
+ return result;
582
+ }
583
+ function readFallbackCache() {
584
+ const cacheDir = resolveCacheDir();
585
+ const openCodeModelsUrl = process.env.OPENCODE_MODELS_URL?.trim();
586
+ if (openCodeModelsUrl) {
587
+ const urlDerivedPath = path4.join(cacheDir, `models-${fastHash(openCodeModelsUrl)}.json`);
588
+ const urlResult = readModelsFromCache(urlDerivedPath);
589
+ if (urlResult !== null) {
590
+ return { status: "cache", models: urlResult };
591
+ }
592
+ }
593
+ const defaultPath = path4.join(cacheDir, MODELS_JSON_FILENAME);
594
+ const defaultResult = readModelsFromCache(defaultPath);
595
+ if (defaultResult !== null) {
596
+ return { status: "cache", models: defaultResult };
597
+ }
598
+ return emptyAvailability();
599
+ }
600
+ function buildSetFromProviders(providers) {
601
+ const result = new Set;
602
+ for (const provider of providers) {
603
+ for (const modelId of Object.keys(provider.models)) {
604
+ result.add(`${provider.id}/${modelId}`);
605
+ }
606
+ }
607
+ return result;
608
+ }
609
+ async function getAvailableModels(client, options = {}) {
610
+ const timeoutMs = options.apiTimeoutMs === undefined ? DEFAULT_API_TIMEOUT_MS : options.apiTimeoutMs;
611
+ if (typeof client.config?.providers !== "function") {
612
+ return readFallbackCache();
613
+ }
614
+ const apiCall = client.config.providers();
615
+ let response;
616
+ try {
617
+ if (timeoutMs === null) {
618
+ response = await apiCall;
619
+ } else {
620
+ const TIMEOUT_SENTINEL = Symbol("timeout");
621
+ const timeoutPromise = new Promise((resolve2) => {
622
+ const timer = setTimeout(() => resolve2(TIMEOUT_SENTINEL), timeoutMs);
623
+ timer.unref?.();
624
+ });
625
+ const raced = await Promise.race([apiCall, timeoutPromise]);
626
+ if (raced === TIMEOUT_SENTINEL) {
627
+ console.warn(`[systematic] client.config.providers() exceeded ${timeoutMs}ms; falling back to models.json cache.`);
628
+ return readFallbackCache();
629
+ }
630
+ response = raced;
631
+ }
632
+ } catch {
633
+ return readFallbackCache();
634
+ }
635
+ if (response.error !== undefined || response.data === undefined) {
636
+ return readFallbackCache();
637
+ }
638
+ return {
639
+ status: "api",
640
+ models: buildSetFromProviders(response.data.providers)
641
+ };
344
642
  }
345
643
 
346
644
  // src/lib/skill-loader.ts
347
- import path3 from "path";
645
+ import path5 from "path";
348
646
  var SKILL_PREFIX = "systematic:";
349
647
  var SKILL_DESCRIPTION_PREFIX = "(Systematic - Skill) ";
350
648
  function formatSkillCommandName(name) {
@@ -361,7 +659,7 @@ function formatSkillDescription(description, fallbackName) {
361
659
  return `${SKILL_DESCRIPTION_PREFIX}${desc}`;
362
660
  }
363
661
  function wrapSkillTemplate(skillPath, body) {
364
- const skillDir = path3.dirname(skillPath);
662
+ const skillDir = path5.dirname(skillPath);
365
663
  return `<skill-instruction>
366
664
  Base directory for this skill: ${skillDir}/
367
665
  File references (@path) in this skill are relative to this directory.
@@ -511,7 +809,7 @@ function loadSkillAsCommand(loaded) {
511
809
  config.subtask = loaded.subtask;
512
810
  return config;
513
811
  }
514
- function collectAgents(dir, disabledAgents, nativeAgents, overlays, authedProviders) {
812
+ function collectAgents(dir, disabledAgents, nativeAgents, overlays, availabilitySet) {
515
813
  const agents = {};
516
814
  const agentList = findAgentsInDir(dir);
517
815
  const disabledSet = new Set(disabledAgents);
@@ -526,12 +824,12 @@ function collectAgents(dir, disabledAgents, nativeAgents, overlays, authedProvid
526
824
  continue;
527
825
  const config = loadAgentAsConfig(agentInfo);
528
826
  if (config) {
529
- agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays, authedProviders);
827
+ agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays, availabilitySet);
530
828
  }
531
829
  }
532
830
  return agents;
533
831
  }
534
- function applyAgentOverlays(config, agentInfo, overlays, authedProviders) {
832
+ function applyAgentOverlays(config, agentInfo, overlays, availabilitySet) {
535
833
  const id = agentInfo.category ? `${agentInfo.category}/${agentInfo.name}` : agentInfo.name;
536
834
  const categoryOverlay = agentInfo.category ? overlays.categoriesByKey.get(agentInfo.category) : undefined;
537
835
  const exactOverlay = overlays.agentsByTargetId.get(id);
@@ -542,17 +840,20 @@ function applyAgentOverlays(config, agentInfo, overlays, authedProviders) {
542
840
  addPermissionRules(permissionRules, config.permission);
543
841
  }
544
842
  result.temperature = inferBuiltInTemperature(agentInfo.name, result.description);
545
- if (agentInfo.category) {
546
- const sourceModel = getSourceCategoryModel(agentInfo.category, authedProviders);
547
- if (sourceModel) {
548
- result.model = sourceModel;
843
+ if (agentInfo.category && availabilitySet !== undefined) {
844
+ const resolved = resolveSourceModel(agentInfo.category, availabilitySet);
845
+ result.model = `${resolved.provider}/${resolved.model}`;
846
+ if (resolved.variant !== undefined) {
847
+ result.variant = resolved.variant;
848
+ } else {
849
+ delete result.variant;
549
850
  }
550
851
  }
551
852
  if (categoryOverlay) {
552
- applyOverlayObject(result, categoryOverlay.value, permissionRules);
853
+ applyOverlayObjectWithVariantClearing(result, categoryOverlay.value, permissionRules);
553
854
  }
554
855
  if (exactOverlay) {
555
- applyOverlayObject(result, exactOverlay.value, permissionRules);
856
+ applyOverlayObjectWithVariantClearing(result, exactOverlay.value, permissionRules);
556
857
  }
557
858
  if (hasPermissionOverlay) {
558
859
  const permission = permissionFromRules(permissionRules);
@@ -577,7 +878,12 @@ var OVERLAY_ASSIGN_FIELDS = [
577
878
  "steps",
578
879
  "hidden"
579
880
  ];
580
- function applyOverlayObject(target, overlay, permissionRules) {
881
+ function applyOverlayObjectWithVariantClearing(target, overlay, permissionRules) {
882
+ const overlayHasModel = Object.hasOwn(overlay, "model");
883
+ const overlayHasVariant = Object.hasOwn(overlay, "variant");
884
+ if (overlayHasModel && !overlayHasVariant) {
885
+ delete target.variant;
886
+ }
581
887
  for (const field of OVERLAY_ASSIGN_FIELDS) {
582
888
  if (Object.hasOwn(overlay, field)) {
583
889
  if (field === "model" && overlay[field] === null) {
@@ -672,15 +978,14 @@ function collectEnabledSkillNames(dir, disabledSkills) {
672
978
  return findSkillsInDir(dir).filter((skillInfo) => !disabledSet.has(skillInfo.name)).map((skillInfo) => skillInfo.name);
673
979
  }
674
980
  function createConfigHandler(deps) {
675
- const { directory, bundledSkillsDir, bundledAgentsDir, bundledCommandsDir } = deps;
676
- const readAuthProviders = deps.getAuthenticatedProviders ?? getAuthenticatedProviders;
981
+ const { directory, bundledSkillsDir, bundledAgentsDir: bundledAgentsDir2, bundledCommandsDir } = deps;
677
982
  return async (config) => {
678
983
  const { config: systematicConfig, overlays } = loadConfigWithSources(directory);
679
984
  const existingAgents = { ...config.agent ?? {} };
680
985
  const existingCommands = { ...config.command ?? {} };
681
986
  const bundledSkills = collectSkillsAsCommands(bundledSkillsDir, systematicConfig.disabled_skills);
682
987
  const enabledSkillNames = collectEnabledSkillNames(bundledSkillsDir, systematicConfig.disabled_skills);
683
- const inventory = buildBundledAgentInventory(bundledAgentsDir, systematicConfig.disabled_agents);
988
+ const inventory = buildBundledAgentInventory(bundledAgentsDir2, systematicConfig.disabled_agents);
684
989
  assertSourceCategoryModelCoverage(inventory.categories);
685
990
  const validatedOverlays = validateAgentOverlays({
686
991
  inventory,
@@ -689,8 +994,9 @@ function createConfigHandler(deps) {
689
994
  enabledSkills: enabledSkillNames
690
995
  });
691
996
  const resolvedOverlays = resolveAgentOverlaySet(validatedOverlays);
692
- const authedProviders = readAuthProviders();
693
- const bundledAgents = collectAgents(bundledAgentsDir, systematicConfig.disabled_agents, existingAgents, resolvedOverlays, authedProviders);
997
+ const availability = deps.client ? await getAvailableModels(deps.client) : undefined;
998
+ const availabilitySet = availability && availability.status !== "unknown" ? availability.models : undefined;
999
+ const bundledAgents = collectAgents(bundledAgentsDir2, systematicConfig.disabled_agents, existingAgents, resolvedOverlays, availabilitySet);
694
1000
  const bundledCommands = collectCommands(bundledCommandsDir, systematicConfig.disabled_commands);
695
1001
  config.agent = {
696
1002
  ...bundledAgents,
@@ -715,8 +1021,8 @@ function registerSkillsPaths(config, skillsDir) {
715
1021
  }
716
1022
 
717
1023
  // src/lib/skill-tool.ts
718
- import fs3 from "fs";
719
- import path4 from "path";
1024
+ import fs4 from "fs";
1025
+ import path6 from "path";
720
1026
  import { pathToFileURL } from "url";
721
1027
  import { tool } from "@opencode-ai/plugin/tool";
722
1028
  function formatSkillsXml(skills) {
@@ -742,17 +1048,17 @@ function discoverSkillFiles(dir, limit = 10) {
742
1048
  function handleEntry(entry, currentDir) {
743
1049
  if (entry.isDirectory()) {
744
1050
  if (!shouldSkipDirectory(entry.name)) {
745
- recurse(path4.resolve(currentDir, entry.name));
1051
+ recurse(path6.resolve(currentDir, entry.name));
746
1052
  }
747
1053
  } else if (shouldIncludeFile(entry.name)) {
748
- files.push(path4.resolve(currentDir, entry.name));
1054
+ files.push(path6.resolve(currentDir, entry.name));
749
1055
  }
750
1056
  }
751
1057
  function recurse(currentDir) {
752
1058
  if (files.length >= limit)
753
1059
  return;
754
1060
  try {
755
- const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
1061
+ const entries = fs4.readdirSync(currentDir, { withFileTypes: true });
756
1062
  for (const entry of entries) {
757
1063
  if (files.length >= limit)
758
1064
  break;
@@ -833,7 +1139,7 @@ function createSkillTool(options) {
833
1139
  throw new Error(`Skill "${requestedName}" not found. Available systematic skills: ${availableSystematic.join(", ")}`);
834
1140
  }
835
1141
  const body = extractSkillBody(matchedSkill.wrappedTemplate);
836
- const dir = path4.dirname(matchedSkill.skillFile);
1142
+ const dir = path6.dirname(matchedSkill.skillFile);
837
1143
  const base = pathToFileURL(dir).href;
838
1144
  const files = discoverSkillFiles(dir);
839
1145
  await context.ask({
@@ -870,17 +1176,17 @@ function createSkillTool(options) {
870
1176
  }
871
1177
 
872
1178
  // src/index.ts
873
- var __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
874
- var packageRoot = path5.resolve(__dirname2, "..");
875
- var bundledSkillsDir = path5.join(packageRoot, "skills");
876
- var bundledAgentsDir = path5.join(packageRoot, "agents");
877
- var bundledCommandsDir = path5.join(packageRoot, "commands");
878
- var packageJsonPath = path5.join(packageRoot, "package.json");
1179
+ var __dirname3 = path7.dirname(fileURLToPath2(import.meta.url));
1180
+ var packageRoot2 = path7.resolve(__dirname3, "..");
1181
+ var bundledSkillsDir = path7.join(packageRoot2, "skills");
1182
+ var bundledAgentsDir2 = path7.join(packageRoot2, "agents");
1183
+ var bundledCommandsDir = path7.join(packageRoot2, "commands");
1184
+ var packageJsonPath = path7.join(packageRoot2, "package.json");
879
1185
  var getPackageVersion = () => {
880
1186
  try {
881
- if (!fs4.existsSync(packageJsonPath))
1187
+ if (!fs5.existsSync(packageJsonPath))
882
1188
  return "unknown";
883
- const content = fs4.readFileSync(packageJsonPath, "utf8");
1189
+ const content = fs5.readFileSync(packageJsonPath, "utf8");
884
1190
  const parsed = JSON.parse(content);
885
1191
  return parsed.version ?? "unknown";
886
1192
  } catch {
@@ -894,8 +1200,9 @@ var initializePlugin = async ({ client, directory }) => {
894
1200
  const configHandler = createConfigHandler({
895
1201
  directory,
896
1202
  bundledSkillsDir,
897
- bundledAgentsDir,
898
- bundledCommandsDir
1203
+ bundledAgentsDir: bundledAgentsDir2,
1204
+ bundledCommandsDir,
1205
+ client
899
1206
  });
900
1207
  return {
901
1208
  config: configHandler,
@@ -42,27 +42,4 @@ 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
- /**
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;
67
45
  export declare function assertSourceCategoryModelCoverage(categories: string[]): void;
68
- export declare function validateSourceCategoryModelDefaults(defaults?: Record<string, unknown>): void;
@@ -1,11 +1,12 @@
1
1
  import type { Config } from '@opencode-ai/plugin';
2
+ import { type OpencodeClientLike } from './model-availability.js';
2
3
  export interface ConfigHandlerDeps {
3
4
  directory: string;
4
5
  bundledSkillsDir: string;
5
6
  bundledAgentsDir: string;
6
7
  bundledCommandsDir: string;
7
- /** Override for authenticated provider reader; for testing. */
8
- getAuthenticatedProviders?: (rootDirOverride?: string) => ReadonlySet<string>;
8
+ /** OpenCode client for availability lookup. When omitted, availability falls back to empty set (last-resort resolution). */
9
+ client?: OpencodeClientLike;
9
10
  }
10
11
  export declare function toTitleCase(name: string): string;
11
12
  export declare function formatAgentDescription(name: string, description: string | undefined): string;
@@ -166,7 +166,6 @@ export type ValidationResult = {
166
166
  errors: readonly z.ZodIssue[];
167
167
  };
168
168
  export declare function validateConfig(input: unknown): ValidationResult;
169
- export declare function assertSourceCategoryModelDefaults(defaults: Record<string, unknown>): void;
170
169
  /**
171
170
  * Overlay fields that require a project-or-higher trust source.
172
171
  *
@@ -0,0 +1,77 @@
1
+ interface ConnectedProvider {
2
+ id: string;
3
+ models: Record<string, unknown>;
4
+ }
5
+ interface ProvidersResponse {
6
+ providers: ConnectedProvider[];
7
+ default: Record<string, string>;
8
+ }
9
+ interface ClientConfigApi {
10
+ providers: () => Promise<{
11
+ data: ProvidersResponse;
12
+ error: undefined;
13
+ } | {
14
+ data: undefined;
15
+ error: unknown;
16
+ }>;
17
+ }
18
+ export interface OpencodeClientLike {
19
+ config: ClientConfigApi;
20
+ }
21
+ /**
22
+ * Outcome of model availability discovery.
23
+ *
24
+ * - `api`: The OpenCode server's `/config/providers` endpoint responded with
25
+ * a connected-providers payload. `models` may be empty if no providers
26
+ * are authenticated; that is authoritative.
27
+ * - `cache`: The API call failed (error envelope, thrown, or timed out) and
28
+ * the local `models.json` cache was readable. `models` reflects whatever
29
+ * OpenCode last wrote to disk.
30
+ * - `unknown`: Both the API call and the cache fallback failed (cache
31
+ * missing, unreadable, corrupt, or schema-mismatched). Resolution should
32
+ * degrade gracefully — callers should treat `unknown` as a signal to
33
+ * skip source-default model pinning so users do not get agents pinned
34
+ * to inaccessible models. `models` is the empty set.
35
+ */
36
+ export type DiscoveryStatus = 'api' | 'cache' | 'unknown';
37
+ export interface ModelAvailability {
38
+ status: DiscoveryStatus;
39
+ /**
40
+ * Set of `${providerId}/${modelId}` strings. Typed `ReadonlySet` because
41
+ * callers must not mutate the returned collection — mutation would corrupt
42
+ * future calls in the same process. Each `ModelAvailability` is a fresh
43
+ * instance (see `emptyAvailability()`), so mutation via cast cannot
44
+ * propagate, but the type makes intent explicit.
45
+ */
46
+ models: ReadonlySet<string>;
47
+ }
48
+ interface AvailabilityOptions {
49
+ /**
50
+ * Maximum time to wait for `client.config.providers()` before falling
51
+ * back to the local cache. Defaults to 1500ms — a startup-budget value
52
+ * that prevents a slow/half-open OpenCode server from holding the plugin
53
+ * indefinitely.
54
+ *
55
+ * Set to `null` to disable the timeout entirely (not recommended).
56
+ */
57
+ apiTimeoutMs?: number | null;
58
+ }
59
+ /**
60
+ * Discover the set of `provider/model` keys the OpenCode server considers
61
+ * connected (or, on API failure, the set last written to the on-disk
62
+ * `models.json` cache).
63
+ *
64
+ * The returned `status` lets callers distinguish three discovery outcomes:
65
+ * - `api`: live answer; safe to pin source-default models against it
66
+ * - `cache`: degraded but informed; the cached `provider/model` keys are
67
+ * plausibly still authoritative
68
+ * - `unknown`: both the API and the cache failed; callers should fall back
69
+ * to OpenCode's parent-model inheritance rather than pinning a source
70
+ * default the user may not have access to
71
+ *
72
+ * The API call is bounded by `apiTimeoutMs` (default 1500ms). On timeout,
73
+ * thrown error, error-envelope response, or undefined data, the cache
74
+ * fallback runs. The function never rejects.
75
+ */
76
+ export declare function getAvailableModels(client: OpencodeClientLike, options?: AvailabilityOptions): Promise<ModelAvailability>;
77
+ export {};
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Provider-grouped source category model defaults for Systematic bundled agents.
3
+ *
4
+ * This module owns the canonical shape, Zod schema, and constant for the
5
+ * per-category model resolution chain. The resolution algorithm walks
6
+ * the provider list in order and picks the first available provider/model pair.
7
+ *
8
+ * Provider catalog is constrained to the 7 IDs with empirical OMO usage-frequency
9
+ * justification: vercel=80, opencode=55, github-copilot=39, opencode-go=26,
10
+ * openai=20, anthropic=18, google=10.
11
+ */
12
+ import { z } from 'zod';
13
+ /**
14
+ * Zod literal union of the 7 supported provider IDs.
15
+ * Ordered by OMO empirical usage frequency (highest first).
16
+ */
17
+ export declare const ProviderID: z.ZodUnion<readonly [z.ZodLiteral<"vercel">, z.ZodLiteral<"opencode">, z.ZodLiteral<"github-copilot">, z.ZodLiteral<"opencode-go">, z.ZodLiteral<"openai">, z.ZodLiteral<"anthropic">, z.ZodLiteral<"google">]>;
18
+ export type ProviderID = z.infer<typeof ProviderID>;
19
+ /**
20
+ * Zod schema for the full source category model defaults map.
21
+ *
22
+ * Schema is **pure** — it validates only structural correctness:
23
+ * - Shape correctness (CategoryDefaultSchema per value)
24
+ * - Provider lists non-empty (enforced by CategoryDefaultSchema)
25
+ * - Model lists non-empty (enforced by ProviderEntrySchema)
26
+ * - (model, variant) pairs unique within a provider entry
27
+ * - Provider IDs unique within a category
28
+ * - variant is non-empty, whitespace-free, max 128 chars
29
+ * - `rationale` and `whenToOverride` reject pipe (`|`) and newline characters
30
+ * so the generator produces well-formed Markdown tables
31
+ *
32
+ * Filesystem coverage (every key resolves to a real `agents/<category>/`
33
+ * directory) is NOT checked here. Use `assertCategoryCoverageOnDisk` to
34
+ * enforce that invariant from tests where it matters; the production
35
+ * runtime path uses `assertSourceCategoryModelCoverage` from
36
+ * `agent-overlays.ts` against an in-memory inventory rather than reading
37
+ * disk again.
38
+ */
39
+ export declare const SourceCategoryDefaultsSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
40
+ rationale: z.ZodString;
41
+ whenToOverride: z.ZodOptional<z.ZodString>;
42
+ providers: z.ZodArray<z.ZodObject<{
43
+ provider: z.ZodUnion<readonly [z.ZodLiteral<"vercel">, z.ZodLiteral<"opencode">, z.ZodLiteral<"github-copilot">, z.ZodLiteral<"opencode-go">, z.ZodLiteral<"openai">, z.ZodLiteral<"anthropic">, z.ZodLiteral<"google">]>;
44
+ models: z.ZodArray<z.ZodObject<{
45
+ model: z.ZodString;
46
+ variant: z.ZodOptional<z.ZodString>;
47
+ }, z.core.$strict>>;
48
+ }, z.core.$strict>>;
49
+ }, z.core.$strict>>;
50
+ /**
51
+ * Verify that every category key in `categories` maps to a real
52
+ * `agents/<category>/` directory on disk under `agentsDir` (defaulting to
53
+ * the package's bundled-agents directory). Throws with a useful message if
54
+ * any keys are unrecognized.
55
+ *
56
+ * Use from tests to lock the SOURCE_CATEGORY_MODEL_DEFAULTS ↔ agents/
57
+ * directory layout contract. The production runtime path validates the
58
+ * inverse direction (every bundled-agent category has a source default)
59
+ * via `assertSourceCategoryModelCoverage` in `agent-overlays.ts`.
60
+ */
61
+ export declare function assertCategoryCoverageOnDisk(categories: readonly string[], agentsDir?: string): void;
62
+ export interface ModelEntry {
63
+ model: string;
64
+ variant?: string;
65
+ }
66
+ export interface ProviderEntry {
67
+ provider: ProviderID;
68
+ models: ModelEntry[];
69
+ }
70
+ export interface CategoryDefault {
71
+ rationale: string;
72
+ whenToOverride?: string;
73
+ providers: ProviderEntry[];
74
+ }
75
+ export type SourceCategoryDefaults = Record<string, CategoryDefault>;
76
+ /**
77
+ * Provider-grouped source model defaults for the 6 Systematic agent categories.
78
+ *
79
+ * Provider chains are ordered by OMO category-fit reasoning. The resolver
80
+ * walks providers in order and picks the first available provider/model pair.
81
+ * If no provider is available, the first entry of the first provider is used as
82
+ * the last-resort fallback.
83
+ *
84
+ * Model choices translate the existing flat-string-array constant in agent-overlays.ts
85
+ * to the new provider-grouped shape, with variant annotations where applicable.
86
+ */
87
+ export declare const SOURCE_CATEGORY_MODEL_DEFAULTS: SourceCategoryDefaults;
88
+ /**
89
+ * Format the SOURCE_CATEGORY_MODEL_DEFAULTS as a GitHub-flavored markdown table
90
+ * for injection into documentation.
91
+ *
92
+ * Columns: Category | Chain | Rationale | When to Override
93
+ *
94
+ * Chain format: comma-separated `provider/model[+variant]` for the first 2–3
95
+ * provider entries (first model per provider). Appends `, …` when there are
96
+ * more than 3 provider entries.
97
+ *
98
+ * Returns a string ending with `\n` for clean concatenation.
99
+ */
100
+ export declare function formatForDocs(): string;
101
+ /**
102
+ * Walk the provider-grouped shape for a category and return the first available
103
+ * provider/model pair from the availability set.
104
+ *
105
+ * Algorithm:
106
+ * 1. Look up the category. Unknown category is a programmer error — throw.
107
+ * 2. Walk providers in declared order. For each provider, walk its models in
108
+ * declared order and test `${provider}/${model}` membership in availabilitySet.
109
+ * 3. On first hit, return { provider, model, variant? }.
110
+ * 4. Last-resort fallback (no available model anywhere): return the first model
111
+ * entry of the first provider entry, including its variant if present.
112
+ */
113
+ export declare function resolveSourceModel(category: string, availabilitySet: ReadonlySet<string>): {
114
+ provider: ProviderID;
115
+ model: string;
116
+ variant?: string;
117
+ };
@@ -46,6 +46,7 @@
46
46
  "variant": {
47
47
  "type": "string",
48
48
  "minLength": 1,
49
+ "maxLength": 128,
49
50
  "pattern": "^\\S+$",
50
51
  "description": "Model variant identifier",
51
52
  "examples": ["v2", "extended"],
@@ -161,7 +162,7 @@
161
162
  "description": "Per-agent configuration overlay",
162
163
  "examples": [
163
164
  {
164
- "model": "anthropic/claude-opus-4.7",
165
+ "model": "anthropic/claude-opus-4-7",
165
166
  "temperature": 0.1,
166
167
  "mode": "subagent"
167
168
  }
@@ -174,7 +175,7 @@
174
175
  "examples": [
175
176
  {
176
177
  "review": {
177
- "model": "anthropic/claude-opus-4.7"
178
+ "model": "anthropic/claude-opus-4-7"
178
179
  }
179
180
  },
180
181
  {}
@@ -204,6 +205,7 @@
204
205
  "variant": {
205
206
  "type": "string",
206
207
  "minLength": 1,
208
+ "maxLength": 128,
207
209
  "pattern": "^\\S+$",
208
210
  "description": "Model variant identifier",
209
211
  "examples": ["v2", "extended"],
@@ -314,7 +316,7 @@
314
316
  "description": "Per-category configuration overlay (same fields as agent minus disable)",
315
317
  "examples": [
316
318
  {
317
- "model": "anthropic/claude-opus-4.7",
319
+ "model": "anthropic/claude-opus-4-7",
318
320
  "temperature": 0.1
319
321
  }
320
322
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fro.bot/systematic",
3
- "version": "2.12.2",
3
+ "version": "2.13.0",
4
4
  "description": "Structured engineering workflows for OpenCode",
5
5
  "type": "module",
6
6
  "homepage": "https://fro.bot/systematic",
@@ -69,8 +69,8 @@
69
69
  "@types/bun": "latest",
70
70
  "@types/js-yaml": "4.0.9",
71
71
  "@types/node": "24.12.3",
72
- "ajv": "^8.20.0",
73
- "ajv-formats": "^3.0.1",
72
+ "ajv": "8.20.0",
73
+ "ajv-formats": "3.0.1",
74
74
  "conventional-changelog-conventionalcommits": "9.3.1",
75
75
  "markdownlint-cli": "0.48.0",
76
76
  "rimraf": "6.1.3",