@fro.bot/systematic 3.15.1 → 3.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +205 -36
- package/dist/{index-7cyxcwf5.js → index-ae6mhwth.js} +688 -160
- package/dist/index.js +42 -29
- package/dist/lib/agent-resolver.d.ts +7 -1
- package/dist/lib/capability-snapshot.d.ts +1 -1
- package/dist/lib/config-schema.d.ts +63 -1
- package/dist/lib/config.d.ts +59 -1
- package/dist/lib/pi-delegate-session.d.ts +2 -0
- package/dist/lib/pi-delegate-tool.d.ts +33 -1
- package/dist/lib/routing-resolver.d.ts +151 -0
- package/dist/pi.js +7496 -252
- package/dist/schemas/systematic-config.schema.json +1027 -185
- package/package.json +4 -4
|
@@ -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
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: [
|
|
7678
|
+
examples: [
|
|
7679
|
+
{ model: "anthropic/claude-opus-4-7", temperature: 0.1 },
|
|
7680
|
+
{ opencode: { variant: "v2" }, pi: { thinking: "high" } }
|
|
7681
|
+
]
|
|
7351
7682
|
});
|
|
7352
|
-
var
|
|
7353
|
-
|
|
7354
|
-
|
|
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
|
|
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
|
|
7800
|
-
|
|
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:
|
|
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 =
|
|
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,
|
|
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 };
|