@fro.bot/systematic 2.10.0 → 2.11.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 +2 -0
- package/dist/index.js +70 -15
- package/dist/lib/agent-overlays.d.ts +22 -1
- package/dist/lib/config-handler.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -338,6 +338,8 @@ 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.
|
|
342
|
+
|
|
341
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.
|
|
342
344
|
|
|
343
345
|
The source defaults are:
|
package/dist/index.js
CHANGED
|
@@ -78,14 +78,15 @@ ${toolMapping}
|
|
|
78
78
|
|
|
79
79
|
// src/lib/agent-overlays.ts
|
|
80
80
|
import fs2 from "fs";
|
|
81
|
+
import os2 from "os";
|
|
81
82
|
import path2 from "path";
|
|
82
83
|
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"
|
|
84
|
+
design: ["openai/gpt-5.5", "anthropic/claude-opus-4.7"],
|
|
85
|
+
docs: ["openai/gpt-5.4-mini", "anthropic/claude-haiku-4-5"],
|
|
86
|
+
"document-review": ["anthropic/claude-opus-4.7", "openai/gpt-5.5"],
|
|
87
|
+
research: ["openai/gpt-5.5", "anthropic/claude-opus-4.7"],
|
|
88
|
+
review: ["anthropic/claude-opus-4.7", "openai/gpt-5.5"],
|
|
89
|
+
workflow: ["openai/gpt-5.4-mini", "anthropic/claude-haiku-4-5"]
|
|
89
90
|
};
|
|
90
91
|
var ALLOWED_OVERLAY_FIELDS = new Set([
|
|
91
92
|
"model",
|
|
@@ -166,10 +167,51 @@ function inferBuiltInTemperature(name, description) {
|
|
|
166
167
|
}
|
|
167
168
|
return 0.3;
|
|
168
169
|
}
|
|
169
|
-
function
|
|
170
|
+
function getAuthenticatedProviders(rootDirOverride) {
|
|
171
|
+
const xdgDataHome = process.env.XDG_DATA_HOME?.trim();
|
|
172
|
+
const rootDir = rootDirOverride || (xdgDataHome && path2.isAbsolute(xdgDataHome) ? xdgDataHome : path2.join(os2.homedir(), ".local/share"));
|
|
173
|
+
const authPath = path2.join(rootDir, "opencode", "auth.json");
|
|
174
|
+
let raw;
|
|
175
|
+
try {
|
|
176
|
+
raw = fs2.readFileSync(authPath, "utf8");
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (isSystemError(err) && err.code === "ENOENT") {
|
|
179
|
+
return new Set;
|
|
180
|
+
}
|
|
181
|
+
console.warn(`[systematic] auth.json unreadable at ${authPath}; ignoring`);
|
|
182
|
+
return new Set;
|
|
183
|
+
}
|
|
184
|
+
let parsed;
|
|
185
|
+
try {
|
|
186
|
+
parsed = JSON.parse(raw);
|
|
187
|
+
} catch {
|
|
188
|
+
console.warn(`[systematic] auth.json malformed at ${authPath}; ignoring`);
|
|
189
|
+
return new Set;
|
|
190
|
+
}
|
|
191
|
+
if (!isRecord(parsed)) {
|
|
192
|
+
console.warn(`[systematic] auth.json malformed at ${authPath}; ignoring`);
|
|
193
|
+
return new Set;
|
|
194
|
+
}
|
|
195
|
+
return new Set(Object.keys(parsed));
|
|
196
|
+
}
|
|
197
|
+
function getSourceCategoryModel(category, authedProviders) {
|
|
170
198
|
if (!category)
|
|
171
199
|
return;
|
|
172
|
-
|
|
200
|
+
const candidates = SOURCE_CATEGORY_MODEL_DEFAULTS[category];
|
|
201
|
+
if (!candidates || candidates.length === 0)
|
|
202
|
+
return;
|
|
203
|
+
if (!authedProviders || authedProviders.size === 0)
|
|
204
|
+
return candidates[0];
|
|
205
|
+
for (const entry of candidates) {
|
|
206
|
+
const slashIndex = entry.indexOf("/");
|
|
207
|
+
if (slashIndex <= 0)
|
|
208
|
+
continue;
|
|
209
|
+
const providerId = entry.slice(0, slashIndex);
|
|
210
|
+
if (authedProviders.has(providerId)) {
|
|
211
|
+
return entry;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return candidates[0];
|
|
173
215
|
}
|
|
174
216
|
function assertSourceCategoryModelCoverage(categories) {
|
|
175
217
|
validateSourceCategoryModelDefaults();
|
|
@@ -179,8 +221,16 @@ function assertSourceCategoryModelCoverage(categories) {
|
|
|
179
221
|
}
|
|
180
222
|
}
|
|
181
223
|
function validateSourceCategoryModelDefaults(defaults = SOURCE_CATEGORY_MODEL_DEFAULTS) {
|
|
182
|
-
for (const [category,
|
|
183
|
-
|
|
224
|
+
for (const [category, value] of Object.entries(defaults)) {
|
|
225
|
+
if (!Array.isArray(value)) {
|
|
226
|
+
throw new Error(`Source category model defaults: ${category} must be a non-empty array of provider/model strings`);
|
|
227
|
+
}
|
|
228
|
+
if (value.length === 0) {
|
|
229
|
+
throw new Error(`Source category model defaults: ${category} must be a non-empty array of provider/model strings`);
|
|
230
|
+
}
|
|
231
|
+
for (const [index, model] of value.entries()) {
|
|
232
|
+
validateModel("source category model defaults", `source category model defaults.${category}[${index}]`, model);
|
|
233
|
+
}
|
|
184
234
|
}
|
|
185
235
|
}
|
|
186
236
|
function validateExactAgentOverlays(inventory, overlays, nativeAgents, enabledSkills) {
|
|
@@ -398,6 +448,9 @@ function validAgentKeys(inventory) {
|
|
|
398
448
|
function throwConfigError(sourcePath, keyPath, message) {
|
|
399
449
|
throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath} ${message}`);
|
|
400
450
|
}
|
|
451
|
+
function isSystemError(err) {
|
|
452
|
+
return typeof err === "object" && err !== null && "code" in err && typeof err.code === "string";
|
|
453
|
+
}
|
|
401
454
|
|
|
402
455
|
// src/lib/skill-loader.ts
|
|
403
456
|
import path3 from "path";
|
|
@@ -567,7 +620,7 @@ function loadSkillAsCommand(loaded) {
|
|
|
567
620
|
config.subtask = loaded.subtask;
|
|
568
621
|
return config;
|
|
569
622
|
}
|
|
570
|
-
function collectAgents(dir, disabledAgents, nativeAgents, overlays) {
|
|
623
|
+
function collectAgents(dir, disabledAgents, nativeAgents, overlays, authedProviders) {
|
|
571
624
|
const agents = {};
|
|
572
625
|
const agentList = findAgentsInDir(dir);
|
|
573
626
|
const disabledSet = new Set(disabledAgents);
|
|
@@ -582,12 +635,12 @@ function collectAgents(dir, disabledAgents, nativeAgents, overlays) {
|
|
|
582
635
|
continue;
|
|
583
636
|
const config = loadAgentAsConfig(agentInfo);
|
|
584
637
|
if (config) {
|
|
585
|
-
agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays);
|
|
638
|
+
agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays, authedProviders);
|
|
586
639
|
}
|
|
587
640
|
}
|
|
588
641
|
return agents;
|
|
589
642
|
}
|
|
590
|
-
function applyAgentOverlays(config, agentInfo, overlays) {
|
|
643
|
+
function applyAgentOverlays(config, agentInfo, overlays, authedProviders) {
|
|
591
644
|
const id = agentInfo.category ? `${agentInfo.category}/${agentInfo.name}` : agentInfo.name;
|
|
592
645
|
const categoryOverlay = agentInfo.category ? overlays.categoriesByKey.get(agentInfo.category) : undefined;
|
|
593
646
|
const exactOverlay = overlays.agentsByTargetId.get(id);
|
|
@@ -599,7 +652,7 @@ function applyAgentOverlays(config, agentInfo, overlays) {
|
|
|
599
652
|
}
|
|
600
653
|
result.temperature = inferBuiltInTemperature(agentInfo.name, result.description);
|
|
601
654
|
if (agentInfo.category) {
|
|
602
|
-
const sourceModel = getSourceCategoryModel(agentInfo.category);
|
|
655
|
+
const sourceModel = getSourceCategoryModel(agentInfo.category, authedProviders);
|
|
603
656
|
if (sourceModel) {
|
|
604
657
|
result.model = sourceModel;
|
|
605
658
|
}
|
|
@@ -729,6 +782,7 @@ function collectEnabledSkillNames(dir, disabledSkills) {
|
|
|
729
782
|
}
|
|
730
783
|
function createConfigHandler(deps) {
|
|
731
784
|
const { directory, bundledSkillsDir, bundledAgentsDir, bundledCommandsDir } = deps;
|
|
785
|
+
const readAuthProviders = deps.getAuthenticatedProviders ?? getAuthenticatedProviders;
|
|
732
786
|
return async (config) => {
|
|
733
787
|
const { config: systematicConfig, overlays } = loadConfigWithSources(directory);
|
|
734
788
|
const existingAgents = { ...config.agent ?? {} };
|
|
@@ -744,7 +798,8 @@ function createConfigHandler(deps) {
|
|
|
744
798
|
enabledSkills: enabledSkillNames
|
|
745
799
|
});
|
|
746
800
|
const resolvedOverlays = resolveAgentOverlaySet(validatedOverlays);
|
|
747
|
-
const
|
|
801
|
+
const authedProviders = readAuthProviders();
|
|
802
|
+
const bundledAgents = collectAgents(bundledAgentsDir, systematicConfig.disabled_agents, existingAgents, resolvedOverlays, authedProviders);
|
|
748
803
|
const bundledCommands = collectCommands(bundledCommandsDir, systematicConfig.disabled_commands);
|
|
749
804
|
config.agent = {
|
|
750
805
|
...bundledAgents,
|
|
@@ -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
|
-
|
|
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;
|