@fro.bot/systematic 3.9.1 → 3.10.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/HARNESSES.md CHANGED
@@ -20,7 +20,7 @@ The four capability rows use the vocabulary in the [OpenCode profile](skills/usi
20
20
 
21
21
  ## OpenCode — Tier 1 shipped adapter
22
22
 
23
- OpenCode is the primary controlled integration. Its profile defines delegation, blocking interaction, task tracking, and skill loading [OC-P]. The underlying source registers `task` with specialized-agent, resume, and background fields [OC-1], `question` as a user-facing tool [OC-2], and `todowrite` as a built-in tool [OC-3]. Systematic converts bundled skills into commands in `src/lib/config-handler.ts:467-485` and registers the skill tool in `src/lib/skill-tool.ts:41-101` [OC-4][OC-5]. Bundled agent markdown omits a model field by invariant [OC-6]; runtime configuration may still supply source-owned defaults [OC-7].
23
+ OpenCode is the primary controlled integration. Its profile defines delegation, blocking interaction, task tracking, and skill loading [OC-P]. The underlying source registers `task` with specialized-agent, resume, and background fields [OC-1], `question` as a user-facing tool [OC-2], and `todowrite` as a built-in tool [OC-3]. Systematic converts bundled skills into commands in `src/lib/config-handler.ts:467-485` and registers the skill tool in `src/lib/skill-tool.ts:41-101` [OC-4][OC-5]. Bundled agent markdown omits a model field by invariant [OC-6]; runtime configuration stays model-free unless user-owned config supplies an explicit overlay [OC-7].
24
24
 
25
25
  Compatibility is direct: OpenCode consumes the concrete invocation language in the profile. The adapter is not a generic claim about other harnesses.
26
26
 
package/dist/index.js CHANGED
@@ -17,10 +17,10 @@ import {
17
17
  } from "./index-y8nc60tz.js";
18
18
 
19
19
  // src/index.ts
20
- import { createHash as createHash5 } from "crypto";
21
- import fs11 from "fs";
22
- import path10 from "path";
23
- import { fileURLToPath as fileURLToPath3, pathToFileURL as pathToFileURL2 } from "url";
20
+ import { createHash as createHash4 } from "crypto";
21
+ import fs10 from "fs";
22
+ import path8 from "path";
23
+ import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "url";
24
24
 
25
25
  // src/lib/bootstrap.ts
26
26
  import fs2 from "fs";
@@ -136,217 +136,27 @@ ${skillUsage}${profileSection}
136
136
  }
137
137
 
138
138
  // src/lib/config-handler.ts
139
- import fs6 from "fs";
140
- import os3 from "os";
141
- import path6 from "path";
139
+ import fs5 from "fs";
140
+ import os2 from "os";
141
+ import path4 from "path";
142
142
 
143
143
  // src/lib/agent-overlays.ts
144
144
  import fs3 from "fs";
