@fro.bot/systematic 3.15.1 → 3.16.1

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.
@@ -1,4 +1,311 @@
1
1
  // @bun
2
+ // src/lib/validation.ts
3
+ function isRecord(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+ function isPermissionSetting(value) {
7
+ return value === "ask" || value === "allow" || value === "deny";
8
+ }
9
+ function isToolsMap(value) {
10
+ if (!isRecord(value))
11
+ return false;
12
+ return Object.values(value).every((entry) => typeof entry === "boolean");
13
+ }
14
+ function isAgentMode(value) {
15
+ return value === "subagent" || value === "primary" || value === "all";
16
+ }
17
+ function extractSimplePermission(data, key) {
18
+ if (!(key in data))
19
+ return;
20
+ const value = data[key];
21
+ return isPermissionSetting(value) ? value : null;
22
+ }
23
+ function extractBashPermission(data) {
24
+ if (!("bash" in data))
25
+ return;
26
+ const bash = data.bash;
27
+ if (isPermissionSetting(bash))
28
+ return bash;
29
+ if (isRecord(bash)) {
30
+ const entries = Object.entries(bash);
31
+ if (entries.every(([, setting]) => isPermissionSetting(setting))) {
32
+ return Object.fromEntries(entries);
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ function buildPermissionObject(edit, bash, webfetch, doom_loop, external_directory, task, skill) {
38
+ const permission = {};
39
+ if (edit)
40
+ permission.edit = edit;
41
+ if (bash)
42
+ permission.bash = bash;
43
+ if (webfetch)
44
+ permission.webfetch = webfetch;
45
+ if (doom_loop)
46
+ permission.doom_loop = doom_loop;
47
+ if (external_directory)
48
+ permission.external_directory = external_directory;
49
+ if (task)
50
+ permission.task = task;
51
+ if (skill)
52
+ permission.skill = skill;
53
+ return Object.keys(permission).length > 0 ? permission : undefined;
54
+ }
55
+ function normalizePermission(value) {
56
+ if (!isRecord(value))
57
+ return;
58
+ const bash = extractBashPermission(value);
59
+ if (bash === null)
60
+ return;
61
+ const edit = extractSimplePermission(value, "edit");
62
+ if (edit === null)
63
+ return;
64
+ const webfetch = extractSimplePermission(value, "webfetch");
65
+ if (webfetch === null)
66
+ return;
67
+ const doom_loop = extractSimplePermission(value, "doom_loop");
68
+ if (doom_loop === null)
69
+ return;
70
+ const external_directory = extractSimplePermission(value, "external_directory");
71
+ if (external_directory === null)
72
+ return;
73
+ const task = extractSimplePermission(value, "task");
74
+ if (task === null)
75
+ return;
76
+ const skill = extractSimplePermission(value, "skill");
77
+ if (skill === null)
78
+ return;
79
+ return buildPermissionObject(edit, bash, webfetch, doom_loop, external_directory, task, skill);
80
+ }
81
+ function extractString(data, key, fallback = "") {
82
+ const value = data[key];
83
+ return typeof value === "string" ? value : fallback;
84
+ }
85
+ function extractNonEmptyString(data, key) {
86
+ const value = data[key];
87
+ if (typeof value !== "string")
88
+ return;
89
+ const trimmed = value.trim();
90
+ return trimmed !== "" ? trimmed : undefined;
91
+ }
92
+ function extractNumber(data, key) {
93
+ const value = data[key];
94
+ return typeof value === "number" ? value : undefined;
95
+ }
96
+ function extractBoolean(data, key) {
97
+ const value = data[key];
98
+ if (typeof value === "boolean")
99
+ return value;
100
+ if (typeof value === "string") {
101
+ const normalized = value.trim().toLowerCase();
102
+ if (normalized === "true")
103
+ return true;
104
+ if (normalized === "false")
105
+ return false;
106
+ }
107
+ return;
108
+ }
109
+
110
+ // src/lib/routing-resolver.ts
111
+ function getOverlayValue(map, key) {
112
+ return map[key]?.value;
113
+ }
114
+ function toSourcedOverlayMap(map) {
115
+ const result = {};
116
+ if (!map)
117
+ return result;
118
+ for (const [key, value] of Object.entries(map)) {
119
+ result[key] = { value, sourcePath: "", keyPath: key };
120
+ }
121
+ return result;
122
+ }
123
+ function toSourcedPiSubagentsOverlays(map) {
124
+ return {
125
+ agents: toSourcedOverlayMap(map?.agents),
126
+ categories: toSourcedOverlayMap(map?.categories)
127
+ };
128
+ }
129
+ function lookupAgentOverlay(overlays, target) {
130
+ return getOverlayValue(overlays.agents, target.agentKey) ?? getOverlayValue(overlays.agents, `${target.category}/${target.agentKey}`);
131
+ }
132
+ function readBlockField(overlay, blockKey, field) {
133
+ if (overlay === undefined)
134
+ return;
135
+ const block = overlay[blockKey];
136
+ if (!isRecord(block))
137
+ return;
138
+ return block[field];
139
+ }
140
+ function readFlatField(overlay, field) {
141
+ if (overlay === undefined)
142
+ return;
143
+ return overlay[field];
144
+ }
145
+ function narrowModelValue(value) {
146
+ if (typeof value === "string")
147
+ return value;
148
+ if (value === null)
149
+ return null;
150
+ return;
151
+ }
152
+ function narrowQualifierValue(value) {
153
+ return typeof value === "string" ? value : undefined;
154
+ }
155
+ function resolveField(candidates) {
156
+ for (const candidate of candidates) {
157
+ if (candidate.value !== undefined) {
158
+ return { value: candidate.value, source: candidate.source };
159
+ }
160
+ }
161
+ return { value: undefined, source: undefined };
162
+ }
163
+ function resolveModel(agentOverlay, categoryOverlay, harness) {
164
+ const { value, source } = resolveField([
165
+ {
166
+ value: readBlockField(agentOverlay, harness, "model"),
167
+ source: { level: "agent", form: "block" }
168
+ },
169
+ {
170
+ value: readFlatField(agentOverlay, "model"),
171
+ source: { level: "agent", form: "flat" }
172
+ },
173
+ {
174
+ value: readBlockField(categoryOverlay, harness, "model"),
175
+ source: { level: "category", form: "block" }
176
+ },
177
+ {
178
+ value: readFlatField(categoryOverlay, "model"),
179
+ source: { level: "category", form: "flat" }
180
+ }
181
+ ]);
182
+ const narrowedValue = narrowModelValue(value);
183
+ return {
184
+ value: narrowedValue,
185
+ source: narrowedValue === undefined ? undefined : source
186
+ };
187
+ }
188
+ var OPENCODE_MODEL_VARIANT_LAYERS = [
189
+ { level: "agent", form: "block" },
190
+ { level: "agent", form: "flat" },
191
+ { level: "category", form: "block" },
192
+ { level: "category", form: "flat" }
193
+ ];
194
+ function readOpencodeLayerField(agentOverlay, categoryOverlay, layer, field) {
195
+ const overlay = layer.level === "agent" ? agentOverlay : categoryOverlay;
196
+ return layer.form === "block" ? readBlockField(overlay, "opencode", field) : readFlatField(overlay, field);
197
+ }
198
+ function resolveOpencodeModelAndVariant(agentOverlay, categoryOverlay) {
199
+ const modelLayerIndex = OPENCODE_MODEL_VARIANT_LAYERS.findIndex((layer) => readOpencodeLayerField(agentOverlay, categoryOverlay, layer, "model") !== undefined);
200
+ const rawModel = modelLayerIndex === -1 ? undefined : readOpencodeLayerField(agentOverlay, categoryOverlay, OPENCODE_MODEL_VARIANT_LAYERS[modelLayerIndex], "model");
201
+ const model = narrowModelValue(rawModel);
202
+ const hasModel = model !== undefined;
203
+ const modelSource = hasModel ? OPENCODE_MODEL_VARIANT_LAYERS[modelLayerIndex] : undefined;
204
+ let qualifier;
205
+ let qualifierSource;
206
+ if (model !== null) {
207
+ const eligibleLayerCount = hasModel ? modelLayerIndex + 1 : OPENCODE_MODEL_VARIANT_LAYERS.length;
208
+ for (let i = 0;i < eligibleLayerCount; i++) {
209
+ const layer = OPENCODE_MODEL_VARIANT_LAYERS[i];
210
+ const narrowed = narrowQualifierValue(readOpencodeLayerField(agentOverlay, categoryOverlay, layer, "variant"));
211
+ if (narrowed !== undefined) {
212
+ qualifier = narrowed;
213
+ qualifierSource = layer;
214
+ break;
215
+ }
216
+ }
217
+ }
218
+ return { model, modelSource, qualifier, qualifierSource };
219
+ }
220
+ function resolvePiThinking(agentOverlay, categoryOverlay, piSubagentsOverlays, target) {
221
+ const blockResolution = resolveField([
222
+ {
223
+ value: readBlockField(agentOverlay, "pi", "thinking"),
224
+ source: { level: "agent", form: "block" }
225
+ },
226
+ {
227
+ value: readBlockField(categoryOverlay, "pi", "thinking"),
228
+ source: { level: "category", form: "block" }
229
+ }
230
+ ]);
231
+ const legacyAgentThinking = getOverlayValue(piSubagentsOverlays.agents, target.agentKey)?.thinking;
232
+ const legacyCategoryThinking = getOverlayValue(piSubagentsOverlays.categories, target.category)?.thinking;
233
+ const legacyValue = legacyAgentThinking !== undefined ? legacyAgentThinking : legacyCategoryThinking;
234
+ const legacyPresent = legacyValue !== undefined;
235
+ const narrowedBlockValue = narrowQualifierValue(blockResolution.value);
236
+ if (narrowedBlockValue !== undefined) {
237
+ return {
238
+ value: narrowedBlockValue,
239
+ source: blockResolution.source,
240
+ legacyPresent
241
+ };
242
+ }
243
+ const narrowedLegacyValue = narrowQualifierValue(legacyValue);
244
+ if (narrowedLegacyValue !== undefined) {
245
+ return {
246
+ value: narrowedLegacyValue,
247
+ source: {
248
+ level: legacyAgentThinking !== undefined ? "agent" : "category",
249
+ form: "legacy-pi-subagents"
250
+ },
251
+ legacyPresent
252
+ };
253
+ }
254
+ return { value: undefined, source: undefined, legacyPresent };
255
+ }
256
+ function resolveRouting(input) {
257
+ const { overlays, piSubagentsOverlays, target, harness } = input;
258
+ const agentOverlay = lookupAgentOverlay(overlays, target);
259
+ const categoryOverlay = getOverlayValue(overlays.categories, target.category);
260
+ if (harness === "opencode") {
261
+ const resolution = resolveOpencodeModelAndVariant(agentOverlay, categoryOverlay);
262
+ return {
263
+ model: resolution.model,
264
+ qualifier: resolution.qualifier,
265
+ source: {
266
+ model: resolution.modelSource,
267
+ qualifier: resolution.qualifierSource
268
+ },
269
+ legacyPiSubagentsThinkingPresent: false,
270
+ harness
271
+ };
272
+ }
273
+ const modelResolution = resolveModel(agentOverlay, categoryOverlay, harness);
274
+ const qualifierResolution = resolvePiThinking(agentOverlay, categoryOverlay, piSubagentsOverlays, target);
275
+ return {
276
+ model: modelResolution.value,
277
+ qualifier: qualifierResolution.value,
278
+ source: {
279
+ model: modelResolution.source,
280
+ qualifier: qualifierResolution.source
281
+ },
282
+ legacyPiSubagentsThinkingPresent: qualifierResolution.legacyPresent,
283
+ harness
284
+ };
285
+ }
286
+ function qualifierResolvesWithoutModel(resolution) {
287
+ if (resolution.harness !== "opencode")
288
+ return false;
289
+ return resolution.qualifier !== undefined && resolution.model === undefined;
290
+ }
291
+ function formatWrittenLegacyPiSubagentsThinkingWarning(scope, key) {
292
+ const writtenPath = `pi_subagents.${scope}.${key}.thinking`;
293
+ const replacementPath = `${scope}.${key}.pi.thinking`;
294
+ return `[systematic] ${writtenPath} is deprecated; set ${replacementPath} instead. The legacy value is ` + "honoured only when the new location is unset, and support for it will be removed in a future release.";
295
+ }
296
+ function collectWrittenLegacyPiSubagentsThinkingWarnings(piSubagentsOverlays) {
297
+ const warnings = [];
298
+ for (const scope of ["agents", "categories"]) {
299
+ for (const [key, overlay] of Object.entries(piSubagentsOverlays[scope])) {
300
+ if (!isRecord(overlay.value) || !Object.hasOwn(overlay.value, "thinking")) {
301
+ continue;
302
+ }
303
+ warnings.push(formatWrittenLegacyPiSubagentsThinkingWarning(scope, key));
304
+ }
305
+ }
306
+ return warnings;
307
+ }
308
+
2
309
  // src/lib/config.ts
3
310
  import fs from "fs";
4
311
  import os from "os";
@@ -7301,17 +7608,30 @@ function trustAny(schema) {
7301
7608
  function trustProtected(schema) {
7302
7609
  return schema.meta({ trust: "project-or-higher" });
7303
7610
  }
7304
- function enforceVariantHasExplicitModel(overlay, ctx) {
7305
- if (overlay.variant === undefined)
7306
- return;
7307
- if (typeof overlay.model === "string")
7308
- return;
7309
- ctx.addIssue({
7310
- code: "custom",
7311
- path: ["variant"],
7312
- message: "variant requires a non-null model in the same overlay; remove variant or set model explicitly"
7313
- });
7314
- }
7611
+ var piSubagentsThinkingSchema = _enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]).meta({
7612
+ description: "pi-subagents reasoning effort level for exported persona frontmatter",
7613
+ examples: ["off", "medium", "high"]
7614
+ });
7615
+ var OpencodeHarnessBlockSchema = object({
7616
+ model: trustProtected(modelSchema).optional(),
7617
+ variant: trustProtected(variantSchema).optional()
7618
+ }).strict().meta({
7619
+ description: "OpenCode-specific routing block. When present, model/variant here override the flat model/variant fields for OpenCode only; the flat fields remain the harness-neutral default and still apply to Pi. `variant` is bound to whichever layer supplies `model`: it is used only from that layer or a more specific one, and is dropped when a more specific layer sets (or nulls) `model` without repeating the variant. A `variant` with no `model` anywhere in the merged overlay is a config-load error raised by the routing resolver, not a parse-time error.",
7620
+ examples: [
7621
+ { model: "anthropic/claude-opus-4-7", variant: "v2" },
7622
+ { model: null }
7623
+ ]
7624
+ });
7625
+ var PiHarnessBlockSchema = object({
7626
+ model: trustProtected(modelSchema).optional(),
7627
+ thinking: trustProtected(piSubagentsThinkingSchema).optional()
7628
+ }).strict().meta({
7629
+ description: "Pi-specific routing block. When present, model/thinking here override the flat model field and the legacy `pi_subagents.<name>.thinking` value for Pi only; the flat model field remains the harness-neutral default and still applies to OpenCode. Unlike OpenCode's `variant`, `thinking` is independent of `model`: it applies to whatever model the delegate ends up running, including one inherited from the parent session, so `thinking` with no `model` anywhere in the merged overlay is valid and never a config-load error.",
7630
+ examples: [
7631
+ { model: "anthropic/claude-opus-4-7", thinking: "high" },
7632
+ { model: null }
7633
+ ]
7634
+ });
7315
7635
  var AgentOverlaySchema = object({
7316
7636
  model: trustProtected(modelSchema).optional(),
7317
7637
  variant: trustProtected(variantSchema).optional(),
@@ -7323,14 +7643,20 @@ var AgentOverlaySchema = object({
7323
7643
  hidden: hiddenSchema.optional(),
7324
7644
  disable: disableSchema.optional(),
7325
7645
  skills: trustProtected(skillsSchema).optional(),
7326
- permission: trustProtected(permissionSchema).optional()
7327
- }).strict().superRefine(enforceVariantHasExplicitModel).meta({
7646
+ permission: trustProtected(permissionSchema).optional(),
7647
+ opencode: trustProtected(OpencodeHarnessBlockSchema).optional(),
7648
+ pi: trustProtected(PiHarnessBlockSchema).optional()
7649
+ }).strict().meta({
7328
7650
  description: "Per-agent configuration overlay",
7329
7651
  examples: [
7330
7652
  {
7331
7653
  model: "anthropic/claude-opus-4-7",
7332
7654
  temperature: 0.1,
7333
7655
  mode: "subagent"
7656
+ },
7657
+ {
7658
+ opencode: { model: "anthropic/claude-opus-4-7", variant: "v2" },
7659
+ pi: { model: "anthropic/claude-opus-4-7", thinking: "high" }
7334
7660
  }
7335
7661
  ]
7336
7662
  });
@@ -7344,14 +7670,56 @@ var CategoryOverlaySchema = object({
7344
7670
  steps: stepsSchema.optional(),
7345
7671
  hidden: hiddenSchema.optional(),
7346
7672
  skills: trustProtected(skillsSchema).optional(),
7347
- permission: trustProtected(permissionSchema).optional()
7348
- }).strict().superRefine(enforceVariantHasExplicitModel).meta({
7673
+ permission: trustProtected(permissionSchema).optional(),
7674
+ opencode: trustProtected(OpencodeHarnessBlockSchema).optional(),
7675
+ pi: trustProtected(PiHarnessBlockSchema).optional()
7676
+ }).strict().meta({
7349
7677
  description: "Per-category configuration overlay (same fields as agent minus disable)",
7350
- examples: [{ model: "anthropic/claude-opus-4-7", temperature: 0.1 }]
7678
+ examples: [
7679
+ { model: "anthropic/claude-opus-4-7", temperature: 0.1 },
7680
+ { opencode: { variant: "v2" }, pi: { thinking: "high" } }
7681
+ ]
7351
7682
  });
7352
- var piSubagentsThinkingSchema = _enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]).meta({
7353
- description: "pi-subagents reasoning effort level for exported persona frontmatter",
7354
- examples: ["off", "medium", "high"]
7683
+ var ProfileOverlaySchema = object({
7684
+ model: trustProtected(modelSchema).optional(),
7685
+ variant: trustProtected(variantSchema).optional(),
7686
+ temperature: trustAny(temperatureSchema).optional(),
7687
+ top_p: trustAny(topPSchema).optional(),
7688
+ opencode: trustProtected(OpencodeHarnessBlockSchema).optional(),
7689
+ pi: trustProtected(PiHarnessBlockSchema).optional()
7690
+ }).strict().meta({
7691
+ description: "Routing-only overlay fields permitted inside a named profile bundle entry: model, variant, temperature, top_p, and the opencode/pi harness blocks. Non-routing fields (permission, skills, mode, hidden, disable, steps, color) are rejected.",
7692
+ examples: [
7693
+ { model: "anthropic/claude-opus-4-7" },
7694
+ { opencode: { variant: "v2" }, pi: { thinking: "high" } }
7695
+ ]
7696
+ });
7697
+ function createProfileBundleSchema(agentNames, qualifiedAgentIds) {
7698
+ return object({
7699
+ agents: object(Object.fromEntries([...agentNames, ...qualifiedAgentIds].map((name) => [
7700
+ name,
7701
+ ProfileOverlaySchema.optional()
7702
+ ]))).strict().optional().meta({
7703
+ description: "Per-agent routing overlays for this profile, keyed by bundled agent name (bare or qualified category/name), using the same routing-only field set as every profile entry. Unknown keys are rejected with a Zod parse error, exactly like the top-level `agents` field.",
7704
+ examples: [{ "correctness-reviewer": { model: "openai/gpt-5" } }]
7705
+ }),
7706
+ categories: record(string(), ProfileOverlaySchema).optional().meta({
7707
+ description: "Per-category routing overlays for this profile, keyed by category name, using the same routing-only field set as every profile entry.",
7708
+ examples: [{ review: { model: "anthropic/claude-opus-4-7" } }]
7709
+ })
7710
+ }).strict().meta({
7711
+ description: "A named routing-only overlay bundle. Entries under agents/categories have the same shape as the top-level agents/categories overlays, restricted to routing fields.",
7712
+ examples: [
7713
+ {
7714
+ agents: { fixer: { model: "anthropic/claude-opus-4-7" } },
7715
+ categories: { review: { pi: { thinking: "high" } } }
7716
+ }
7717
+ ]
7718
+ });
7719
+ }
7720
+ var profileSchema = string().min(1).nullable().optional().meta({
7721
+ description: "Selects a named entry from this source's profiles map as the routing overlay bundle to apply. null explicitly selects the base config (no profile), which still wins over a lower-priority source's selection. Omitting the field entirely defers the decision to a lower-priority source.",
7722
+ examples: ["personal", null]
7355
7723
  });
7356
7724
  var piSubagentsMaxTurnsSchema = number().int().nonnegative().meta({
7357
7725
  description: "pi-subagents maximum turns for a delegated persona (0 = unlimited)",
@@ -7444,6 +7812,18 @@ function createSystematicConfigSchema(opts) {
7444
7812
  description: "Per-category configuration overlays keyed by category name",
7445
7813
  examples: [{ review: { model: "anthropic/claude-opus-4-7" } }, {}]
7446
7814
  }),
7815
+ profiles: record(string(), createProfileBundleSchema(agentNames, qualifiedAgentIds)).default({}).meta({
7816
+ description: "Named routing-only overlay bundles, selectable by name via the profile field. Only valid in user config or OPENCODE_CONFIG_DIR config \u2014 a project config may select a profile but may not define this field.",
7817
+ examples: [
7818
+ {
7819
+ personal: {
7820
+ agents: { "correctness-reviewer": { model: "openai/gpt-5" } }
7821
+ }
7822
+ },
7823
+ {}
7824
+ ]
7825
+ }),
7826
+ profile: profileSchema,
7447
7827
  disabled_skills: array(_enum([...skillNames, ...removedSkillNames])).default([]).meta({
7448
7828
  description: "Array of bundled skill names to disable globally. Unknown skill names are rejected at parse time.",
7449
7829
  examples: [["ce:plan", "ce:review"]]
@@ -7494,7 +7874,9 @@ var SECURITY_OVERLAY_FIELDS = [
7494
7874
  "model",
7495
7875
  "variant",
7496
7876
  "skills",
7497
- "permission"
7877
+ "permission",
7878
+ "opencode",
7879
+ "pi"
7498
7880
  ];
7499
7881
 
7500
7882
  // src/lib/config.ts
@@ -7526,22 +7908,42 @@ var PROTECTED_OVERLAY_FIELD_PATHS = {
7526
7908
  model: "agents.*.model",
7527
7909
  permission: "agents.*.permission",
7528
7910
  skills: "agents.*.skills",
7529
- variant: "agents.*.variant"
7911
+ variant: "agents.*.variant",
7912
+ opencode: "agents.*.opencode",
7913
+ pi: "agents.*.pi"
7530
7914
  },
7531
7915
  categories: {
7532
7916
  model: "categories.*.model",
7533
7917
  permission: "categories.*.permission",
7534
7918
  skills: "categories.*.skills",
7535
- variant: "categories.*.variant"
7919
+ variant: "categories.*.variant",
7920
+ opencode: "categories.*.opencode",
7921
+ pi: "categories.*.pi"
7536
7922
  }
7537
7923
  };
7538
- var PROJECT_PROTECTED_FIELDS = new Set(["workflow_guard"]);
7924
+ var PROJECT_PROTECTED_FIELDS = new Set(["workflow_guard", "profiles"]);
7539
7925
  var CURRENT_SKILL_NAMES_SET = new Set(BUNDLED_SKILL_NAMES);
7540
7926
  var CURRENT_AGENT_NAMES_SET = new Set([
7541
7927
  ...BUNDLED_AGENT_NAMES,
7542
7928
  ...BUNDLED_AGENT_QUALIFIED_IDS
7543
7929
  ]);
7544
7930
  var REMOVED_AGENT_CATEGORIES_SET = new Set(REMOVED_BUNDLED_AGENT_CATEGORIES);
7931
+ var BUNDLED_AGENT_CATEGORY_BY_KEY = new Map(BUNDLED_AGENT_QUALIFIED_IDS.map((qualifiedId) => {
7932
+ const separatorIndex = qualifiedId.indexOf("/");
7933
+ return [
7934
+ qualifiedId.slice(separatorIndex + 1),
7935
+ qualifiedId.slice(0, separatorIndex)
7936
+ ];
7937
+ }));
7938
+ var BUNDLED_AGENT_KEYS_BY_CATEGORY = (() => {
7939
+ const byCategory = new Map;
7940
+ for (const [agentKey, category] of BUNDLED_AGENT_CATEGORY_BY_KEY) {
7941
+ const existing = byCategory.get(category) ?? [];
7942
+ existing.push(agentKey);
7943
+ byCategory.set(category, existing);
7944
+ }
7945
+ return byCategory;
7946
+ })();
7545
7947
  function computeDroppedNames(names, allowedSet) {
7546
7948
  return names.filter((n) => !allowedSet.has(n));
7547
7949
  }
@@ -7602,24 +8004,37 @@ function formatUnrecognizedKeysHint(badKeys, topField) {
7602
8004
  const joined = badKeys.map((k) => `'${k}'`).join(", ");
7603
8005
  return `Unrecognized keys ${joined} in \`${topField}\`. These must be bundled names. See ${TYPED_VALIDATION_DOCS_URL} for the full list of valid names.`;
7604
8006
  }
8007
+ function formatProfileAgentUnrecognizedKeysHint(badKeys, profileName) {
8008
+ const prefixed = badKeys.map((k) => `profiles.${profileName}.agents.${k}`);
8009
+ const isAre = prefixed.length === 1 ? "is not a bundled agent" : "are not bundled agents";
8010
+ return `${prefixed.join(", ")} ${isAre}. See ${TYPED_VALIDATION_DOCS_URL} for the full list of valid names.`;
8011
+ }
8012
+ function enrichProfileAgentUnrecognizedKeysIssue(issue) {
8013
+ if (issue.code !== "unrecognized_keys" || issue.path[0] !== "profiles" || issue.path[2] !== "agents" || typeof issue.path[1] !== "string") {
8014
+ return null;
8015
+ }
8016
+ const hint = formatProfileAgentUnrecognizedKeysHint(issue.keys, issue.path[1]);
8017
+ return { ...issue, message: hint };
8018
+ }
8019
+ function enrichTopLevelTypedKeyIssue(issue, rawInput) {
8020
+ const topField = issue.path[0];
8021
+ if (typeof topField !== "string" || !TYPED_KEY_FIELDS.has(topField)) {
8022
+ return null;
8023
+ }
8024
+ if (issue.code === "unrecognized_keys") {
8025
+ const hint = formatUnrecognizedKeysHint(issue.keys, topField);
8026
+ return { ...issue, message: hint };
8027
+ }
8028
+ if (issue.code === "invalid_value" && (topField === "disabled_agents" || topField === "disabled_skills")) {
8029
+ const badValue = resolveValueAtPath(rawInput, issue.path);
8030
+ const kind = topField === "disabled_agents" ? "agent" : "skill";
8031
+ const hint = typeof badValue === "string" ? `Unrecognized ${kind} name '${badValue}' in \`${topField}\`. This must be a bundled name. See ${TYPED_VALIDATION_DOCS_URL} for the full list of valid names.` : `Invalid value in \`${topField}\`. See ${TYPED_VALIDATION_DOCS_URL} for the full list of valid names.`;
8032
+ return { ...issue, message: hint };
8033
+ }
8034
+ return null;
8035
+ }
7605
8036
  function enrichUnrecognizedKeyIssues(issues, rawInput) {
7606
- return issues.map((issue) => {
7607
- const topField = issue.path[0];
7608
- if (typeof topField !== "string" || !TYPED_KEY_FIELDS.has(topField)) {
7609
- return issue;
7610
- }
7611
- if (issue.code === "unrecognized_keys") {
7612
- const hint = formatUnrecognizedKeysHint(issue.keys, topField);
7613
- return { ...issue, message: hint };
7614
- }
7615
- if (issue.code === "invalid_value" && (topField === "disabled_agents" || topField === "disabled_skills")) {
7616
- const badValue = resolveValueAtPath(rawInput, issue.path);
7617
- const kind = topField === "disabled_agents" ? "agent" : "skill";
7618
- const hint = typeof badValue === "string" ? `Unrecognized ${kind} name '${badValue}' in \`${topField}\`. This must be a bundled name. See ${TYPED_VALIDATION_DOCS_URL} for the full list of valid names.` : `Invalid value in \`${topField}\`. See ${TYPED_VALIDATION_DOCS_URL} for the full list of valid names.`;
7619
- return { ...issue, message: hint };
7620
- }
7621
- return issue;
7622
- });
8037
+ return issues.map((issue) => enrichProfileAgentUnrecognizedKeysIssue(issue) ?? enrichTopLevelTypedKeyIssue(issue, rawInput) ?? issue);
7623
8038
  }
7624
8039
  function resolveValueAtPath(root, path2) {
7625
8040
  let current = root;
@@ -7677,6 +8092,7 @@ function loadConfigSource(filePath, trust, invalidSource) {
7677
8092
  return {
7678
8093
  metadata: { kind: trust, presence: "present" },
7679
8094
  source: {
8095
+ kind: "file",
7680
8096
  canonicalPath: resolveConfigSourcePath(filePath),
7681
8097
  config,
7682
8098
  path: filePath,
@@ -7729,6 +8145,13 @@ function collectProjectProtectedFields(rawConfig, trust) {
7729
8145
  sourceKind: "project"
7730
8146
  }
7731
8147
  ] : [],
8148
+ ...Object.hasOwn(rawConfig, "profiles") ? [
8149
+ {
8150
+ fieldPath: "profiles",
8151
+ outcome: "blocked",
8152
+ sourceKind: "project"
8153
+ }
8154
+ ] : [],
7732
8155
  ...collectOverlayProtectedFields(rawConfig.agents, "agents"),
7733
8156
  ...collectOverlayProtectedFields(rawConfig.categories, "categories")
7734
8157
  ];
@@ -7765,6 +8188,92 @@ function mergeArraysUnique(arr1, arr2) {
7765
8188
  set.add(item);
7766
8189
  return Array.from(set);
7767
8190
  }
8191
+ var PROFILE_DOCS_URL = "https://fro.bot/systematic/reference/configuration#profiles";
8192
+ var NO_PROFILE_SELECTION = {
8193
+ activeProfile: null,
8194
+ profileSelectorSource: null,
8195
+ profileFallback: null,
8196
+ bundle: null,
8197
+ bundleSource: null
8198
+ };
8199
+ function resolveProfileSelector(input) {
8200
+ const candidates = [
8201
+ ["custom", input.customConfig?.profile],
8202
+ ["project", input.projectConfig?.profile],
8203
+ ["user", input.userConfig?.profile]
8204
+ ];
8205
+ for (const [source, value] of candidates) {
8206
+ if (value !== undefined)
8207
+ return { value, source };
8208
+ }
8209
+ return null;
8210
+ }
8211
+ function lookupProfileBundle(input, name) {
8212
+ if (input.customSource) {
8213
+ const bundle = input.customSource.config.profiles?.[name];
8214
+ if (bundle !== undefined) {
8215
+ return { bundle, definingSource: input.customSource };
8216
+ }
8217
+ }
8218
+ if (input.userSource) {
8219
+ const bundle = input.userSource.config.profiles?.[name];
8220
+ if (bundle !== undefined) {
8221
+ return { bundle, definingSource: input.userSource };
8222
+ }
8223
+ }
8224
+ return;
8225
+ }
8226
+ function trustedDefaultProfileName(input) {
8227
+ const userDefault = input.userConfig?.profile;
8228
+ return typeof userDefault === "string" ? userDefault : undefined;
8229
+ }
8230
+ function resolveActiveProfile(input) {
8231
+ const selection = resolveProfileSelector(input);
8232
+ if (selection === null)
8233
+ return NO_PROFILE_SELECTION;
8234
+ if (selection.value === null) {
8235
+ return {
8236
+ activeProfile: null,
8237
+ profileSelectorSource: selection.source,
8238
+ profileFallback: null,
8239
+ bundle: null,
8240
+ bundleSource: null
8241
+ };
8242
+ }
8243
+ const requested = selection.value;
8244
+ const requestedLookup = lookupProfileBundle(input, requested);
8245
+ if (requestedLookup !== undefined) {
8246
+ return {
8247
+ activeProfile: requested,
8248
+ profileSelectorSource: selection.source,
8249
+ profileFallback: null,
8250
+ bundle: requestedLookup.bundle,
8251
+ bundleSource: requestedLookup.definingSource
8252
+ };
8253
+ }
8254
+ const trustedDefault = trustedDefaultProfileName(input);
8255
+ const fallbackLookup = trustedDefault !== undefined && trustedDefault !== requested ? lookupProfileBundle(input, trustedDefault) : undefined;
8256
+ if (fallbackLookup !== undefined && trustedDefault !== undefined) {
8257
+ input.warningSink(`[systematic] profile "${requested}" (selected by ${selection.source} config) is not defined in \`profiles\`; falling back to your default profile "${trustedDefault}". See ${PROFILE_DOCS_URL} for how to define a profile.`);
8258
+ return {
8259
+ activeProfile: trustedDefault,
8260
+ profileSelectorSource: selection.source,
8261
+ profileFallback: { requested, usedDefault: trustedDefault },
8262
+ bundle: fallbackLookup.bundle,
8263
+ bundleSource: fallbackLookup.definingSource
8264
+ };
8265
+ }
8266
+ const sourceNote = selection.source === "user" ? "" : ` (selected by ${selection.source} config)`;
8267
+ const alsoMissingNote = trustedDefault !== undefined && trustedDefault !== requested ? ` Your default profile "${trustedDefault}" is also not defined in \`profiles\`.` : trustedDefault === undefined ? " No default profile is configured (`profile` in your user config)." : "";
8268
+ input.warningSink(`[systematic] profile "${requested}"${sourceNote} is not defined in \`profiles\`; using base configuration (no profile).${alsoMissingNote} See ${PROFILE_DOCS_URL} for how to define a profile.`);
8269
+ return {
8270
+ activeProfile: null,
8271
+ profileSelectorSource: selection.source,
8272
+ profileFallback: { requested, usedDefault: null },
8273
+ bundle: null,
8274
+ bundleSource: null
8275
+ };
8276
+ }
7768
8277
  function loadConfig(projectDir, options) {
7769
8278
  return loadConfigWithSources(projectDir, options).config;
7770
8279
  }
@@ -7786,22 +8295,53 @@ function loadConfigWithSources(projectDir, options) {
7786
8295
  const projectSource = project.source;
7787
8296
  const customSource = custom.source;
7788
8297
  const sources = [userSource, projectSource, customSource].filter((source) => source !== null);
7789
- const mergedOverlays = mergeOverlaySources(sources);
8298
+ const userConfig = userSource?.config;
8299
+ const projectConfig = projectSource?.config;
8300
+ const customConfig = customSource?.config;
8301
+ const warned = new Set;
8302
+ if (projectSource?.protectedFields.some((field) => field.fieldPath === "profiles")) {
8303
+ warningSink(`[systematic] \`profiles\` in project config (${projectSource.path}) is only valid in user config or OPENCODE_CONFIG_DIR config and has been ignored. Its bundles are not selectable even if this project also sets \`profile\`.`);
8304
+ }
8305
+ assertAllProfileBundlesAreValid(userConfig, customConfig);
8306
+ const profileSelection = resolveActiveProfile({
8307
+ userConfig,
8308
+ userSource,
8309
+ projectConfig,
8310
+ customConfig,
8311
+ customSource,
8312
+ warningSink
8313
+ });
8314
+ const profileEntry = profileSelection.bundle && profileSelection.bundleSource && typeof profileSelection.activeProfile === "string" ? {
8315
+ kind: "profile-bundle",
8316
+ canonicalPath: profileSelection.bundleSource.canonicalPath,
8317
+ path: profileSelection.bundleSource.path,
8318
+ config: {
8319
+ agents: profileSelection.bundle.agents,
8320
+ categories: profileSelection.bundle.categories
8321
+ },
8322
+ protectedFields: [],
8323
+ profileName: profileSelection.activeProfile
8324
+ } : null;
8325
+ const overlaySources = [
8326
+ userSource,
8327
+ profileEntry,
8328
+ projectSource,
8329
+ customSource
8330
+ ].filter((source) => source !== null);
8331
+ const mergedOverlays = mergeOverlaySources(overlaySources);
7790
8332
  const mergedPiSubagentsOverlays = mergePiSubagentsOverlaySources(sources);
7791
8333
  const droppedCategories = Object.keys(mergedOverlays.categories).filter((name) => REMOVED_AGENT_CATEGORIES_SET.has(name));
7792
- const warned = new Set;
7793
8334
  warnDroppedNames(droppedCategories, "categories", warned, "v3.0.0", warningSink);
7794
8335
  const droppedCategorySet = new Set(droppedCategories);
7795
8336
  const overlays = droppedCategorySet.size === 0 ? mergedOverlays : {
7796
8337
  ...mergedOverlays,
7797
8338
  categories: Object.fromEntries(Object.entries(mergedOverlays.categories).filter(([key]) => !droppedCategorySet.has(key)))
7798
8339
  };
7799
- const userConfig = userSource?.config;
7800
- const projectConfig = projectSource?.config;
7801
- const customConfig = customSource?.config;
8340
+ const mergedDisabledAgents = mergeArraysUnique(mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_agents, userConfig?.disabled_agents), projectConfig?.disabled_agents), customConfig?.disabled_agents);
8341
+ assertRoutingInvariants(overlays, mergedPiSubagentsOverlays, new Set(mergedDisabledAgents), warningSink);
7802
8342
  const result = {
7803
8343
  disabled_skills: mergeArraysUnique(mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_skills, userConfig?.disabled_skills), projectConfig?.disabled_skills), customConfig?.disabled_skills),
7804
- disabled_agents: mergeArraysUnique(mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_agents, userConfig?.disabled_agents), projectConfig?.disabled_agents), customConfig?.disabled_agents),
8344
+ disabled_agents: mergedDisabledAgents,
7805
8345
  disabled_commands: mergeArraysUnique(mergeArraysUnique(mergeArraysUnique(DEFAULT_CONFIG.disabled_commands, userConfig?.disabled_commands), projectConfig?.disabled_commands), customConfig?.disabled_commands),
7806
8346
  bootstrap: {
7807
8347
  ...DEFAULT_CONFIG.bootstrap,
@@ -7837,11 +8377,13 @@ function loadConfigWithSources(projectDir, options) {
7837
8377
  config: effectiveConfig,
7838
8378
  metadata: buildConfigObservationMetadata({
7839
8379
  custom: custom.metadata,
8380
+ profileSelection,
7840
8381
  project: project.metadata,
7841
8382
  sources,
7842
8383
  user: user.metadata
7843
8384
  }),
7844
- overlays
8385
+ overlays,
8386
+ piSubagentsOverlays: mergedPiSubagentsOverlays
7845
8387
  };
7846
8388
  }
7847
8389
  function buildConfigObservationMetadata(summary) {
@@ -7878,7 +8420,10 @@ function buildConfigObservationMetadata(summary) {
7878
8420
  return {
7879
8421
  authorities: sortAuthorities(authorities),
7880
8422
  protectedFields: sortProtectedFields(protectedFields),
7881
- sources
8423
+ sources,
8424
+ activeProfile: summary.profileSelection.activeProfile,
8425
+ profileSelectorSource: summary.profileSelection.profileSelectorSource,
8426
+ profileFallback: summary.profileSelection.profileFallback
7882
8427
  };
7883
8428
  }
7884
8429
  function dedupeSourceMetadata(metadata, sourcePaths) {
@@ -7904,6 +8449,75 @@ function sortAuthorities(authorities) {
7904
8449
  function sortProtectedFields(fields) {
7905
8450
  return [...fields].sort((left, right) => left.fieldPath === right.fieldPath ? left.sourceKind.localeCompare(right.sourceKind) : left.fieldPath.localeCompare(right.fieldPath));
7906
8451
  }
8452
+ function collectRoutingTargets(overlays, disabledAgents = new Set) {
8453
+ const targets = new Map;
8454
+ const isDisabled = (agentKey, category) => {
8455
+ const qualifiedId = `${category}/${agentKey}`;
8456
+ if (disabledAgents.has(agentKey) || disabledAgents.has(qualifiedId)) {
8457
+ return true;
8458
+ }
8459
+ const overlay = overlays.agents[agentKey]?.value ?? overlays.agents[qualifiedId]?.value;
8460
+ return overlay?.disable === true;
8461
+ };
8462
+ const addTarget = (agentKey, category) => {
8463
+ if (isDisabled(agentKey, category))
8464
+ return;
8465
+ targets.set(`${category}/${agentKey}`, { agentKey, category });
8466
+ };
8467
+ for (const rawKey of Object.keys(overlays.agents)) {
8468
+ const separatorIndex = rawKey.indexOf("/");
8469
+ if (separatorIndex === -1) {
8470
+ const category = BUNDLED_AGENT_CATEGORY_BY_KEY.get(rawKey);
8471
+ if (category !== undefined)
8472
+ addTarget(rawKey, category);
8473
+ continue;
8474
+ }
8475
+ const category = rawKey.slice(0, separatorIndex);
8476
+ const agentKey = rawKey.slice(separatorIndex + 1);
8477
+ if (BUNDLED_AGENT_CATEGORY_BY_KEY.get(agentKey) === category) {
8478
+ addTarget(agentKey, category);
8479
+ }
8480
+ }
8481
+ for (const categoryKey of Object.keys(overlays.categories)) {
8482
+ const agentKeys = BUNDLED_AGENT_KEYS_BY_CATEGORY.get(categoryKey);
8483
+ if (agentKeys === undefined)
8484
+ continue;
8485
+ for (const agentKey of agentKeys)
8486
+ addTarget(agentKey, categoryKey);
8487
+ }
8488
+ return Array.from(targets.values());
8489
+ }
8490
+ function assertProfileBundleCategoryKeysAreBundledCategories(profileName, bundle) {
8491
+ for (const categoryKey of Object.keys(bundle.categories ?? {})) {
8492
+ if (!BUNDLED_AGENT_KEYS_BY_CATEGORY.has(categoryKey)) {
8493
+ throw new Error(`Invalid Systematic config: profiles.${profileName}.categories.${categoryKey} is not a bundled agent category. Valid categories: ${Array.from(BUNDLED_AGENT_KEYS_BY_CATEGORY.keys()).join(", ")}`);
8494
+ }
8495
+ }
8496
+ }
8497
+ function assertAllProfileBundlesAreValid(userConfig, customConfig) {
8498
+ for (const config of [userConfig, customConfig]) {
8499
+ for (const [profileName, bundle] of Object.entries(config?.profiles ?? {})) {
8500
+ assertProfileBundleCategoryKeysAreBundledCategories(profileName, bundle);
8501
+ }
8502
+ }
8503
+ }
8504
+ function assertRoutingInvariants(overlays, piSubagentsOverlays, disabledAgents, warningSink) {
8505
+ const targets = collectRoutingTargets(overlays, disabledAgents);
8506
+ for (const target of targets) {
8507
+ const resolution = resolveRouting({
8508
+ overlays,
8509
+ piSubagentsOverlays,
8510
+ target,
8511
+ harness: "opencode"
8512
+ });
8513
+ if (qualifierResolvesWithoutModel(resolution)) {
8514
+ throw new Error(`Invalid Systematic config: agents.${target.agentKey}.variant resolves to "${resolution.qualifier}" on the opencode harness, but no model resolves for agents.${target.agentKey} on opencode at any layer (agent, category, block, or flat). Set a model at the same layer or a lower one, or remove the qualifier.`);
8515
+ }
8516
+ }
8517
+ for (const warning of collectWrittenLegacyPiSubagentsThinkingWarnings(piSubagentsOverlays)) {
8518
+ warningSink(warning);
8519
+ }
8520
+ }
7907
8521
  function mergeOverlaySources(sources) {
7908
8522
  const result = {
7909
8523
  agents: {},
@@ -7923,15 +8537,15 @@ function mergeOverlayMap(target, source, mapKey) {
7923
8537
  throwInvalidOverlay(source.path, mapKey);
7924
8538
  }
7925
8539
  for (const [key, value] of Object.entries(overlayMap)) {
7926
- const keyPath = `${mapKey}.${key}`;
8540
+ const keyPath = source.kind === "profile-bundle" ? `profiles.${source.profileName}.${mapKey}.${key}` : `${mapKey}.${key}`;
7927
8541
  if (!isRecord2(value)) {
7928
8542
  throwInvalidOverlay(source.path, keyPath);
7929
8543
  }
7930
- if (source.trust === "project") {
8544
+ if (source.kind === "file" && source.trust === "project") {
7931
8545
  rejectProjectSecurityOverlay(source.path, keyPath, value);
7932
8546
  }
7933
8547
  const previous = target[key];
7934
- const nextValue = source.trust === "project" && previous ? preserveSecurityFields(previous.value, value) : value;
8548
+ const nextValue = resolveOverlayEntryValue(previous?.value, value, source);
7935
8549
  target[key] = {
7936
8550
  value: nextValue,
7937
8551
  sourcePath: source.path,
@@ -7939,6 +8553,16 @@ function mergeOverlayMap(target, source, mapKey) {
7939
8553
  };
7940
8554
  }
7941
8555
  }
8556
+ function resolveOverlayEntryValue(previous, next, source) {
8557
+ if (!previous)
8558
+ return next;
8559
+ if (source.kind === "profile-bundle") {
8560
+ return mergeProfileOverlayValue(previous, next);
8561
+ }
8562
+ if (source.trust === "project")
8563
+ return preserveSecurityFields(previous, next);
8564
+ return next;
8565
+ }
7942
8566
  function rejectProjectSecurityOverlay(sourcePath, keyPath, value) {
7943
8567
  for (const field of SECURITY_OVERLAY_FIELDS) {
7944
8568
  if (Object.hasOwn(value, field)) {
@@ -7955,6 +8579,18 @@ function preserveSecurityFields(previous, next) {
7955
8579
  }
7956
8580
  return result;
7957
8581
  }
8582
+ var HARNESS_BLOCK_KEYS = ["opencode", "pi"];
8583
+ function mergeProfileOverlayValue(previous, next) {
8584
+ const result = { ...previous, ...next };
8585
+ for (const blockKey of HARNESS_BLOCK_KEYS) {
8586
+ const previousBlock = previous[blockKey];
8587
+ const nextBlock = next[blockKey];
8588
+ if (isRecord2(previousBlock) && isRecord2(nextBlock)) {
8589
+ result[blockKey] = { ...previousBlock, ...nextBlock };
8590
+ }
8591
+ }
8592
+ return result;
8593
+ }
7958
8594
  var PI_SUBAGENTS_PROTECTED_FIELD_SET = new Set(PI_SUBAGENTS_PROTECTED_FIELDS);
7959
8595
  function mergePiSubagentsOverlaySources(sources) {
7960
8596
  const result = {
@@ -8057,114 +8693,6 @@ function parseFrontmatter(content) {
8057
8693
  }
8058
8694
  }
8059
8695
 
8060
- // src/lib/validation.ts
8061
- function isRecord(value) {
8062
- return typeof value === "object" && value !== null && !Array.isArray(value);
8063
- }
8064
- function isPermissionSetting(value) {
8065
- return value === "ask" || value === "allow" || value === "deny";
8066
- }
8067
- function isToolsMap(value) {
8068
- if (!isRecord(value))
8069
- return false;
8070
- return Object.values(value).every((entry) => typeof entry === "boolean");
8071
- }
8072
- function isAgentMode(value) {
8073
- return value === "subagent" || value === "primary" || value === "all";
8074
- }
8075
- function extractSimplePermission(data, key) {
8076
- if (!(key in data))
8077
- return;
8078
- const value = data[key];
8079
- return isPermissionSetting(value) ? value : null;
8080
- }
8081
- function extractBashPermission(data) {
8082
- if (!("bash" in data))
8083
- return;
8084
- const bash = data.bash;
8085
- if (isPermissionSetting(bash))
8086
- return bash;
8087
- if (isRecord(bash)) {
8088
- const entries = Object.entries(bash);
8089
- if (entries.every(([, setting]) => isPermissionSetting(setting))) {
8090
- return Object.fromEntries(entries);
8091
- }
8092
- }
8093
- return null;
8094
- }
8095
- function buildPermissionObject(edit, bash, webfetch, doom_loop, external_directory, task, skill) {
8096
- const permission = {};
8097
- if (edit)
8098
- permission.edit = edit;
8099
- if (bash)
8100
- permission.bash = bash;
8101
- if (webfetch)
8102
- permission.webfetch = webfetch;
8103
- if (doom_loop)
8104
- permission.doom_loop = doom_loop;
8105
- if (external_directory)
8106
- permission.external_directory = external_directory;
8107
- if (task)
8108
- permission.task = task;
8109
- if (skill)
8110
- permission.skill = skill;
8111
- return Object.keys(permission).length > 0 ? permission : undefined;
8112
- }
8113
- function normalizePermission(value) {
8114
- if (!isRecord(value))
8115
- return;
8116
- const bash = extractBashPermission(value);
8117
- if (bash === null)
8118
- return;
8119
- const edit = extractSimplePermission(value, "edit");
8120
- if (edit === null)
8121
- return;
8122
- const webfetch = extractSimplePermission(value, "webfetch");
8123
- if (webfetch === null)
8124
- return;
8125
- const doom_loop = extractSimplePermission(value, "doom_loop");
8126
- if (doom_loop === null)
8127
- return;
8128
- const external_directory = extractSimplePermission(value, "external_directory");
8129
- if (external_directory === null)
8130
- return;
8131
- const task = extractSimplePermission(value, "task");
8132
- if (task === null)
8133
- return;
8134
- const skill = extractSimplePermission(value, "skill");
8135
- if (skill === null)
8136
- return;
8137
- return buildPermissionObject(edit, bash, webfetch, doom_loop, external_directory, task, skill);
8138
- }
8139
- function extractString(data, key, fallback = "") {
8140
- const value = data[key];
8141
- return typeof value === "string" ? value : fallback;
8142
- }
8143
- function extractNonEmptyString(data, key) {
8144
- const value = data[key];
8145
- if (typeof value !== "string")
8146
- return;
8147
- const trimmed = value.trim();
8148
- return trimmed !== "" ? trimmed : undefined;
8149
- }
8150
- function extractNumber(data, key) {
8151
- const value = data[key];
8152
- return typeof value === "number" ? value : undefined;
8153
- }
8154
- function extractBoolean(data, key) {
8155
- const value = data[key];
8156
- if (typeof value === "boolean")
8157
- return value;
8158
- if (typeof value === "string") {
8159
- const normalized = value.trim().toLowerCase();
8160
- if (normalized === "true")
8161
- return true;
8162
- if (normalized === "false")
8163
- return false;
8164
- }
8165
- return;
8166
- }
8167
-
8168
8696
  // src/lib/walk-dir.ts
8169
8697
  import fs2 from "fs";
8170
8698
  import path2 from "path";
@@ -8483,4 +9011,4 @@ function discoverSkills(options) {
8483
9011
  return Array.from(byName.values());
8484
9012
  }
8485
9013
 
8486
- export { parseFrontmatter, parse, parseTree, modify, applyEdits, _isoDateTime, _isoDate, _isoTime, _isoDuration, string, ZodISODateTime, ZodISODate, ZodISOTime, ZodISODuration, number, boolean, array, object, union, discriminatedUnion, record, _enum, literal, AgentOverlaySchema, CategoryOverlaySchema, loadConfig, loadConfigWithSources, getConfigPaths, isRecord, extractString, isDiscoverableMarkdown, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, findSkillsInDir, discoverSkills };
9014
+ export { parseFrontmatter, parse, parseTree, modify, applyEdits, _isoDateTime, _isoDate, _isoTime, _isoDuration, string, ZodISODateTime, ZodISODate, ZodISOTime, ZodISODuration, number, boolean, array, object, union, discriminatedUnion, record, _enum, literal, AgentOverlaySchema, CategoryOverlaySchema, isRecord, extractString, toSourcedPiSubagentsOverlays, resolveRouting, loadConfig, loadConfigWithSources, collectRoutingTargets, getConfigPaths, isDiscoverableMarkdown, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, findSkillsInDir, discoverSkills };