145
- import path3 from "path";
146
-
147
- // src/lib/source-model-defaults.ts
148
- import * as path2 from "path";
149
- import { fileURLToPath } from "url";
150
- var __filename2 = fileURLToPath(import.meta.url);
151
- var __dirname2 = path2.dirname(__filename2);
152
- var packageRoot = path2.resolve(__dirname2, "..", "..");
153
- var bundledAgentsDir = path2.join(packageRoot, "agents");
154
- var ProviderID = exports_external.union([
155
- exports_external.literal("vercel"),
156
- exports_external.literal("opencode"),
157
- exports_external.literal("github-copilot"),
158
- exports_external.literal("opencode-go"),
159
- exports_external.literal("openai"),
160
- exports_external.literal("anthropic"),
161
- exports_external.literal("google")
162
- ]);
163
- 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");
164
- 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");
165
- var ModelEntrySchema = exports_external.object({
166
- model: exports_external.string().min(1, "model must be a non-empty string"),
167
- variant: variantSchema.optional()
168
- }).strict();
169
- var ProviderEntrySchema = exports_external.object({
170
- provider: ProviderID,
171
- models: exports_external.array(ModelEntrySchema).min(1, "models must be non-empty \u2014 every provider entry must list at least one model")
172
- }).strict().refine((entry) => {
173
- const seen = new Set;
174
- for (const m of entry.models) {
175
- const key = `${m.model}::${m.variant ?? ""}`;
176
- if (seen.has(key))
177
- return false;
178
- seen.add(key);
179
- }
180
- return true;
181
- }, {
182
- message: "duplicate (model, variant) pair within a provider entry \u2014 each model+variant combination must be unique"
183
- });
184
- var CategoryDefaultSchema = exports_external.object({
185
- rationale: tableCellSchema,
186
- whenToOverride: tableCellSchema.optional(),
187
- providers: exports_external.array(ProviderEntrySchema).min(1, "providers must be non-empty \u2014 every category must list at least one provider")
188
- }).strict().refine((cat) => {
189
- const seen = new Set;
190
- for (const p of cat.providers) {
191
- if (seen.has(p.provider))
192
- return false;
193
- seen.add(p.provider);
194
- }
195
- return true;
196
- }, {
197
- message: "duplicate provider ID within a category \u2014 each provider must appear at most once per category"
198
- });
199
- var SourceCategoryDefaultsSchema = exports_external.record(exports_external.string(), CategoryDefaultSchema);
200
- var SOURCE_CATEGORY_MODEL_DEFAULTS = {
201
- design: {
202
- rationale: "High-judgment UX, product, and design work benefits from a strong general reasoning model with broad creative capability.",
203
- whenToOverride: "Override to a faster/cheaper model when design tasks are primarily templating or low-stakes layout work.",
204
- providers: [
205
- {
206
- provider: "github-copilot",
207
- models: [{ model: "gemini-3.1-pro-preview" }]
208
- },
209
- {
210
- provider: "openai",
211
- models: [{ model: "gpt-5.5", variant: "high" }]
212
- },
213
- {
214
- provider: "anthropic",
215
- models: [{ model: "claude-opus-4-7", variant: "max" }]
216
- },
217
- {
218
- provider: "vercel",
219
- models: [{ model: "v0-1.5-md" }]
220
- }
221
- ]
222
- },
223
- "document-review": {
224
- rationale: "Requirements and plan critique benefit from the strongest nuanced reasoning to surface non-obvious gaps and contradictions.",
225
- providers: [
226
- {
227
- provider: "anthropic",
228
- models: [{ model: "claude-opus-4-7", variant: "max" }]
229
- },
230
- {
231
- provider: "openai",
232
- models: [{ model: "gpt-5.5", variant: "high" }]
233
- },
234
- {
235
- provider: "github-copilot",
236
- models: [{ model: "gemini-3.1-pro-preview" }]
237
- },
238
- {
239
- provider: "opencode",
240
- models: [{ model: "claude-sonnet-4-6" }]
241
- }
242
- ]
243
- },
244
- research: {
245
- rationale: "Tool-heavy synthesis and source evaluation benefit from a strong general reasoning model with broad knowledge.",
246
- providers: [
247
- {
248
- provider: "openai",
249
- models: [{ model: "gpt-5.4-mini" }]
250
- },
251
- {
252
- provider: "anthropic",
253
- models: [{ model: "claude-sonnet-4-6" }]
254
- },
255
- {
256
- provider: "github-copilot",
257
- models: [{ model: "gemini-3.1-pro-preview" }]
258
- },
259
- {
260
- provider: "opencode",
261
- models: [{ model: "claude-sonnet-4-6" }]
262
- }
263
- ]
264
- },
265
- review: {
266
- rationale: "Code, security, and adversarial review benefits from the strongest reasoning to catch subtle bugs and security issues.",
267
- whenToOverride: "Override to a faster model when review tasks are primarily style or formatting checks rather than correctness or security analysis.",
268
- providers: [
269
- {
270
- provider: "anthropic",
271
- models: [{ model: "claude-sonnet-4-6" }]
272
- },
273
- {
274
- provider: "openai",
275
- models: [{ model: "gpt-5.3-codex" }]
276
- },
277
- {
278
- provider: "github-copilot",
279
- models: [{ model: "gemini-3.1-pro-preview" }]
280
- },
281
- {
282
- provider: "opencode",
283
- models: [{ model: "claude-sonnet-4-6" }]
284
- }
285
- ]
286
- },
287
- workflow: {
288
- rationale: "Orchestration and bounded implementation tasks should default cheaper and faster; strong reasoning is rarely needed for routing decisions.",
289
- providers: [
290
- {
291
- provider: "openai",
292
- models: [{ model: "gpt-5.4-mini" }]
293
- },
294
- {
295
- provider: "anthropic",
296
- models: [{ model: "claude-sonnet-4-6" }]
297
- },
298
- {
299
- provider: "opencode",
300
- models: [{ model: "claude-haiku-4-5" }]
301
- },
302
- {
303
- provider: "opencode-go",
304
- models: [{ model: "claude-haiku-4-5" }]
305
- }
306
- ]
307
- }
308
- };
309
- function resolveSourceModel(category, availabilitySet) {
310
- const categoryDefault = SOURCE_CATEGORY_MODEL_DEFAULTS[category];
311
- if (!categoryDefault) {
312
- throw new Error(`resolveSourceModel: unknown category "${category}". Valid categories: ${Object.keys(SOURCE_CATEGORY_MODEL_DEFAULTS).join(", ")}`);
313
- }
314
- for (const providerEntry of categoryDefault.providers) {
315
- for (const modelEntry of providerEntry.models) {
316
- const key = `${providerEntry.provider}/${modelEntry.model}`;
317
- if (availabilitySet.has(key)) {
318
- return {
319
- provider: providerEntry.provider,
320
- model: modelEntry.model,
321
- variant: modelEntry.variant
322
- };
323
- }
324
- }
325
- }
326
- const firstProvider = categoryDefault.providers[0];
327
- const firstModel = firstProvider.models[0];
328
- return {
329
- provider: firstProvider.provider,
330
- model: firstModel.model,
331
- variant: firstModel.variant
332
- };
333
- }
334
-
335
- // src/lib/agent-overlays.ts
145
+ import path2 from "path";
336
146
  function buildBundledAgentInventory(agentsDir, disabledAgents) {
337
147
  const categories = readCategoryDirs(agentsDir);
338
148
  const agentsByQualifiedId = {};
339
149
  const stemCategories = new Map;
340
150
  const disabledSet = new Set(disabledAgents);
341
151
  for (const category of categories) {
342
- for (const fileName of readMarkdownFiles(path3.join(agentsDir, category))) {
152
+ for (const fileName of readMarkdownFiles(path2.join(agentsDir, category))) {
343
153
  const key = fileName.replace(/\.md$/, "");
344
154
  const id = `${category}/${key}`;
345
155
  agentsByQualifiedId[id] = {
346
156
  id,
347
157
  key,
348
158
  category,
349
- file: path3.join(agentsDir, category, fileName),
159
+ file: path2.join(agentsDir, category, fileName),
350
160
  disabled: disabledSet.has(key) || disabledSet.has(id)
351
161
  };
352
162
  const existing = stemCategories.get(key) ?? [];
@@ -383,12 +193,6 @@ function resolveAgentOverlaySet(overlays) {
383
193
  categoriesByKey: new Map(overlays.categories.map((overlay) => [overlay.key, overlay]))
384
194
  };
385
195
  }
386
- function assertSourceCategoryModelCoverage(categories) {
387
- const missingCategories = categories.filter((category) => !Object.hasOwn(SOURCE_CATEGORY_MODEL_DEFAULTS, category));
388
- if (missingCategories.length > 0) {
389
- throw new Error(`Source category model defaults missing intentional coverage for: ${missingCategories.join(", ")}`);
390
- }
391
- }
392
196
  function validateExactAgentOverlays(inventory, overlays, nativeAgents, enabledSkills) {
393
197
  const result = [];
394
198
  const seenTargets = new Map;
@@ -490,174 +294,9 @@ function throwConfigError(sourcePath, keyPath, message) {
490
294
  throw new Error(`Invalid Systematic config in ${sourcePath}: ${keyPath} ${message}`);
491
295
  }
492
296
 
493
- // src/lib/model-availability.ts
494
- import { createHash } from "crypto";
495
- import fs4 from "fs";
496
- import os2 from "os";
497
- import path4 from "path";
498
- function emptyAvailability() {
499
- return { status: "unknown", models: new Set };
500
- }
501
- var MAX_CACHE_FILE_BYTES = 16 * 1024 * 1024;
502
- var DEFAULT_API_TIMEOUT_MS = 1500;
503
- var MODELS_JSON_FILENAME = "models.json";
504
- var availabilityCache = new WeakMap;
505
- function resolveCacheDir() {
506
- const xdgCacheHome = process.env.XDG_CACHE_HOME?.trim();
507
- const cacheBase = xdgCacheHome && path4.isAbsolute(xdgCacheHome) ? xdgCacheHome : path4.join(os2.homedir(), ".cache");
508
- return path4.join(cacheBase, "opencode");
509
- }
510
- function fastHash(input) {
511
- return createHash("sha1").update(input).digest("hex");
512
- }
513
- function isProviderRecord(value) {
514
- if (!isRecord(value))
515
- return false;
516
- for (const entry of Object.values(value)) {
517
- if (!isRecord(entry))
518
- return false;
519
- if (!isRecord(entry.models))
520
- return false;
521
- }
522
- return true;
523
- }
524
- function readModelsFromCache(filePath) {
525
- let fd;
526
- try {
527
- fd = fs4.openSync(filePath, "r");
528
- } catch {
529
- return null;
530
- }
531
- let raw;
532
- try {
533
- let stat;
534
- try {
535
- stat = fs4.fstatSync(fd);
536
- } catch {
537
- return null;
538
- }
539
- if (!stat.isFile())
540
- return null;
541
- if (stat.size === 0)
542
- return null;
543
- if (stat.size > MAX_CACHE_FILE_BYTES) {
544
- console.warn(`[systematic] models.json at ${filePath} is ${stat.size} bytes (>${MAX_CACHE_FILE_BYTES}); treating as cache miss.`);
545
- return null;
546
- }
547
- const buffer = Buffer.alloc(stat.size);
548
- let bytesRead;
549
- try {
550
- bytesRead = fs4.readSync(fd, buffer, 0, stat.size, 0);
551
- } catch {
552
- return null;
553
- }
554
- if (bytesRead !== stat.size)
555
- return null;
556
- raw = buffer.toString("utf8");
557
- } finally {
558
- try {
559
- fs4.closeSync(fd);
560
- } catch {}
561
- }
562
- if (raw.trim().length === 0)
563
- return null;
564
- let parsed;
565
- try {
566
- parsed = JSON.parse(raw);
567
- } catch {
568
- return null;
569
- }
570
- if (!isProviderRecord(parsed)) {
571
- console.warn(`[systematic] models.json schema mismatch at ${filePath}; treating as cache miss. Upstream shape may have drifted.`);
572
- return null;
573
- }
574
- const result = new Set;
575
- for (const [providerId, providerData] of Object.entries(parsed)) {
576
- for (const modelId of Object.keys(providerData.models)) {
577
- result.add(`${providerId}/${modelId}`);
578
- }
579
- }
580
- return result;
581
- }
582
- function readFallbackCache() {
583
- const cacheDir = resolveCacheDir();
584
- const openCodeModelsUrl = process.env.OPENCODE_MODELS_URL?.trim();
585
- if (openCodeModelsUrl) {
586
- const urlDerivedPath = path4.join(cacheDir, `models-${fastHash(openCodeModelsUrl)}.json`);
587
- const urlResult = readModelsFromCache(urlDerivedPath);
588
- if (urlResult !== null && urlResult.size > 0) {
589
- return { status: "cache", models: urlResult };
590
- }
591
- return emptyAvailability();
592
- }
593
- const defaultPath = path4.join(cacheDir, MODELS_JSON_FILENAME);
594
- const defaultResult = readModelsFromCache(defaultPath);
595
- if (defaultResult !== null && defaultResult.size > 0) {
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 cached = availabilityCache.get(client);
611
- if (cached !== undefined) {
612
- return cached;
613
- }
614
- const cacheAndReturn = (envelope) => {
615
- if (envelope.status !== "unknown") {
616
- availabilityCache.set(client, envelope);
617
- }
618
- return envelope;
619
- };
620
- const timeoutMs = options.apiTimeoutMs === undefined ? DEFAULT_API_TIMEOUT_MS : options.apiTimeoutMs;
621
- if (typeof client.config?.providers !== "function") {
622
- return cacheAndReturn(readFallbackCache());
623
- }
624
- const apiCall = client.config.providers();
625
- let response;
626
- try {
627
- if (timeoutMs === null) {
628
- response = await apiCall;
629
- } else {
630
- const TIMEOUT_SENTINEL = Symbol("timeout");
631
- const timeoutPromise = new Promise((resolve2) => {
632
- const timer = setTimeout(() => resolve2(TIMEOUT_SENTINEL), timeoutMs);
633
- timer.unref?.();
634
- });
635
- const raced = await Promise.race([apiCall, timeoutPromise]);
636
- if (raced === TIMEOUT_SENTINEL) {
637
- console.warn(`[systematic] client.config.providers() exceeded ${timeoutMs}ms; falling back to models.json cache.`);
638
- return cacheAndReturn(readFallbackCache());
639
- }
640
- response = raced;
641
- }
642
- } catch {
643
- return cacheAndReturn(readFallbackCache());
644
- }
645
- if (response.error !== undefined || response.data === undefined) {
646
- return cacheAndReturn(readFallbackCache());
647
- }
648
- const models = buildSetFromProviders(response.data.providers);
649
- if (models.size === 0) {
650
- return cacheAndReturn(emptyAvailability());
651
- }
652
- return cacheAndReturn({
653
- status: "api",
654
- models
655
- });
656
- }
657
-
658
297
  // src/lib/skill-loader.ts
659
- import fs5 from "fs";
660
- import path5 from "path";
298
+ import fs4 from "fs";
299
+ import path3 from "path";
661
300
  var SKILL_PREFIX = "systematic:";
662
301
  var SKILL_DESCRIPTION_PREFIX = "(Systematic) ";
663
302
  function formatSkillCommandName(name2) {
@@ -674,7 +313,7 @@ function formatSkillDescription(description, fallbackName) {
674
313
  return `${SKILL_DESCRIPTION_PREFIX}${desc}`;
675
314
  }
676
315
  function wrapSkillTemplate(skillPath, body2) {
677
- const skillDir = path5.dirname(skillPath);
316
+ const skillDir = path3.dirname(skillPath);
678
317
  return `<skill-instruction>
679
318
  Base directory for this skill: ${skillDir}/
680
319
  File references (@path) in this skill are relative to this directory.
@@ -692,7 +331,7 @@ function extractSkillBody(wrappedTemplate) {
692
331
  }
693
332
  function loadSkill(skillInfo) {
694
333
  try {
695
- const content = fs5.readFileSync(skillInfo.skillFile, "utf8");
334
+ const content = fs4.readFileSync(skillInfo.skillFile, "utf8");
696
335
  const { body: body2 } = parseFrontmatter(content);
697
336
  const wrappedTemplate = wrapSkillTemplate(skillInfo.skillFile, body2);
698
337
  return {
@@ -750,7 +389,7 @@ function formatAgentDescription(name2, description) {
750
389
  }
751
390
  function loadAgentAsConfig(agentInfo) {
752
391
  try {
753
- const content = fs6.readFileSync(agentInfo.file, "utf8");
392
+ const content = fs5.readFileSync(agentInfo.file, "utf8");
754
393
  const {
755
394
  description,
756
395
  prompt,
@@ -799,7 +438,7 @@ function loadAgentAsConfig(agentInfo) {
799
438
  }
800
439
  function loadCommandAsConfig(commandInfo) {
801
440
  try {
802
- const content = fs6.readFileSync(commandInfo.file, "utf8");
441
+ const content = fs5.readFileSync(commandInfo.file, "utf8");
803
442
  const { name: name2, description, agent, model, subtask } = extractCommandFrontmatter(content);
804
443
  const { body: body2 } = parseFrontmatter(content);
805
444
  const cleanName = commandInfo.name.replace(/^\//, "");
@@ -839,7 +478,7 @@ function loadSkillAsCommand(loaded) {
839
478
  config.subtask = loaded.subtask;
840
479
  return config;
841
480
  }
842
- function collectAgents(dir, disabledAgents, nativeAgents, overlays, availabilitySet) {
481
+ function collectAgents(dir, disabledAgents, nativeAgents, overlays) {
843
482
  const agents = {};
844
483
  const agentList = findAgentsInDir(dir);
845
484
  const disabledSet = new Set(disabledAgents);
@@ -854,12 +493,12 @@ function collectAgents(dir, disabledAgents, nativeAgents, overlays, availability
854
493
  continue;
855
494
  const config = loadAgentAsConfig(agentInfo);
856
495
  if (config) {
857
- agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays, availabilitySet);
496
+ agents[agentInfo.name] = applyAgentOverlays(config, agentInfo, overlays);
858
497
  }
859
498
  }
860
499
  return agents;
861
500
  }
862
- function applyAgentOverlays(config, agentInfo, overlays, availabilitySet) {
501
+ function applyAgentOverlays(config, agentInfo, overlays) {
863
502
  const id = agentInfo.category ? `${agentInfo.category}/${agentInfo.name}` : agentInfo.name;
864
503
  const categoryOverlay = agentInfo.category ? overlays.categoriesByKey.get(agentInfo.category) : undefined;
865
504
  const exactOverlay = overlays.agentsByTargetId.get(id);
@@ -869,23 +508,11 @@ function applyAgentOverlays(config, agentInfo, overlays, availabilitySet) {
869
508
  if (hasPermissionOverlay && isRecord(config.permission)) {
870
509
  addPermissionRules(permissionRules, config.permission);
871
510
  }
872
- applySourceModelDefault(result, agentInfo, availabilitySet);
873
511
  applyAgentOverlay(result, categoryOverlay?.value, permissionRules);
874
512
  applyAgentOverlay(result, exactOverlay?.value, permissionRules);
875
513
  applyPermissionOverlay(result, permissionRules, hasPermissionOverlay);
876
514
  return result;
877
515
  }
878
- function applySourceModelDefault(target, agentInfo, availabilitySet) {
879
- if (!agentInfo.category || availabilitySet === undefined)
880
- return;
881
- const resolved = resolveSourceModel(agentInfo.category, availabilitySet);
882
- target.model = `${resolved.provider}/${resolved.model}`;
883
- if (resolved.variant !== undefined) {
884
- target.variant = resolved.variant;
885
- } else {
886
- delete target.variant;
887
- }
888
- }
889
516
  function applyAgentOverlay(target, overlay, permissionRules) {
890
517
  if (overlay === undefined)
891
518
  return;
@@ -1058,9 +685,9 @@ function collectEnabledSkillNames(dir, disabledSkills) {
1058
685
  return findSkillsInDir(dir).filter((skillInfo) => !disabledSet.has(skillInfo.name)).map((skillInfo) => skillInfo.name);
1059
686
  }
1060
687
  function createConfigHandler(deps) {
1061
- const { directory, bundledSkillsDir, bundledAgentsDir: bundledAgentsDir2, bundledCommandsDir } = deps;
1062
- const homeDir = deps.homeDir ?? os3.homedir();
1063
- const opencodeConfigDir = deps.opencodeConfigDir ?? path6.join(homeDir, ".config/opencode");
688
+ const { directory, bundledSkillsDir, bundledAgentsDir, bundledCommandsDir } = deps;
689
+ const homeDir = deps.homeDir ?? os2.homedir();
690
+ const opencodeConfigDir = deps.opencodeConfigDir ?? path4.join(homeDir, ".config/opencode");
1064
691
  const opencodeConfigDirOverride = process.env.OPENCODE_CONFIG_DIR?.trim() ? process.env.OPENCODE_CONFIG_DIR : undefined;
1065
692
  return async (config) => {
1066
693
  const { config: systematicConfig, overlays } = loadConfigWithSources(directory);
@@ -1069,10 +696,7 @@ function createConfigHandler(deps) {
1069
696
  const nativeAgents = Object.fromEntries(Object.entries(existingAgents).filter(([, agent]) => !isSystematicAgentConfig(agent)));
1070
697
  const bundledSkills = collectSkillsAsCommands(bundledSkillsDir, systematicConfig.disabled_skills);
1071
698
  const enabledSkillNames = collectEnabledSkillNames(bundledSkillsDir, systematicConfig.disabled_skills);
1072
- const inventory = buildBundledAgentInventory(bundledAgentsDir2, systematicConfig.disabled_agents);
1073
- const availability = deps.client ? await getAvailableModels(deps.client) : undefined;
1074
- const availabilitySet = availability && availability.status !== "unknown" ? availability.models : undefined;
1075
- assertSourceCategoryModelCoverage(inventory.categories);
699
+ const inventory = buildBundledAgentInventory(bundledAgentsDir, systematicConfig.disabled_agents);
1076
700
  const validatedOverlays = validateAgentOverlays({
1077
701
  inventory,
1078
702
  overlays,
@@ -1080,7 +704,7 @@ function createConfigHandler(deps) {
1080
704
  enabledSkills: enabledSkillNames
1081
705
  });
1082
706
  const resolvedOverlays = resolveAgentOverlaySet(validatedOverlays);
1083
- const bundledAgents = collectAgents(bundledAgentsDir2, systematicConfig.disabled_agents, nativeAgents, resolvedOverlays, availabilitySet);
707
+ const bundledAgents = collectAgents(bundledAgentsDir, systematicConfig.disabled_agents, nativeAgents, resolvedOverlays);
1084
708
  const bundledCommands = collectCommands(bundledCommandsDir, systematicConfig.disabled_commands);
1085
709
  const discoveredSkillCommands = systematicConfig.skills_as_commands !== false ? collectDiscoveredSkillsAsCommands(directory, homeDir, opencodeConfigDir, opencodeConfigDirOverride, systematicConfig.disabled_commands) : {};
1086
710
  const bundledAgentKeys = new Set(Object.keys(bundledAgents));
@@ -1106,21 +730,21 @@ function registerSkillsPaths(config, skillsDir) {
1106
730
  };
1107
731
  }
1108
732
  function removeSystematicSkillPaths(paths) {
1109
- return paths.filter((path7) => !isSystematicSkillPath(path7));
733
+ return paths.filter((path5) => !isSystematicSkillPath(path5));
1110
734
  }
1111
- function isSystematicSkillPath(path7) {
1112
- const normalizedPath = normalizePath(path7);
735
+ function isSystematicSkillPath(path5) {
736
+ const normalizedPath = normalizePath(path5);
1113
737
  return normalizedPath.endsWith("/.config/opencode/systematic/skills") || normalizedPath.endsWith("/.cache/opencode/systematic/skills") || normalizedPath.endsWith("/.local/share/opencode/systematic/skills") || normalizedPath.endsWith("/.opencode/systematic/skills") || /(?:^|\/)\.cache\/opencode\/packages\/@fro\.bot\/systematic@[^/]+\/node_modules\/@fro\.bot\/systematic\/skills(?:$|\/)/u.test(normalizedPath);
1114
738
  }
1115
- function normalizePath(path7) {
1116
- return path7.replaceAll("\\", "/").replace(/\/+$/u, "");
739
+ function normalizePath(path5) {
740
+ return path5.replaceAll("\\", "/").replace(/\/+$/u, "");
1117
741
  }
1118
742
 
1119
743
  // src/lib/opencode-operation-observer.ts
1120
744
  import { spawnSync } from "child_process";
1121
- import { createHash as createHash2 } from "crypto";
1122
- import fs7 from "fs";
1123
- import path7 from "path";
745
+ import { createHash } from "crypto";
746
+ import fs6 from "fs";
747
+ import path5 from "path";
1124
748
  var DEFAULT_LIMITS = {
1125
749
  maxCommandOutputBytes: 1048576,
1126
750
  maxCommandTimeoutMs: 5000,
@@ -1141,7 +765,7 @@ function unavailable(result) {
1141
765
  return { status: "unavailable", reasonCode: result.reasonCode };
1142
766
  }
1143
767
  function digest(domain, facts) {
1144
- const hash = createHash2("sha256");
768
+ const hash = createHash("sha256");
1145
769
  hash.update(`systematic:opencode-observer:${domain}:v1\x00`);
1146
770
  for (const fact of facts) {
1147
771
  hash.update(fact);
@@ -1232,11 +856,11 @@ function parseStageEntries(output, maxPathBytes) {
1232
856
  return entries.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
1233
857
  }
1234
858
  function safePath(root, relativePath) {
1235
- if (path7.isAbsolute(relativePath))
859
+ if (path5.isAbsolute(relativePath))
1236
860
  return;
1237
- const absolutePath = path7.resolve(root, relativePath);
1238
- const relative = path7.relative(root, absolutePath);
1239
- if (relative === "" || relative === ".." || relative.startsWith(`..${path7.sep}`)) {
861
+ const absolutePath = path5.resolve(root, relativePath);
862
+ const relative = path5.relative(root, absolutePath);
863
+ if (relative === "" || relative === ".." || relative.startsWith(`..${path5.sep}`)) {
1240
864
  return;
1241
865
  }
1242
866
  return absolutePath;
@@ -1259,25 +883,25 @@ function runCommand(runner, args2, cwd, limits) {
1259
883
  }
1260
884
  return { status: "ok", output: { stdout: result.stdout } };
1261
885
  }
1262
- function canonicalPath(filePath, realPath = fs7.realpathSync) {
886
+ function canonicalPath(filePath, realPath = fs6.realpathSync) {
1263
887
  try {
1264
888
  return realPath(filePath);
1265
889
  } catch {
1266
890
  return;
1267
891
  }
1268
892
  }
1269
- function requiredGitPath(runner, args2, cwd, limits, realPath = fs7.realpathSync) {
893
+ function requiredGitPath(runner, args2, cwd, limits, realPath = fs6.realpathSync) {
1270
894
  const result = runCommand(runner, args2, cwd, limits);
1271
895
  if (result.status === "error")
1272
896
  return result;
1273
897
  const rawValue = result.output.stdout.trim();
1274
- if (!path7.isAbsolute(rawValue)) {
898
+ if (!path5.isAbsolute(rawValue)) {
1275
899
  return { status: "error", reasonCode: "target-unavailable" };
1276
900
  }
1277
901
  const value = canonicalPath(rawValue, realPath);
1278
902
  return value === undefined ? { status: "error", reasonCode: "target-unavailable" } : { status: "ok", value };
1279
903
  }
1280
- function captureParentIdentity(targetDirectory, runner, limits, realPath = fs7.realpathSync) {
904
+ function captureParentIdentity(targetDirectory, runner, limits, realPath = fs6.realpathSync) {
1281
905
  const targetRoot = requiredGitPath(runner, ["rev-parse", "--show-toplevel"], targetDirectory, limits, realPath);
1282
906
  if (targetRoot.status === "error")
1283
907
  return targetRoot;
@@ -1305,53 +929,53 @@ function captureParentIdentity(targetDirectory, runner, limits, realPath = fs7.r
1305
929
  }
1306
930
  } : { status: "error", reasonCode: "target-unavailable" };
1307
931
  }
1308
- function readGitfileTarget(gitfilePath, realPath = fs7.realpathSync) {
932
+ function readGitfileTarget(gitfilePath, realPath = fs6.realpathSync) {
1309
933
  let contents;
1310
934
  try {
1311
- contents = fs7.readFileSync(gitfilePath, "utf8");
935
+ contents = fs6.readFileSync(gitfilePath, "utf8");
1312
936
  } catch {
1313
937
  return;
1314
938
  }
1315
939
  const match = /^gitdir:\s*(.+?)\s*$/im.exec(contents);
1316
940
  if (!match)
1317
941
  return;
1318
- const target = path7.isAbsolute(match[1]) ? match[1] : path7.resolve(path7.dirname(gitfilePath), match[1]);
942
+ const target = path5.isAbsolute(match[1]) ? match[1] : path5.resolve(path5.dirname(gitfilePath), match[1]);
1319
943
  return canonicalPath(target, realPath);
1320
944
  }
1321
945
  function readGitdirBacklink(backlinkPath, realPath) {
1322
946
  let contents;
1323
947
  try {
1324
- contents = fs7.readFileSync(backlinkPath, "utf8").trim();
948
+ contents = fs6.readFileSync(backlinkPath, "utf8").trim();
1325
949
  } catch {
1326
950
  return;
1327
951
  }
1328
952
  if (contents.length === 0)
1329
953
  return;
1330
- const target = path7.isAbsolute(contents) ? contents : path7.resolve(path7.dirname(backlinkPath), contents);
954
+ const target = path5.isAbsolute(contents) ? contents : path5.resolve(path5.dirname(backlinkPath), contents);
1331
955
  return canonicalPath(target, realPath);
1332
956
  }
1333
957
  function isPathWithin(root, candidate) {
1334
- const relative = path7.relative(root, candidate);
1335
- return relative !== "" && relative !== ".." && !relative.startsWith(`..${path7.sep}`) && !path7.isAbsolute(relative);
958
+ const relative = path5.relative(root, candidate);
959
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path5.sep}`) && !path5.isAbsolute(relative);
1336
960
  }
1337
961
  function validateDotGitLinkage(targetRoot, gitDir, commonDir, realPath) {
1338
- const dotGit = path7.join(targetRoot, ".git");
962
+ const dotGit = path5.join(targetRoot, ".git");
1339
963
  let dotGitStat;
1340
964
  try {
1341
- dotGitStat = fs7.lstatSync(dotGit);
965
+ dotGitStat = fs6.lstatSync(dotGit);
1342
966
  } catch {
1343
967
  return false;
1344
968
  }
1345
969
  if (dotGitStat.isDirectory()) {
1346
970
  return gitDir === commonDir && canonicalPath(dotGit, realPath) === commonDir;
1347
971
  }
1348
- if (!dotGitStat.isFile() || dotGitStat.isSymbolicLink() || !isPathWithin(path7.join(commonDir, "worktrees"), gitDir) || readGitfileTarget(dotGit, realPath) !== gitDir) {
972
+ if (!dotGitStat.isFile() || dotGitStat.isSymbolicLink() || !isPathWithin(path5.join(commonDir, "worktrees"), gitDir) || readGitfileTarget(dotGit, realPath) !== gitDir) {
1349
973
  return false;
1350
974
  }
1351
- const backlinkPath = path7.join(gitDir, "gitdir");
975
+ const backlinkPath = path5.join(gitDir, "gitdir");
1352
976
  let backlinkStat;
1353
977
  try {
1354
- backlinkStat = fs7.lstatSync(backlinkPath);
978
+ backlinkStat = fs6.lstatSync(backlinkPath);
1355
979
  } catch {
1356
980
  return false;
1357
981
  }
@@ -1360,7 +984,7 @@ function validateDotGitLinkage(targetRoot, gitDir, commonDir, realPath) {
1360
984
  function validateRegisteredWorktree(candidateDirectory, parentIdentity, options = {}) {
1361
985
  const runner = options.commandRunner ?? defaultCommandRunner;
1362
986
  const limits = mergeLimits(options.limits);
1363
- const realPath = options.realPath ?? fs7.realpathSync;
987
+ const realPath = options.realPath ?? fs6.realpathSync;
1364
988
  const candidateRoot = canonicalPath(candidateDirectory, realPath);
1365
989
  if (candidateRoot === undefined) {
1366
990
  return { status: "error", reasonCode: "target-unavailable" };
@@ -1527,7 +1151,7 @@ function readWorktreeRevisionV2(runner, fileReader, symlinkReader, statReader, c
1527
1151
  if (paths.length > limits.maxFiles) {
1528
1152
  return { status: "error", reasonCode: "file-limit" };
1529
1153
  }
1530
- const hash = createHash2("sha256");
1154
+ const hash = createHash("sha256");
1531
1155
  hash.update("systematic:opencode-observer:worktree:v2\x00");
1532
1156
  let totalBytes = 0;
1533
1157
  for (const relativePath of paths) {
@@ -1745,19 +1369,19 @@ function createOpencodeOperationObserver(options) {
1745
1369
  const limits = mergeLimits(options.limits);
1746
1370
  let targetDirectory;
1747
1371
  try {
1748
- targetDirectory = (options.realPath ?? fs7.realpathSync)(options.targetDirectory);
1372
+ targetDirectory = (options.realPath ?? fs6.realpathSync)(options.targetDirectory);
1749
1373
  } catch {
1750
- targetDirectory = path7.resolve(options.targetDirectory);
1374
+ targetDirectory = path5.resolve(options.targetDirectory);
1751
1375
  }
1752
1376
  const runner = options.commandRunner ?? defaultCommandRunner;
1753
1377
  const remoteRunner = options.remoteCommandRunner ?? defaultRemoteCommandRunner;
1754
- const realPath = options.realPath ?? fs7.realpathSync;
1378
+ const realPath = options.realPath ?? fs6.realpathSync;
1755
1379
  const parentIdentityResult = captureParentIdentity(targetDirectory, runner, limits, realPath);
1756
1380
  const targetDigest = digest("target", [targetDirectory]);
1757
- const fileReader = options.fileReader ?? fs7.readFileSync;
1758
- const symlinkReader = options.symlinkReader ?? fs7.readlinkSync;
1381
+ const fileReader = options.fileReader ?? fs6.readFileSync;
1382
+ const symlinkReader = options.symlinkReader ?? fs6.readlinkSync;
1759
1383
  const statReader = options.statReader ?? ((filePath) => {
1760
- const stat = fs7.lstatSync(filePath);
1384
+ const stat = fs6.lstatSync(filePath);
1761
1385
  return {
1762
1386
  isFile: stat.isFile(),
1763
1387
  isSymbolicLink: stat.isSymbolicLink(),
@@ -1820,11 +1444,11 @@ function createOpencodeOperationObserver(options) {
1820
1444
 
1821
1445
  // src/lib/opencode-workflow-guard.ts
1822
1446
  import { randomBytes as randomBytes4 } from "crypto";
1823
- import fs9 from "fs";
1824
- import path8 from "path";
1447
+ import fs8 from "fs";
1448
+ import path6 from "path";
1825
1449
 
1826
1450
  // src/lib/question-attestation.ts
1827
- import { createHash as createHash3, randomBytes } from "crypto";
1451
+ import { createHash as createHash2, randomBytes } from "crypto";
1828
1452
  var CANONICAL_QUESTION_WORDING = "Confirm the requested guarded transition.";
1829
1453
  var MAX_ID_LENGTH = 128;
1830
1454
  var MAX_RESOURCE_LENGTH = 512;
@@ -1855,7 +1479,7 @@ function isQuestionPurpose(value) {
1855
1479
  return value === "transition" || value === "session-disablement";
1856
1480
  }
1857
1481
  function digestResource(resource) {
1858
- return `sha256:${createHash3("sha256").update(`systematic-question-attestation:v1:resource:${resource}`, "utf8").digest("hex")}`;
1482
+ return `sha256:${createHash2("sha256").update(`systematic-question-attestation:v1:resource:${resource}`, "utf8").digest("hex")}`;
1859
1483
  }
1860
1484
  function bindingKey(input, resourceDigest) {
1861
1485
  return [
@@ -2341,7 +1965,7 @@ function createQuestionAttestation(options = {}) {
2341
1965
  import { randomBytes as randomBytes2 } from "crypto";
2342
1966
 
2343
1967
  // src/lib/receipt-readback.ts
2344
- import { createHash as createHash4 } from "crypto";
1968
+ import { createHash as createHash3 } from "crypto";
2345
1969
  var RECEIPT_READBACK_SCHEMA_VERSION = 2;
2346
1970
  var RECEIPT_READBACK_PROTOCOL_VERSION = 2;
2347
1971
  var LEGACY_RECEIPT_READBACK_SCHEMA_VERSION = 1;
@@ -2512,7 +2136,7 @@ function sessionSaltHex(value) {
2512
2136
  }
2513
2137
  function digestReceiptIdentity(domain, identity, sessionSalt) {
2514
2138
  const salt = sessionSaltHex(sessionSalt) ?? "invalid-salt";
2515
- return createHash4("sha256").update(`systematic/receipt-ledger/${domain}/v1/${salt}/${identity}`).digest("hex");
2139
+ return createHash3("sha256").update(`systematic/receipt-ledger/${domain}/v1/${salt}/${identity}`).digest("hex");
2516
2140
  }
2517
2141
  function decodeSessionSalt(value) {
2518
2142
  return Uint8Array.from(Buffer.from(value, "hex"));
@@ -2675,7 +2299,7 @@ function serializeEnvelope(envelope) {
2675
2299
  });
2676
2300
  }
2677
2301
  function hashIntegrity(scope, value) {
2678
- return createHash4("sha256").update(`systematic/receipt-readback/${scope}/v1/${value}`).digest("hex");
2302
+ return createHash3("sha256").update(`systematic/receipt-readback/${scope}/v1/${value}`).digest("hex");
2679
2303
  }
2680
2304
  function mintIntegrity(envelope, sessionSalt) {
2681
2305
  return hashIntegrity("mint", `${sessionSalt}/${serializeEnvelope(envelope)}`);
@@ -4361,8 +3985,8 @@ function normalizeCapabilities(input) {
4361
3985
  import { randomBytes as randomBytes3 } from "crypto";
4362
3986
 
4363
3987
  // src/lib/receipt-classifier.ts
4364
- import fs8 from "fs";
4365
- import { fileURLToPath as fileURLToPath2 } from "url";
3988
+ import fs7 from "fs";
3989
+ import { fileURLToPath } from "url";
4366
3990
 
4367
3991
  // node_modules/.bun/web-tree-sitter@0.25.10/node_modules/web-tree-sitter/tree-sitter.js
4368
3992
  var __defProp = Object.defineProperty;
@@ -5798,8 +5422,8 @@ var Module2 = (() => {
5798
5422
  var moduleRtn;
5799
5423
  var Module = moduleArg;
5800
5424
  var readyPromiseResolve, readyPromiseReject;
5801
- var readyPromise = new Promise((resolve2, reject) => {
5802
- readyPromiseResolve = resolve2;
5425
+ var readyPromise = new Promise((resolve, reject) => {
5426
+ readyPromiseResolve = resolve;
5803
5427
  readyPromiseReject = reject;
5804
5428
  });
5805
5429
  var ENVIRONMENT_IS_WEB = typeof window == "object";
@@ -5821,11 +5445,11 @@ var Module2 = (() => {
5821
5445
  throw toThrow;
5822
5446
  }, "quit_");
5823
5447
  var scriptDirectory = "";
5824
- function locateFile(path8) {
5448
+ function locateFile(path6) {
5825
5449
  if (Module["locateFile"]) {
5826
- return Module["locateFile"](path8, scriptDirectory);
5450
+ return Module["locateFile"](path6, scriptDirectory);
5827
5451
  }
5828
- return scriptDirectory + path8;
5452
+ return scriptDirectory + path6;
5829
5453
  }
5830
5454
  __name(locateFile, "locateFile");
5831
5455
  var readAsync, readBinary;
@@ -5879,13 +5503,13 @@ var Module2 = (() => {
5879
5503
  }
5880
5504
  readAsync = /* @__PURE__ */ __name(async (url) => {
5881
5505
  if (isFileURI(url)) {
5882
- return new Promise((resolve2, reject) => {
5506
+ return new Promise((resolve, reject) => {
5883
5507
  var xhr = new XMLHttpRequest;
5884
5508
  xhr.open("GET", url, true);
5885
5509
  xhr.responseType = "arraybuffer";
5886
5510
  xhr.onload = () => {
5887
5511
  if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
5888
- resolve2(xhr.response);
5512
+ resolve(xhr.response);
5889
5513
  return;
5890
5514
  }
5891
5515
  reject(xhr.status);
@@ -6105,10 +5729,10 @@ var Module2 = (() => {
6105
5729
  __name(receiveInstantiationResult, "receiveInstantiationResult");
6106
5730
  var info2 = getWasmImports();
6107
5731
  if (Module["instantiateWasm"]) {
6108
- return new Promise((resolve2, reject) => {
5732
+ return new Promise((resolve, reject) => {
6109
5733
  Module["instantiateWasm"](info2, (mod, inst) => {
6110
5734
  receiveInstance(mod, inst);
6111
- resolve2(mod.exports);
5735
+ resolve(mod.exports);
6112
5736
  });
6113
5737
  });
6114
5738
  }
@@ -7899,7 +7523,7 @@ function matchesToolIdentity(tool, executable) {
7899
7523
  async function initializeParserAssets(options) {
7900
7524
  const treeSitterWasmPath = options.treeSitterWasmPath ?? resolveAsset("web-tree-sitter/tree-sitter.wasm");
7901
7525
  const bashWasmPath = options.bashWasmPath ?? resolveAsset("tree-sitter-bash/tree-sitter-bash.wasm");
7902
- if (!fs8.existsSync(treeSitterWasmPath) || !fs8.existsSync(bashWasmPath)) {
7526
+ if (!fs7.existsSync(treeSitterWasmPath) || !fs7.existsSync(bashWasmPath)) {
7903
7527
  throw new Error("parser-asset-unavailable");
7904
7528
  }
7905
7529
  try {
@@ -7920,7 +7544,7 @@ async function initializeParserAssets(options) {
7920
7544
  }
7921
7545
  function resolveAsset(specifier) {
7922
7546
  const resolved = import.meta.resolve(specifier);
7923
- return resolved.startsWith("file://") ? fileURLToPath2(resolved) : resolved;
7547
+ return resolved.startsWith("file://") ? fileURLToPath(resolved) : resolved;
7924
7548
  }
7925
7549
  function classifyParserFailure(error) {
7926
7550
  if (error instanceof Error && error.message === "grammar-incompatible") {
@@ -10777,16 +10401,16 @@ function canonicalFileTarget(filePath, realPath) {
10777
10401
  const existing = canonicalExistingPath(filePath, realPath);
10778
10402
  if (existing)
10779
10403
  return existing;
10780
- const parent = canonicalExistingPath(path8.dirname(filePath), realPath);
10781
- const basename = path8.basename(filePath);
10782
- return parent && basename && basename !== "." && basename !== ".." ? path8.join(parent, basename) : undefined;
10404
+ const parent = canonicalExistingPath(path6.dirname(filePath), realPath);
10405
+ const basename = path6.basename(filePath);
10406
+ return parent && basename && basename !== "." && basename !== ".." ? path6.join(parent, basename) : undefined;
10783
10407
  }
10784
10408
  function pathWithinOrEqual(root, candidate) {
10785
- const relative = path8.relative(root, candidate);
10786
- return relative === "" || relative !== ".." && !relative.startsWith(`..${path8.sep}`) && !path8.isAbsolute(relative);
10409
+ const relative = path6.relative(root, candidate);
10410
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${path6.sep}`) && !path6.isAbsolute(relative);
10787
10411
  }
10788
10412
  function targetIsGitAdminStorage(targetPath, validation) {
10789
- return pathWithinOrEqual(path8.join(validation.targetRoot, ".git"), targetPath) || pathWithinOrEqual(validation.gitDir, targetPath) || pathWithinOrEqual(validation.commonDir, targetPath);
10413
+ return pathWithinOrEqual(path6.join(validation.targetRoot, ".git"), targetPath) || pathWithinOrEqual(validation.gitDir, targetPath) || pathWithinOrEqual(validation.commonDir, targetPath);
10790
10414
  }
10791
10415
  function targetResultFromValidation(candidatePath, validation) {
10792
10416
  if (validation.status === "error")
@@ -10802,7 +10426,7 @@ function targetResultFromValidation(candidatePath, validation) {
10802
10426
  function trustedParentTarget(parentTargetRoot, candidatePath) {
10803
10427
  if (!pathWithinOrEqual(parentTargetRoot, candidatePath))
10804
10428
  return;
10805
- if (pathWithinOrEqual(path8.join(parentTargetRoot, ".git"), candidatePath)) {
10429
+ if (pathWithinOrEqual(path6.join(parentTargetRoot, ".git"), candidatePath)) {
10806
10430
  return unavailableOperationTarget();
10807
10431
  }
10808
10432
  return { status: "available", targetRoot: parentTargetRoot };
@@ -10815,14 +10439,14 @@ function validationForCandidate(candidatePath, options, targetPath = candidatePa
10815
10439
  }
10816
10440
  }
10817
10441
  function deriveFileTarget(rawPath, baseDirectory, parentTargetRoot, options, realPath, allowParentFastPath = true) {
10818
- const resolvedPath = path8.resolve(baseDirectory, rawPath);
10442
+ const resolvedPath = path6.resolve(baseDirectory, rawPath);
10819
10443
  const canonicalPath2 = canonicalFileTarget(resolvedPath, realPath);
10820
10444
  if (!canonicalPath2)
10821
10445
  return unavailableOperationTarget();
10822
- const parentResult = allowParentFastPath && !path8.isAbsolute(rawPath) ? trustedParentTarget(parentTargetRoot, canonicalPath2) : undefined;
10446
+ const parentResult = allowParentFastPath && !path6.isAbsolute(rawPath) ? trustedParentTarget(parentTargetRoot, canonicalPath2) : undefined;
10823
10447
  if (parentResult)
10824
10448
  return parentResult;
10825
- return validationForCandidate(path8.dirname(canonicalPath2), options, canonicalPath2);
10449
+ return validationForCandidate(path6.dirname(canonicalPath2), options, canonicalPath2);
10826
10450
  }
10827
10451
  function sharedTargetRoot(targets, extraRoots = []) {
10828
10452
  const roots = [...extraRoots];
@@ -10835,10 +10459,10 @@ function sharedTargetRoot(targets, extraRoots = []) {
10835
10459
  return targetRoot && roots.every((root) => root === targetRoot) ? targetRoot : undefined;
10836
10460
  }
10837
10461
  function deriveDirectoryTarget(rawPath, baseDirectory, parentTargetRoot, options, realPath) {
10838
- const resolvedPath = canonicalExistingPath(path8.resolve(baseDirectory, rawPath), realPath);
10462
+ const resolvedPath = canonicalExistingPath(path6.resolve(baseDirectory, rawPath), realPath);
10839
10463
  if (!resolvedPath)
10840
10464
  return { status: "unavailable" };
10841
- const parentResult = path8.isAbsolute(rawPath) ? undefined : trustedParentTarget(parentTargetRoot, resolvedPath);
10465
+ const parentResult = path6.isAbsolute(rawPath) ? undefined : trustedParentTarget(parentTargetRoot, resolvedPath);
10842
10466
  if (parentResult?.status === "unavailable") {
10843
10467
  return { status: "unavailable" };
10844
10468
  }
@@ -10946,14 +10570,14 @@ function deriveBashTarget(args2, sessionLocation, parentTargetRoot, options, rea
10946
10570
  if (typeof args2.workdir !== "string" || args2.workdir.length === 0) {
10947
10571
  return unavailableOperationTarget();
10948
10572
  }
10949
- const candidatePath = canonicalExistingPath(path8.resolve(sessionLocation, args2.workdir), realPath);
10573
+ const candidatePath = canonicalExistingPath(path6.resolve(sessionLocation, args2.workdir), realPath);
10950
10574
  if (!candidatePath)
10951
10575
  return unavailableOperationTarget();
10952
- const parentResult = path8.isAbsolute(args2.workdir) ? undefined : trustedParentTarget(parentTargetRoot, candidatePath);
10576
+ const parentResult = path6.isAbsolute(args2.workdir) ? undefined : trustedParentTarget(parentTargetRoot, candidatePath);
10953
10577
  return parentResult ?? validationForCandidate(candidatePath, options);
10954
10578
  }
10955
10579
  function deriveOpencodeOperationTarget(tool, args2, options) {
10956
- const realPath = options.realPath ?? fs9.realpathSync;
10580
+ const realPath = options.realPath ?? fs8.realpathSync;
10957
10581
  const parentTargetRoot = canonicalExistingPath(options.parentTargetRoot, realPath);
10958
10582
  if (!parentTargetRoot)
10959
10583
  return unavailableOperationTarget();
@@ -11030,7 +10654,7 @@ function canonicalPatchText(patchText, sessionLocation) {
11030
10654
  const filePath = line.slice(prefix.length).trim();
11031
10655
  if (!filePath)
11032
10656
  return;
11033
- segments[index] = `${prefix}${path8.resolve(sessionLocation, filePath)}`;
10657
+ segments[index] = `${prefix}${path6.resolve(sessionLocation, filePath)}`;
11034
10658
  }
11035
10659
  return segments.join("");
11036
10660
  }
@@ -11044,13 +10668,13 @@ function canonicalHunks(value, sessionLocation) {
11044
10668
  }
11045
10669
  const canonical = {
11046
10670
  ...hunk,
11047
- path: path8.resolve(sessionLocation, hunk.path)
10671
+ path: path6.resolve(sessionLocation, hunk.path)
11048
10672
  };
11049
10673
  if (hunk.move_path !== undefined) {
11050
10674
  if (typeof hunk.move_path !== "string" || !hunk.move_path) {
11051
10675
  return;
11052
10676
  }
11053
- canonical.move_path = path8.resolve(sessionLocation, hunk.move_path);
10677
+ canonical.move_path = path6.resolve(sessionLocation, hunk.move_path);
11054
10678
  }
11055
10679
  hunks.push(canonical);
11056
10680
  }
@@ -11339,7 +10963,7 @@ function createSessionRuntime(options, operationObservers, recoveredOperationCon
11339
10963
  function parentObserverRegistration() {
11340
10964
  if (!options.observer)
11341
10965
  return;
11342
- const targetRoot = canonicalExistingPath(options.targetDirectory ?? process.cwd(), fs9.realpathSync);
10966
+ const targetRoot = canonicalExistingPath(options.targetDirectory ?? process.cwd(), fs8.realpathSync);
11343
10967
  if (!targetRoot)
11344
10968
  return;
11345
10969
  let validation;
@@ -12288,7 +11912,7 @@ function createSessionRuntime(options, operationObservers, recoveredOperationCon
12288
11912
  markUnavailable();
12289
11913
  return;
12290
11914
  }
12291
- const canonicalParentTargetRoot = canonicalExistingPath(parentTargetRoot, fs9.realpathSync);
11915
+ const canonicalParentTargetRoot = canonicalExistingPath(parentTargetRoot, fs8.realpathSync);
12292
11916
  if (!canonicalParentTargetRoot) {
12293
11917
  markUnavailable();
12294
11918
  return;
@@ -13324,8 +12948,8 @@ function createOpencodeWorkflowGuard(options) {
13324
12948
  }
13325
12949
 
13326
12950
  // src/lib/skill-resolver.ts
13327
- import fs10 from "fs";
13328
- import path9 from "path";
12951
+ import fs9 from "fs";
12952
+ import path7 from "path";
13329
12953
  import { pathToFileURL } from "url";
13330
12954
 
13331
12955
  // src/lib/skill-catalog.ts
@@ -13387,7 +13011,7 @@ function buildSkillToolParameterHint(options) {
13387
13011
  }
13388
13012
  function buildSkillContentOutput(matchedSkill) {
13389
13013
  const body2 = extractSkillBody(matchedSkill.wrappedTemplate);
13390
- const dir = path9.dirname(matchedSkill.skillFile);
13014
+ const dir = path7.dirname(matchedSkill.skillFile);
13391
13015
  const base = pathToFileURL(dir).href;
13392
13016
  const files = discoverSkillFiles(dir);
13393
13017
  const lines = [
@@ -13418,17 +13042,17 @@ function discoverSkillFiles(dir, limit = 10) {
13418
13042
  function handleEntry(entry, currentDir) {
13419
13043
  if (entry.isDirectory()) {
13420
13044
  if (!shouldSkipDirectory(entry.name)) {
13421
- recurse(path9.resolve(currentDir, entry.name));
13045
+ recurse(path7.resolve(currentDir, entry.name));
13422
13046
  }
13423
13047
  } else if (shouldIncludeFile(entry.name)) {
13424
- files.push(path9.resolve(currentDir, entry.name));
13048
+ files.push(path7.resolve(currentDir, entry.name));
13425
13049
  }
13426
13050
  }
13427
13051
  function recurse(currentDir) {
13428
13052
  if (files.length >= limit)
13429
13053
  return;
13430
13054
  try {
13431
- const entries = fs10.readdirSync(currentDir, { withFileTypes: true });
13055
+ const entries = fs9.readdirSync(currentDir, { withFileTypes: true });
13432
13056
  for (const entry of entries) {
13433
13057
  if (files.length >= limit)
13434
13058
  break;
@@ -13486,19 +13110,19 @@ function createSkillTool(options) {
13486
13110
  }
13487
13111
 
13488
13112
  // src/index.ts
13489
- var __dirname3 = path10.dirname(fileURLToPath3(import.meta.url));
13490
- var packageRoot2 = path10.resolve(__dirname3, "..");
13491
- var bundledSkillsDir = path10.join(packageRoot2, "skills");
13492
- var bundledAgentsDir2 = path10.join(packageRoot2, "agents");
13493
- var bundledCommandsDir = path10.join(packageRoot2, "commands");
13494
- var packageJsonPath = path10.join(packageRoot2, "package.json");
13495
- var canonicalPackageSource = pathToFileURL2(fs11.realpathSync(packageRoot2)).href;
13496
- var registrationSourceIdentity = createHash5("sha256").update(`systematic/opencode-registration-source/v1/${canonicalPackageSource}`).digest("hex");
13113
+ var __dirname2 = path8.dirname(fileURLToPath2(import.meta.url));
13114
+ var packageRoot = path8.resolve(__dirname2, "..");
13115
+ var bundledSkillsDir = path8.join(packageRoot, "skills");
13116
+ var bundledAgentsDir = path8.join(packageRoot, "agents");
13117
+ var bundledCommandsDir = path8.join(packageRoot, "commands");
13118
+ var packageJsonPath = path8.join(packageRoot, "package.json");
13119
+ var canonicalPackageSource = pathToFileURL2(fs10.realpathSync(packageRoot)).href;
13120
+ var registrationSourceIdentity = createHash4("sha256").update(`systematic/opencode-registration-source/v1/${canonicalPackageSource}`).digest("hex");
13497
13121
  var getPackageVersion = () => {
13498
13122
  try {
13499
- if (!fs11.existsSync(packageJsonPath))
13123
+ if (!fs10.existsSync(packageJsonPath))
13500
13124
  return "unknown";
13501
- const content = fs11.readFileSync(packageJsonPath, "utf8");
13125
+ const content = fs10.readFileSync(packageJsonPath, "utf8");
13502
13126
  const parsed = JSON.parse(content);
13503
13127
  return parsed.version ?? "unknown";
13504
13128
  } catch {
@@ -13519,9 +13143,8 @@ var initializePlugin = async ({
13519
13143
  const configHandler = createConfigHandler({
13520
13144
  directory,
13521
13145
  bundledSkillsDir,
13522
- bundledAgentsDir: bundledAgentsDir2,
13523
- bundledCommandsDir,
13524
- client
13146
+ bundledAgentsDir,
13147
+ bundledCommandsDir
13525
13148
  });
13526
13149
  const observer = createOpencodeOperationObserver({
13527
13150
  targetDirectory: typeof worktree === "string" ? worktree : directory
@@ -41,4 +41,3 @@ export interface ResolvedAgentOverlaySet {
41
41
  export declare function buildBundledAgentInventory(agentsDir: string, disabledAgents: string[]): BundledAgentInventory;
42
42
  export declare function validateAgentOverlays({ inventory, overlays, nativeAgents, enabledSkills, }: ValidateAgentOverlaysOptions): ValidatedAgentOverlays;
43
43
  export declare function resolveAgentOverlaySet(overlays: ValidatedAgentOverlays): ResolvedAgentOverlaySet;
44
- export declare function assertSourceCategoryModelCoverage(categories: string[]): void;
@@ -1,12 +1,9 @@
1
1
  import type { Config } from '@opencode-ai/plugin';
2
- import { type OpencodeClientLike } from './model-availability.js';
3
2
  export interface ConfigHandlerDeps {
4
3
  directory: string;
5
4
  bundledSkillsDir: string;
6
5
  bundledAgentsDir: string;
7
6
  bundledCommandsDir: string;
8
- /** OpenCode client for availability lookup. When omitted, availability falls back to empty set (last-resort resolution). */
9
- client?: OpencodeClientLike;
10
7
  /** Home directory for discovered-skill lookups. Defaults to `os.homedir()`; inject a temp dir in tests. */
11
8
  homeDir?: string;
12
9
  /** OpenCode global config directory override for discovered-skill lookups. Defaults to `<homeDir>/.config/opencode`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fro.bot/systematic",
3
- "version": "3.9.1",
3
+ "version": "3.10.0",
4
4
  "description": "Compound-engineering loops for OpenCode, Pi, and Claude Code",
5
5
  "type": "module",
6
6
  "homepage": "https://fro.bot/systematic",
@@ -1,83 +0,0 @@
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 AND `models` is non-empty. An authoritatively
26
- * empty response (`data.providers = []`, or providers present with zero
27
- * models) collapses to `'unknown'` instead — see below — because the
28
- * operational consequence is identical and downstream consumers should treat
29
- * both cases the same way.
30
- * - `cache`: The API call failed (error envelope, thrown, or timed out) and
31
- * the local `models.json` cache was readable AND non-empty. `models`
32
- * reflects whatever OpenCode last wrote to disk. A cache that loads
33
- * successfully but produces a zero-size set collapses to `'unknown'` for
34
- * the same operational reason an empty API response does.
35
- * - `unknown`: Either both the API call and the cache fallback failed (cache
36
- * missing, unreadable, corrupt, or schema-mismatched), OR the API call
37
- * succeeded with zero usable models. Resolution should degrade gracefully —
38
- * callers should treat `unknown` as a signal to skip source-default model
39
- * pinning so users do not get agents pinned to inaccessible models.
40
- * `models` is the empty set.
41
- */
42
- export type DiscoveryStatus = 'api' | 'cache' | 'unknown';
43
- export interface ModelAvailability {
44
- status: DiscoveryStatus;
45
- /**
46
- * Set of `${providerId}/${modelId}` strings. Typed `ReadonlySet` because
47
- * callers must not mutate the returned collection — mutation would corrupt
48
- * future calls in the same process. Each `ModelAvailability` is a fresh
49
- * instance (see `emptyAvailability()`), so mutation via cast cannot
50
- * propagate, but the type makes intent explicit.
51
- */
52
- models: ReadonlySet<string>;
53
- }
54
- interface AvailabilityOptions {
55
- /**
56
- * Maximum time to wait for `client.config.providers()` before falling
57
- * back to the local cache. Defaults to 1500ms — a startup-budget value
58
- * that prevents a slow/half-open OpenCode server from holding the plugin
59
- * indefinitely.
60
- *
61
- * Set to `null` to disable the timeout entirely (not recommended).
62
- */
63
- apiTimeoutMs?: number | null;
64
- }
65
- /**
66
- * Discover the set of `provider/model` keys the OpenCode server considers
67
- * connected (or, on API failure, the set last written to the on-disk
68
- * `models.json` cache).
69
- *
70
- * The returned `status` lets callers distinguish three discovery outcomes:
71
- * - `api`: live answer; safe to pin source-default models against it
72
- * - `cache`: degraded but informed; the cached `provider/model` keys are
73
- * plausibly still authoritative
74
- * - `unknown`: both the API and the cache failed; callers should fall back
75
- * to OpenCode's parent-model inheritance rather than pinning a source
76
- * default the user may not have access to
77
- *
78
- * The API call is bounded by `apiTimeoutMs` (default 1500ms). On timeout,
79
- * thrown error, error-envelope response, or undefined data, the cache
80
- * fallback runs. The function never rejects.
81
- */
82
- export declare function getAvailableModels(client: OpencodeClientLike, options?: AvailabilityOptions): Promise<ModelAvailability>;
83
- export {};
@@ -1,117 +0,0 @@
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 5 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
- };