@fro.bot/systematic 3.6.2 → 3.8.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.d.ts +18 -0
- package/dist/cli.js +1071 -127
- package/dist/{index-1mb4baxr.js → index-hjbt4p5s.js} +335 -27
- package/dist/index.js +96 -222
- package/dist/lib/capability-snapshot.d.ts +195 -0
- package/dist/lib/config-schema.d.ts +1 -1
- package/dist/lib/config.d.ts +40 -2
- package/dist/lib/discovered-skills.d.ts +3 -0
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
import {
|
|
4
4
|
applyEdits,
|
|
5
|
+
discoverSkills,
|
|
6
|
+
extractString,
|
|
5
7
|
findAgentsInDir,
|
|
6
8
|
findCommandsInDir,
|
|
7
9
|
findSkillsInDir,
|
|
@@ -11,22 +13,781 @@ import {
|
|
|
11
13
|
parse,
|
|
12
14
|
parseFrontmatter,
|
|
13
15
|
parseTree
|
|
14
|
-
} from "./index-
|
|
16
|
+
} from "./index-hjbt4p5s.js";
|
|
15
17
|
|
|
16
18
|
// src/cli.ts
|
|
17
|
-
import
|
|
19
|
+
import fs5 from "fs";
|
|
20
|
+
import os2 from "os";
|
|
18
21
|
import path4 from "path";
|
|
19
22
|
|
|
23
|
+
// src/lib/agent-resolver.ts
|
|
24
|
+
import fs from "fs";
|
|
25
|
+
function buildAgentCatalog(agentsDir) {
|
|
26
|
+
const infos = findAgentsInDir(agentsDir);
|
|
27
|
+
const byName = new Map;
|
|
28
|
+
const entries = [];
|
|
29
|
+
for (const info of infos) {
|
|
30
|
+
const content = fs.readFileSync(info.file, "utf8");
|
|
31
|
+
const entry = parseValidatedAgentEntry(content, info.file);
|
|
32
|
+
try {
|
|
33
|
+
resolveToolAllowlist(entry.toolsSource);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
36
|
+
throw new Error(`Agent file "${info.file}": ${message}`);
|
|
37
|
+
}
|
|
38
|
+
const existing = byName.get(entry.name);
|
|
39
|
+
if (existing) {
|
|
40
|
+
existing.push(info.category ?? "(root)");
|
|
41
|
+
} else {
|
|
42
|
+
byName.set(entry.name, [info.category ?? "(root)"]);
|
|
43
|
+
}
|
|
44
|
+
entries.push(entry);
|
|
45
|
+
}
|
|
46
|
+
const duplicates = [...byName.entries()].filter(([, cats]) => cats.length > 1);
|
|
47
|
+
if (duplicates.length > 0) {
|
|
48
|
+
const detail = duplicates.map(([name, cats]) => `"${name}" (categories: ${cats.join(", ")})`).join("; ");
|
|
49
|
+
throw new Error(`Duplicate persona name(s) detected while flattening the agent catalog: ${detail}. ` + "Persona names must be unique across categories once category is dropped.");
|
|
50
|
+
}
|
|
51
|
+
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
52
|
+
}
|
|
53
|
+
function parseValidatedAgentEntry(content, sourceFile) {
|
|
54
|
+
const { data, body, parseError } = parseFrontmatter(content);
|
|
55
|
+
if (parseError) {
|
|
56
|
+
throw new Error(`Failed to parse YAML frontmatter in agent file "${sourceFile}".`);
|
|
57
|
+
}
|
|
58
|
+
const name = extractString(data, "name");
|
|
59
|
+
if (name.trim() === "") {
|
|
60
|
+
throw new Error(`Agent file "${sourceFile}" is missing a non-empty "name" in its frontmatter.`);
|
|
61
|
+
}
|
|
62
|
+
const description = extractString(data, "description");
|
|
63
|
+
if (description.trim() === "") {
|
|
64
|
+
throw new Error(`Agent file "${sourceFile}" is missing a non-empty "description" in its frontmatter.`);
|
|
65
|
+
}
|
|
66
|
+
const prompt = body.trim();
|
|
67
|
+
if (prompt === "") {
|
|
68
|
+
throw new Error(`Agent file "${sourceFile}" has an empty persona body (system prompt).`);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
name,
|
|
72
|
+
description,
|
|
73
|
+
body: prompt,
|
|
74
|
+
toolsSource: extractRawToolsSource(data, sourceFile)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function extractRawToolsSource(data, sourceFile) {
|
|
78
|
+
if (!("tools" in data))
|
|
79
|
+
return;
|
|
80
|
+
const value = data.tools;
|
|
81
|
+
if (typeof value !== "string") {
|
|
82
|
+
throw new Error(`Agent file "${sourceFile}" has a "tools" frontmatter value that is not a string; expected a comma-separated list (e.g. "Read, Grep, Glob, Bash").`);
|
|
83
|
+
}
|
|
84
|
+
const trimmed = value.trim();
|
|
85
|
+
if (trimmed === "") {
|
|
86
|
+
throw new Error(`Agent file "${sourceFile}" declares an empty "tools" frontmatter value.`);
|
|
87
|
+
}
|
|
88
|
+
return trimmed;
|
|
89
|
+
}
|
|
90
|
+
var DEFAULT_READONLY_TOOLS = ["read", "grep", "find", "ls"];
|
|
91
|
+
var OPENCODE_TO_PI_TOOL = {
|
|
92
|
+
Read: "read",
|
|
93
|
+
Grep: "grep",
|
|
94
|
+
Glob: "find",
|
|
95
|
+
Bash: "bash",
|
|
96
|
+
Edit: "edit",
|
|
97
|
+
Write: "write"
|
|
98
|
+
};
|
|
99
|
+
function unknownDeclaredToolError(toolName) {
|
|
100
|
+
const error = new Error(`Unknown declared tool "${toolName}" in persona frontmatter; refusing to map ` + "to a Pi built-in (fail-closed). Known tools: " + `${Object.keys(OPENCODE_TO_PI_TOOL).join(", ")}.`);
|
|
101
|
+
error.name = "UnknownDeclaredToolError";
|
|
102
|
+
return error;
|
|
103
|
+
}
|
|
104
|
+
function resolveToolAllowlist(toolsSource) {
|
|
105
|
+
if (toolsSource === undefined) {
|
|
106
|
+
return { tools: [...DEFAULT_READONLY_TOOLS] };
|
|
107
|
+
}
|
|
108
|
+
const declared = toolsSource.split(",").map((t) => t.trim()).filter((t) => t !== "");
|
|
109
|
+
const mapped = [];
|
|
110
|
+
for (const name of declared) {
|
|
111
|
+
if (name === "Task") {
|
|
112
|
+
throw unknownDeclaredToolError(name);
|
|
113
|
+
}
|
|
114
|
+
const piName = OPENCODE_TO_PI_TOOL[name];
|
|
115
|
+
if (!piName) {
|
|
116
|
+
throw unknownDeclaredToolError(name);
|
|
117
|
+
}
|
|
118
|
+
mapped.push(piName);
|
|
119
|
+
}
|
|
120
|
+
return { tools: mapped };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/lib/capability-snapshot.ts
|
|
124
|
+
var CAPABILITY_SNAPSHOT_SCHEMA_VERSION = "cli-capabilities.v1";
|
|
125
|
+
var CAPABILITY_SNAPSHOT_COMMAND = "systematic capabilities";
|
|
126
|
+
var CAPABILITY_SOURCE_IDS = [
|
|
127
|
+
"config:custom",
|
|
128
|
+
"config:global",
|
|
129
|
+
"config:project",
|
|
130
|
+
"config:user",
|
|
131
|
+
"discovery:agents",
|
|
132
|
+
"discovery:skills",
|
|
133
|
+
"host:runtime",
|
|
134
|
+
"package"
|
|
135
|
+
];
|
|
136
|
+
var CONFIG_SOURCE_KINDS = ["custom", "project", "user"];
|
|
137
|
+
var CONFIG_AUTHORITY_FIELD_PATHS = [
|
|
138
|
+
"bootstrap.enabled",
|
|
139
|
+
"bootstrap.file",
|
|
140
|
+
"skills_as_commands",
|
|
141
|
+
"workflow_guard.debug",
|
|
142
|
+
"workflow_guard.mode"
|
|
143
|
+
];
|
|
144
|
+
var CONFIG_PROTECTED_FIELD_PATHS = [
|
|
145
|
+
"workflow_guard",
|
|
146
|
+
"agents.*.model",
|
|
147
|
+
"agents.*.permission",
|
|
148
|
+
"agents.*.skills",
|
|
149
|
+
"agents.*.variant",
|
|
150
|
+
"categories.*.model",
|
|
151
|
+
"categories.*.permission",
|
|
152
|
+
"categories.*.skills",
|
|
153
|
+
"categories.*.variant"
|
|
154
|
+
];
|
|
155
|
+
var CONFIG_SOURCE_ERROR_CODES = [
|
|
156
|
+
"parse-failed",
|
|
157
|
+
"read-failed",
|
|
158
|
+
"schema-invalid",
|
|
159
|
+
"source-invalid"
|
|
160
|
+
];
|
|
161
|
+
var CONFIG_SOURCE_KIND_SET = new Set(CONFIG_SOURCE_KINDS);
|
|
162
|
+
var CONFIG_AUTHORITY_FIELD_PATH_SET = new Set(CONFIG_AUTHORITY_FIELD_PATHS);
|
|
163
|
+
var CONFIG_PROTECTED_FIELD_PATH_SET = new Set(CONFIG_PROTECTED_FIELD_PATHS);
|
|
164
|
+
var CONFIG_SOURCE_ERROR_CODE_SET = new Set(CONFIG_SOURCE_ERROR_CODES);
|
|
165
|
+
var CAPABILITY_SOURCE_PRESENCE = ["absent", "invalid", "present"];
|
|
166
|
+
var CAPABILITY_STATUSES = ["available", "unknown", "unavailable"];
|
|
167
|
+
var CAPABILITY_FACT_IDS = [
|
|
168
|
+
"config-authority",
|
|
169
|
+
"config-field-authority",
|
|
170
|
+
"config-protected-field",
|
|
171
|
+
"discovery-summary",
|
|
172
|
+
"discovery-source-issue",
|
|
173
|
+
"host-runtime"
|
|
174
|
+
];
|
|
175
|
+
var CAPABILITY_DISCOVERY_IDS = ["agents", "skills"];
|
|
176
|
+
var CAPABILITY_LIMITATION_CODES = [
|
|
177
|
+
"authority-unproven",
|
|
178
|
+
"discovery-not-collected",
|
|
179
|
+
"host-runtime-unobservable"
|
|
180
|
+
];
|
|
181
|
+
var CAPABILITY_ERROR_CODES = [
|
|
182
|
+
"source-malformed",
|
|
183
|
+
"source-read-failed",
|
|
184
|
+
"source-unsupported",
|
|
185
|
+
"structural-invalid"
|
|
186
|
+
];
|
|
187
|
+
var CAPABILITY_SOURCE_ID_SET = new Set(CAPABILITY_SOURCE_IDS);
|
|
188
|
+
var CAPABILITY_SOURCE_PRESENCE_SET = new Set(CAPABILITY_SOURCE_PRESENCE);
|
|
189
|
+
var CAPABILITY_STATUS_SET = new Set(CAPABILITY_STATUSES);
|
|
190
|
+
var CAPABILITY_FACT_ID_SET = new Set(CAPABILITY_FACT_IDS);
|
|
191
|
+
var CAPABILITY_DISCOVERY_ID_SET = new Set(CAPABILITY_DISCOVERY_IDS);
|
|
192
|
+
var CAPABILITY_LIMITATION_CODE_SET = new Set(CAPABILITY_LIMITATION_CODES);
|
|
193
|
+
var CAPABILITY_ERROR_CODE_SET = new Set(CAPABILITY_ERROR_CODES);
|
|
194
|
+
var MAX_DISPLAY_ID_LENGTH = 128;
|
|
195
|
+
var MAX_PACKAGE_NAME_LENGTH = 128;
|
|
196
|
+
var MAX_PACKAGE_VERSION_LENGTH = 64;
|
|
197
|
+
var MAX_DISCOVERY_COUNT = 1e5;
|
|
198
|
+
var MAX_DISCOVERY_ROOTS = 32;
|
|
199
|
+
function compareText(left, right) {
|
|
200
|
+
if (left < right)
|
|
201
|
+
return -1;
|
|
202
|
+
if (left > right)
|
|
203
|
+
return 1;
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
function isRecord(value) {
|
|
207
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
208
|
+
}
|
|
209
|
+
function assertRecord(value, label) {
|
|
210
|
+
if (!isRecord(value))
|
|
211
|
+
throw new TypeError(`${label} must be an object`);
|
|
212
|
+
}
|
|
213
|
+
function assertAllowedKeys(value, allowedKeys, label) {
|
|
214
|
+
const allowed = new Set(allowedKeys);
|
|
215
|
+
for (const key of Object.keys(value)) {
|
|
216
|
+
if (!allowed.has(key))
|
|
217
|
+
throw new TypeError(`${label} has unknown key ${key}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function assertString(value, label) {
|
|
221
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
222
|
+
throw new TypeError(`${label} must be a non-empty string`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function assertEnum(value, values, label) {
|
|
226
|
+
if (typeof value !== "string" || !values.has(value)) {
|
|
227
|
+
throw new TypeError(`${label} is not an allowed value`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function normalizeDisplayId(value, label) {
|
|
231
|
+
assertString(value, label);
|
|
232
|
+
const normalized = value.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
233
|
+
if (normalized.length > MAX_DISPLAY_ID_LENGTH || normalized.startsWith("/") || normalized.includes("..") || !/^[A-Za-z0-9._:-]+(?:\/[A-Za-z0-9._:-]+)*$/.test(normalized)) {
|
|
234
|
+
throw new TypeError(`${label} must be a bounded relative display ID`);
|
|
235
|
+
}
|
|
236
|
+
return normalized;
|
|
237
|
+
}
|
|
238
|
+
function normalizePackageName(value) {
|
|
239
|
+
assertString(value, "package.name");
|
|
240
|
+
if (value.length > MAX_PACKAGE_NAME_LENGTH || !/^@?[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)?$/.test(value)) {
|
|
241
|
+
throw new TypeError("package.name is not an allowed package identity");
|
|
242
|
+
}
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
function normalizePackageVersion(value) {
|
|
246
|
+
assertString(value, "package.version");
|
|
247
|
+
if (value.length > MAX_PACKAGE_VERSION_LENGTH || !/^[A-Za-z0-9][A-Za-z0-9.+_-]*$/.test(value)) {
|
|
248
|
+
throw new TypeError("package.version is not an allowed version identity");
|
|
249
|
+
}
|
|
250
|
+
return value;
|
|
251
|
+
}
|
|
252
|
+
function normalizeExecutable(value) {
|
|
253
|
+
assertString(value, "argv[0]");
|
|
254
|
+
const segments = value.replaceAll("\\", "/").split("/");
|
|
255
|
+
const executable = segments.at(-1) ?? "";
|
|
256
|
+
if (executable.length === 0 || executable.length > MAX_DISPLAY_ID_LENGTH || !/^[A-Za-z0-9._:-]+$/.test(executable)) {
|
|
257
|
+
throw new TypeError("argv[0] is not an allowed executable identity");
|
|
258
|
+
}
|
|
259
|
+
return executable;
|
|
260
|
+
}
|
|
261
|
+
function normalizeArgv(argv) {
|
|
262
|
+
if (argv.length < 2) {
|
|
263
|
+
throw new TypeError("argv must include an executable and capabilities command");
|
|
264
|
+
}
|
|
265
|
+
const executable = normalizeExecutable(argv[0] ?? "");
|
|
266
|
+
if (argv[1] !== "capabilities") {
|
|
267
|
+
throw new TypeError("argv must identify the capabilities command");
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
executable,
|
|
271
|
+
subcommand: "capabilities"
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function canonicalizePath(value) {
|
|
275
|
+
assertString(value, "root.path");
|
|
276
|
+
const normalizedSeparators = value.replaceAll("\\", "/");
|
|
277
|
+
const absolute = normalizedSeparators.startsWith("/");
|
|
278
|
+
const segments = [];
|
|
279
|
+
for (const segment of normalizedSeparators.split("/")) {
|
|
280
|
+
if (segment === "" || segment === ".")
|
|
281
|
+
continue;
|
|
282
|
+
if (segment === "..") {
|
|
283
|
+
if (segments.length > 0 && segments.at(-1) !== "..")
|
|
284
|
+
segments.pop();
|
|
285
|
+
else if (!absolute)
|
|
286
|
+
segments.push(segment);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
segments.push(segment);
|
|
290
|
+
}
|
|
291
|
+
const joined = segments.join("/");
|
|
292
|
+
return absolute ? `/${joined}` : joined;
|
|
293
|
+
}
|
|
294
|
+
function normalizeObservedAt(options) {
|
|
295
|
+
const observedAt = options.observedAt;
|
|
296
|
+
if (observedAt !== undefined) {
|
|
297
|
+
assertString(observedAt, "observedAt");
|
|
298
|
+
const parsed = new Date(observedAt);
|
|
299
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
300
|
+
throw new TypeError("observedAt must be a valid timestamp");
|
|
301
|
+
}
|
|
302
|
+
return parsed.toISOString();
|
|
303
|
+
}
|
|
304
|
+
const now = options.clock?.() ?? Date.now();
|
|
305
|
+
const timestamp = now instanceof Date ? now : new Date(now);
|
|
306
|
+
if (Number.isNaN(timestamp.getTime())) {
|
|
307
|
+
throw new TypeError("clock must return a valid timestamp");
|
|
308
|
+
}
|
|
309
|
+
return timestamp.toISOString();
|
|
310
|
+
}
|
|
311
|
+
function normalizePackage(value) {
|
|
312
|
+
assertRecord(value, "package");
|
|
313
|
+
assertAllowedKeys(value, ["name", "version"], "package");
|
|
314
|
+
return {
|
|
315
|
+
name: normalizePackageName(value.name),
|
|
316
|
+
version: normalizePackageVersion(value.version)
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function normalizeRoots(roots) {
|
|
320
|
+
const normalized = roots.map((root, index) => {
|
|
321
|
+
assertRecord(root, `roots[${index}]`);
|
|
322
|
+
assertAllowedKeys(root, ["id", "path"], `roots[${index}]`);
|
|
323
|
+
const id = normalizeDisplayId(root.id, `roots[${index}].id`);
|
|
324
|
+
const path = canonicalizePath(root.path);
|
|
325
|
+
return { id, path };
|
|
326
|
+
});
|
|
327
|
+
normalized.sort((left, right) => {
|
|
328
|
+
const pathOrder = compareText(left.path, right.path);
|
|
329
|
+
return pathOrder !== 0 ? pathOrder : compareText(left.id, right.id);
|
|
330
|
+
});
|
|
331
|
+
const unique = [];
|
|
332
|
+
let previousPath;
|
|
333
|
+
for (const root of normalized) {
|
|
334
|
+
if (root.path === previousPath)
|
|
335
|
+
continue;
|
|
336
|
+
previousPath = root.path;
|
|
337
|
+
unique.push({ id: root.id });
|
|
338
|
+
}
|
|
339
|
+
unique.sort((left, right) => compareText(left.id, right.id));
|
|
340
|
+
return unique;
|
|
341
|
+
}
|
|
342
|
+
function normalizeSources(sources) {
|
|
343
|
+
const normalized = sources.map(normalizeSource);
|
|
344
|
+
normalized.sort(compareSources);
|
|
345
|
+
const unique = [];
|
|
346
|
+
const seen = new Set;
|
|
347
|
+
for (const source of normalized) {
|
|
348
|
+
const key = source.canonicalPath ?? `source:${source.sourceId}`;
|
|
349
|
+
if (seen.has(key))
|
|
350
|
+
continue;
|
|
351
|
+
seen.add(key);
|
|
352
|
+
unique.push(source);
|
|
353
|
+
}
|
|
354
|
+
unique.sort((left, right) => compareText(left.sourceId, right.sourceId));
|
|
355
|
+
return unique.map(({ canonicalPath: _canonicalPath, ...source }) => source);
|
|
356
|
+
}
|
|
357
|
+
function compareSources(left, right) {
|
|
358
|
+
const pathOrder = compareText(left.canonicalPath ?? "", right.canonicalPath ?? "");
|
|
359
|
+
if (pathOrder !== 0)
|
|
360
|
+
return pathOrder;
|
|
361
|
+
const sourceOrder = compareText(left.sourceId, right.sourceId);
|
|
362
|
+
if (sourceOrder !== 0)
|
|
363
|
+
return sourceOrder;
|
|
364
|
+
return compareText(left.presence, right.presence);
|
|
365
|
+
}
|
|
366
|
+
function normalizeSource(source, index) {
|
|
367
|
+
assertRecord(source, `sources[${index}]`);
|
|
368
|
+
assertAllowedKeys(source, ["errorCode", "path", "presence", "sourceId", "sourceKind"], `sources[${index}]`);
|
|
369
|
+
assertEnum(source.sourceId, CAPABILITY_SOURCE_ID_SET, `sources[${index}].sourceId`);
|
|
370
|
+
assertEnum(source.presence, CAPABILITY_SOURCE_PRESENCE_SET, `sources[${index}].presence`);
|
|
371
|
+
assertSourceKind(source, index);
|
|
372
|
+
const canonicalPath = source.path === undefined ? undefined : canonicalizePath(source.path);
|
|
373
|
+
if (source.presence === "invalid") {
|
|
374
|
+
assertEnum(source.errorCode, CAPABILITY_ERROR_CODE_SET, `sources[${index}].errorCode`);
|
|
375
|
+
return {
|
|
376
|
+
errorCode: source.errorCode,
|
|
377
|
+
kind: "source",
|
|
378
|
+
presence: source.presence,
|
|
379
|
+
sourceId: source.sourceId,
|
|
380
|
+
...source.sourceKind === undefined ? {} : { sourceKind: source.sourceKind },
|
|
381
|
+
canonicalPath
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (source.errorCode !== undefined) {
|
|
385
|
+
throw new TypeError(`sources[${index}].errorCode is only valid for invalid sources`);
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
kind: "source",
|
|
389
|
+
presence: source.presence,
|
|
390
|
+
sourceId: source.sourceId,
|
|
391
|
+
...source.sourceKind === undefined ? {} : { sourceKind: source.sourceKind },
|
|
392
|
+
canonicalPath
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function assertSourceKind(source, index) {
|
|
396
|
+
if (source.sourceKind === undefined)
|
|
397
|
+
return;
|
|
398
|
+
assertEnum(source.sourceKind, CONFIG_SOURCE_KIND_SET, `sources[${index}].sourceKind`);
|
|
399
|
+
if (source.sourceId !== configSourceId(source.sourceKind)) {
|
|
400
|
+
throw new TypeError(`sources[${index}].sourceKind does not match sourceId`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function configSourceId(sourceKind) {
|
|
404
|
+
return `config:${sourceKind}`;
|
|
405
|
+
}
|
|
406
|
+
function configErrorCode(errorCode) {
|
|
407
|
+
return errorCode === "read-failed" ? "source-read-failed" : "source-malformed";
|
|
408
|
+
}
|
|
409
|
+
function normalizeConfigObservation(config) {
|
|
410
|
+
assertRecord(config, "config");
|
|
411
|
+
assertAllowedKeys(config, ["authorities", "protectedFields", "sources"], "config");
|
|
412
|
+
if (!Array.isArray(config.sources)) {
|
|
413
|
+
throw new TypeError("config.sources must be an array");
|
|
414
|
+
}
|
|
415
|
+
if (!Array.isArray(config.authorities)) {
|
|
416
|
+
throw new TypeError("config.authorities must be an array");
|
|
417
|
+
}
|
|
418
|
+
if (!Array.isArray(config.protectedFields)) {
|
|
419
|
+
throw new TypeError("config.protectedFields must be an array");
|
|
420
|
+
}
|
|
421
|
+
const sources = config.sources.map((source, index) => {
|
|
422
|
+
assertRecord(source, `config.sources[${index}]`);
|
|
423
|
+
assertAllowedKeys(source, ["errorCode", "kind", "path", "presence"], `config.sources[${index}]`);
|
|
424
|
+
assertEnum(source.kind, CONFIG_SOURCE_KIND_SET, `config.sources[${index}].kind`);
|
|
425
|
+
assertEnum(source.presence, CAPABILITY_SOURCE_PRESENCE_SET, `config.sources[${index}].presence`);
|
|
426
|
+
const pathValue = source.path;
|
|
427
|
+
if (pathValue !== undefined) {
|
|
428
|
+
assertString(pathValue, `config.sources[${index}].path`);
|
|
429
|
+
canonicalizePath(pathValue);
|
|
430
|
+
}
|
|
431
|
+
if (source.errorCode !== undefined) {
|
|
432
|
+
assertEnum(source.errorCode, CONFIG_SOURCE_ERROR_CODE_SET, `config.sources[${index}].errorCode`);
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
...source.errorCode === undefined ? {} : { errorCode: configErrorCode(source.errorCode) },
|
|
436
|
+
...pathValue === undefined ? {} : { path: pathValue },
|
|
437
|
+
presence: source.presence,
|
|
438
|
+
sourceId: configSourceId(source.kind),
|
|
439
|
+
sourceKind: source.kind
|
|
440
|
+
};
|
|
441
|
+
});
|
|
442
|
+
const facts = [];
|
|
443
|
+
for (const source of sources) {
|
|
444
|
+
if (source.presence !== "invalid" || source.errorCode === undefined)
|
|
445
|
+
continue;
|
|
446
|
+
facts.push({
|
|
447
|
+
errorCode: source.errorCode,
|
|
448
|
+
factId: "config-authority",
|
|
449
|
+
sourceId: source.sourceId,
|
|
450
|
+
status: "unavailable"
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
config.authorities.forEach((authority, index) => {
|
|
454
|
+
assertRecord(authority, `config.authorities[${index}]`);
|
|
455
|
+
assertAllowedKeys(authority, ["fieldPath", "sourceKind"], `config.authorities[${index}]`);
|
|
456
|
+
assertEnum(authority.fieldPath, CONFIG_AUTHORITY_FIELD_PATH_SET, `config.authorities[${index}].fieldPath`);
|
|
457
|
+
assertEnum(authority.sourceKind, CONFIG_SOURCE_KIND_SET, `config.authorities[${index}].sourceKind`);
|
|
458
|
+
facts.push({
|
|
459
|
+
factId: "config-field-authority",
|
|
460
|
+
fieldPath: authority.fieldPath,
|
|
461
|
+
kind: "authority",
|
|
462
|
+
sourceId: configSourceId(authority.sourceKind),
|
|
463
|
+
status: "available"
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
config.protectedFields.forEach((protectedField, index) => {
|
|
467
|
+
assertRecord(protectedField, `config.protectedFields[${index}]`);
|
|
468
|
+
assertAllowedKeys(protectedField, ["fieldPath", "outcome", "sourceKind"], `config.protectedFields[${index}]`);
|
|
469
|
+
assertEnum(protectedField.fieldPath, CONFIG_PROTECTED_FIELD_PATH_SET, `config.protectedFields[${index}].fieldPath`);
|
|
470
|
+
assertEnum(protectedField.sourceKind, CONFIG_SOURCE_KIND_SET, `config.protectedFields[${index}].sourceKind`);
|
|
471
|
+
if (protectedField.outcome !== "blocked") {
|
|
472
|
+
throw new TypeError(`config.protectedFields[${index}].outcome must be blocked`);
|
|
473
|
+
}
|
|
474
|
+
facts.push({
|
|
475
|
+
factId: "config-protected-field",
|
|
476
|
+
fieldPath: protectedField.fieldPath,
|
|
477
|
+
kind: "protection",
|
|
478
|
+
outcome: "blocked",
|
|
479
|
+
sourceId: configSourceId(protectedField.sourceKind),
|
|
480
|
+
status: "available"
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
return { facts, sources };
|
|
484
|
+
}
|
|
485
|
+
function normalizeFact(value, label) {
|
|
486
|
+
assertRecord(value, label);
|
|
487
|
+
assertAllowedKeys(value, [
|
|
488
|
+
"errorCode",
|
|
489
|
+
"factId",
|
|
490
|
+
"fieldPath",
|
|
491
|
+
"kind",
|
|
492
|
+
"limitationCode",
|
|
493
|
+
"outcome",
|
|
494
|
+
"sourceId",
|
|
495
|
+
"status",
|
|
496
|
+
"count",
|
|
497
|
+
"discoveryId",
|
|
498
|
+
"winningRoots"
|
|
499
|
+
], label);
|
|
500
|
+
assertEnum(value.factId, CAPABILITY_FACT_ID_SET, `${label}.factId`);
|
|
501
|
+
if (value.factId === "config-field-authority") {
|
|
502
|
+
return normalizeAuthorityFact(value, label);
|
|
503
|
+
}
|
|
504
|
+
if (value.factId === "config-protected-field") {
|
|
505
|
+
return normalizeProtectionFact(value, label);
|
|
506
|
+
}
|
|
507
|
+
if (value.factId === "discovery-summary") {
|
|
508
|
+
return normalizeDiscoveryFact(value, label);
|
|
509
|
+
}
|
|
510
|
+
if (value.factId === "discovery-source-issue") {
|
|
511
|
+
return normalizeDiscoverySourceIssueFact(value, label);
|
|
512
|
+
}
|
|
513
|
+
return normalizeStatusFact(value, label);
|
|
514
|
+
}
|
|
515
|
+
function normalizeAuthorityFact(value, label) {
|
|
516
|
+
assertNoDiscoveryFields(value, label);
|
|
517
|
+
assertEnum(value.fieldPath, CONFIG_AUTHORITY_FIELD_PATH_SET, `${label}.fieldPath`);
|
|
518
|
+
if (value.kind !== "authority") {
|
|
519
|
+
throw new TypeError(`${label}.kind must be authority`);
|
|
520
|
+
}
|
|
521
|
+
assertAvailableFactSource(value, label);
|
|
522
|
+
return {
|
|
523
|
+
factId: "config-field-authority",
|
|
524
|
+
fieldPath: value.fieldPath,
|
|
525
|
+
kind: "authority",
|
|
526
|
+
sourceId: value.sourceId,
|
|
527
|
+
status: "available"
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function normalizeProtectionFact(value, label) {
|
|
531
|
+
assertNoDiscoveryFields(value, label);
|
|
532
|
+
assertEnum(value.fieldPath, CONFIG_PROTECTED_FIELD_PATH_SET, `${label}.fieldPath`);
|
|
533
|
+
if (value.kind !== "protection") {
|
|
534
|
+
throw new TypeError(`${label}.kind must be protection`);
|
|
535
|
+
}
|
|
536
|
+
if (value.outcome !== "blocked") {
|
|
537
|
+
throw new TypeError(`${label}.outcome must be blocked`);
|
|
538
|
+
}
|
|
539
|
+
assertAvailableFactSource(value, label);
|
|
540
|
+
return {
|
|
541
|
+
factId: "config-protected-field",
|
|
542
|
+
fieldPath: value.fieldPath,
|
|
543
|
+
kind: "protection",
|
|
544
|
+
outcome: "blocked",
|
|
545
|
+
sourceId: value.sourceId,
|
|
546
|
+
status: "available"
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function assertAvailableFactSource(value, label) {
|
|
550
|
+
assertEnum(value.sourceId, CAPABILITY_SOURCE_ID_SET, `${label}.sourceId`);
|
|
551
|
+
if (value.status !== "available") {
|
|
552
|
+
throw new TypeError(`${label}.status must be available`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function normalizeStatusFact(value, label) {
|
|
556
|
+
assertNoDiscoveryFields(value, label);
|
|
557
|
+
if (value.kind !== undefined && value.kind !== "status") {
|
|
558
|
+
throw new TypeError(`${label}.kind must be status`);
|
|
559
|
+
}
|
|
560
|
+
assertEnum(value.status, CAPABILITY_STATUS_SET, `${label}.status`);
|
|
561
|
+
if (value.sourceId !== undefined) {
|
|
562
|
+
assertEnum(value.sourceId, CAPABILITY_SOURCE_ID_SET, `${label}.sourceId`);
|
|
563
|
+
}
|
|
564
|
+
switch (value.status) {
|
|
565
|
+
case "available":
|
|
566
|
+
return normalizeAvailableFact(value, label);
|
|
567
|
+
case "unknown":
|
|
568
|
+
return normalizeUnknownFact(value, label);
|
|
569
|
+
case "unavailable":
|
|
570
|
+
return normalizeUnavailableFact(value, label);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function assertNoDiscoveryFields(value, label) {
|
|
574
|
+
if (value.count !== undefined || value.discoveryId !== undefined || value.winningRoots !== undefined) {
|
|
575
|
+
throw new TypeError(`${label} discovery fields require discovery kind`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function normalizeDiscoveryFact(value, label) {
|
|
579
|
+
if (value.status === "unavailable") {
|
|
580
|
+
return normalizeUnavailableDiscoveryFact(value, label);
|
|
581
|
+
}
|
|
582
|
+
if (value.status !== "available") {
|
|
583
|
+
throw new TypeError(`${label}.status must be available or unavailable`);
|
|
584
|
+
}
|
|
585
|
+
assertAvailableDiscoveryFact(value, label);
|
|
586
|
+
assertEnum(value.discoveryId, CAPABILITY_DISCOVERY_ID_SET, `${label}.discoveryId`);
|
|
587
|
+
const expectedSourceId = `discovery:${value.discoveryId}`;
|
|
588
|
+
if (value.sourceId !== expectedSourceId) {
|
|
589
|
+
throw new TypeError(`${label}.sourceId does not match discoveryId`);
|
|
590
|
+
}
|
|
591
|
+
assertEnum(value.sourceId, CAPABILITY_SOURCE_ID_SET, `${label}.sourceId`);
|
|
592
|
+
const count = value.count;
|
|
593
|
+
if (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0 || count > MAX_DISCOVERY_COUNT) {
|
|
594
|
+
throw new TypeError(`${label}.count must be a bounded integer`);
|
|
595
|
+
}
|
|
596
|
+
if (!Array.isArray(value.winningRoots)) {
|
|
597
|
+
throw new TypeError(`${label}.winningRoots must be an array`);
|
|
598
|
+
}
|
|
599
|
+
if (value.winningRoots.length > MAX_DISCOVERY_ROOTS) {
|
|
600
|
+
throw new TypeError(`${label}.winningRoots exceeds the bounded limit`);
|
|
601
|
+
}
|
|
602
|
+
const winningRoots = value.winningRoots.map((root, index) => {
|
|
603
|
+
assertString(root, `${label}.winningRoots[${index}]`);
|
|
604
|
+
return normalizeDisplayId(root, `${label}.winningRoots[${index}]`);
|
|
605
|
+
});
|
|
606
|
+
winningRoots.sort(compareText);
|
|
607
|
+
return {
|
|
608
|
+
count,
|
|
609
|
+
discoveryId: value.discoveryId,
|
|
610
|
+
factId: "discovery-summary",
|
|
611
|
+
kind: "discovery",
|
|
612
|
+
sourceId: value.sourceId,
|
|
613
|
+
status: "available",
|
|
614
|
+
winningRoots: [...new Set(winningRoots)]
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function normalizeDiscoverySourceIssueFact(value, label) {
|
|
618
|
+
if (value.kind !== "status") {
|
|
619
|
+
throw new TypeError(`${label}.kind must be status`);
|
|
620
|
+
}
|
|
621
|
+
if (value.status !== "unavailable") {
|
|
622
|
+
throw new TypeError(`${label}.status must be unavailable`);
|
|
623
|
+
}
|
|
624
|
+
if (value.sourceId !== "discovery:skills") {
|
|
625
|
+
throw new TypeError(`${label}.sourceId must be discovery:skills`);
|
|
626
|
+
}
|
|
627
|
+
if (value.errorCode !== "source-malformed" && value.errorCode !== "source-read-failed") {
|
|
628
|
+
throw new TypeError(`${label}.errorCode is not a discovery source error`);
|
|
629
|
+
}
|
|
630
|
+
if (value.count !== undefined || value.discoveryId !== undefined || value.fieldPath !== undefined || value.limitationCode !== undefined || value.outcome !== undefined || value.winningRoots !== undefined) {
|
|
631
|
+
throw new TypeError(`${label} has invalid discovery source issue fields`);
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
errorCode: value.errorCode,
|
|
635
|
+
factId: "discovery-source-issue",
|
|
636
|
+
kind: "status",
|
|
637
|
+
sourceId: "discovery:skills",
|
|
638
|
+
status: "unavailable"
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function normalizeUnavailableDiscoveryFact(value, label) {
|
|
642
|
+
if (value.kind !== undefined && value.kind !== "status") {
|
|
643
|
+
throw new TypeError(`${label}.kind must be status`);
|
|
644
|
+
}
|
|
645
|
+
if (value.sourceId === undefined) {
|
|
646
|
+
throw new TypeError(`${label}.sourceId is required`);
|
|
647
|
+
}
|
|
648
|
+
if (value.count !== undefined || value.discoveryId !== undefined || value.winningRoots !== undefined) {
|
|
649
|
+
throw new TypeError(`${label} unavailable discovery fields are invalid`);
|
|
650
|
+
}
|
|
651
|
+
assertEnum(value.sourceId, CAPABILITY_SOURCE_ID_SET, `${label}.sourceId`);
|
|
652
|
+
return normalizeUnavailableFact(value, label);
|
|
653
|
+
}
|
|
654
|
+
function assertAvailableDiscoveryFact(value, label) {
|
|
655
|
+
if (value.kind !== "discovery") {
|
|
656
|
+
throw new TypeError(`${label}.kind must be discovery`);
|
|
657
|
+
}
|
|
658
|
+
if (value.errorCode !== undefined || value.limitationCode !== undefined || value.outcome !== undefined) {
|
|
659
|
+
throw new TypeError(`${label} available discovery fields are invalid`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function normalizeAvailableFact(value, label) {
|
|
663
|
+
if (value.sourceId !== undefined || value.errorCode !== undefined) {
|
|
664
|
+
throw new TypeError(`${label} available facts cannot include source or error metadata`);
|
|
665
|
+
}
|
|
666
|
+
if (value.limitationCode !== undefined) {
|
|
667
|
+
throw new TypeError(`${label} available facts cannot include a limitation`);
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
factId: value.factId,
|
|
671
|
+
kind: "status",
|
|
672
|
+
status: "available"
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function normalizeUnknownFact(value, label) {
|
|
676
|
+
assertEnum(value.limitationCode, CAPABILITY_LIMITATION_CODE_SET, `${label}.limitationCode`);
|
|
677
|
+
if (value.sourceId !== undefined || value.errorCode !== undefined) {
|
|
678
|
+
throw new TypeError(`${label} unknown facts cannot include source or error metadata`);
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
factId: value.factId,
|
|
682
|
+
kind: "status",
|
|
683
|
+
limitationCode: value.limitationCode,
|
|
684
|
+
status: "unknown"
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function normalizeUnavailableFact(value, label) {
|
|
688
|
+
assertEnum(value.errorCode, CAPABILITY_ERROR_CODE_SET, `${label}.errorCode`);
|
|
689
|
+
if (value.limitationCode !== undefined) {
|
|
690
|
+
throw new TypeError(`${label} unavailable facts cannot include a limitation`);
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
errorCode: value.errorCode,
|
|
694
|
+
factId: value.factId,
|
|
695
|
+
kind: "status",
|
|
696
|
+
...value.sourceId === undefined ? {} : { sourceId: value.sourceId },
|
|
697
|
+
status: "unavailable"
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function normalizeFacts(facts) {
|
|
701
|
+
const normalized = facts.map((fact, index) => normalizeFact(fact, `facts[${index}]`));
|
|
702
|
+
normalized.sort(compareFacts);
|
|
703
|
+
return normalized;
|
|
704
|
+
}
|
|
705
|
+
function compareFacts(left, right) {
|
|
706
|
+
const factOrder = compareText(left.factId, right.factId);
|
|
707
|
+
if (factOrder !== 0)
|
|
708
|
+
return factOrder;
|
|
709
|
+
const fieldOrder = compareText(factField(left, "fieldPath"), factField(right, "fieldPath"));
|
|
710
|
+
if (fieldOrder !== 0)
|
|
711
|
+
return fieldOrder;
|
|
712
|
+
const statusOrder = compareText(left.status, right.status);
|
|
713
|
+
if (statusOrder !== 0)
|
|
714
|
+
return statusOrder;
|
|
715
|
+
const sourceOrder = compareText(factField(left, "sourceId"), factField(right, "sourceId"));
|
|
716
|
+
if (sourceOrder !== 0)
|
|
717
|
+
return sourceOrder;
|
|
718
|
+
const discoveryOrder = compareText(factField(left, "discoveryId"), factField(right, "discoveryId"));
|
|
719
|
+
if (discoveryOrder !== 0)
|
|
720
|
+
return discoveryOrder;
|
|
721
|
+
return compareText(factField(left, "errorCode"), factField(right, "errorCode"));
|
|
722
|
+
}
|
|
723
|
+
function factField(fact, field) {
|
|
724
|
+
if (field === "discoveryId" && "discoveryId" in fact)
|
|
725
|
+
return fact.discoveryId;
|
|
726
|
+
if (field === "errorCode" && "errorCode" in fact)
|
|
727
|
+
return fact.errorCode;
|
|
728
|
+
if (field === "fieldPath" && "fieldPath" in fact)
|
|
729
|
+
return fact.fieldPath;
|
|
730
|
+
if (field === "sourceId" && "sourceId" in fact)
|
|
731
|
+
return fact.sourceId ?? "";
|
|
732
|
+
return "";
|
|
733
|
+
}
|
|
734
|
+
function buildCapabilitySnapshot(options) {
|
|
735
|
+
assertRecord(options, "options");
|
|
736
|
+
assertAllowedKeys(options, [
|
|
737
|
+
"argv",
|
|
738
|
+
"clock",
|
|
739
|
+
"config",
|
|
740
|
+
"facts",
|
|
741
|
+
"observedAt",
|
|
742
|
+
"outputSink",
|
|
743
|
+
"package",
|
|
744
|
+
"roots",
|
|
745
|
+
"sources"
|
|
746
|
+
], "options");
|
|
747
|
+
if (!Array.isArray(options.argv))
|
|
748
|
+
throw new TypeError("argv must be an array");
|
|
749
|
+
if (!Array.isArray(options.roots))
|
|
750
|
+
throw new TypeError("roots must be an array");
|
|
751
|
+
if (options.sources !== undefined && !Array.isArray(options.sources)) {
|
|
752
|
+
throw new TypeError("sources must be an array");
|
|
753
|
+
}
|
|
754
|
+
if (options.facts !== undefined && !Array.isArray(options.facts)) {
|
|
755
|
+
throw new TypeError("facts must be an array");
|
|
756
|
+
}
|
|
757
|
+
const configObservation = options.config ? normalizeConfigObservation(options.config) : { facts: [], sources: [] };
|
|
758
|
+
const snapshot = {
|
|
759
|
+
command: CAPABILITY_SNAPSHOT_COMMAND,
|
|
760
|
+
facts: normalizeFacts([
|
|
761
|
+
...options.facts ?? [],
|
|
762
|
+
...configObservation.facts
|
|
763
|
+
]),
|
|
764
|
+
identity: {
|
|
765
|
+
argv: normalizeArgv(options.argv),
|
|
766
|
+
package: normalizePackage(options.package)
|
|
767
|
+
},
|
|
768
|
+
observedAt: normalizeObservedAt(options),
|
|
769
|
+
roots: normalizeRoots(options.roots),
|
|
770
|
+
schemaVersion: CAPABILITY_SNAPSHOT_SCHEMA_VERSION,
|
|
771
|
+
sources: normalizeSources([
|
|
772
|
+
...options.sources ?? [],
|
|
773
|
+
...configObservation.sources
|
|
774
|
+
])
|
|
775
|
+
};
|
|
776
|
+
const serialized = JSON.stringify(snapshot);
|
|
777
|
+
options.outputSink?.(serialized);
|
|
778
|
+
return snapshot;
|
|
779
|
+
}
|
|
780
|
+
|
|
20
781
|
// src/lib/pi-subagents-export.ts
|
|
21
782
|
import * as crypto2 from "crypto";
|
|
22
|
-
import * as
|
|
783
|
+
import * as fs3 from "fs";
|
|
23
784
|
import * as os from "os";
|
|
24
785
|
import * as path2 from "path";
|
|
25
786
|
import { fileURLToPath } from "url";
|
|
26
787
|
|
|
27
788
|
// src/lib/pi-subagents-personas.ts
|
|
28
789
|
import * as crypto from "crypto";
|
|
29
|
-
import * as
|
|
790
|
+
import * as fs2 from "fs";
|
|
30
791
|
import * as path from "path";
|
|
31
792
|
var CURATED_PERSONAS = [
|
|
32
793
|
{
|
|
@@ -293,7 +1054,7 @@ function generateAll(repoRoot) {
|
|
|
293
1054
|
const fullPath = path.join(repoRoot, curatedEntry.relPath);
|
|
294
1055
|
let rawContent;
|
|
295
1056
|
try {
|
|
296
|
-
rawContent =
|
|
1057
|
+
rawContent = fs2.readFileSync(fullPath, "utf-8");
|
|
297
1058
|
} catch (err) {
|
|
298
1059
|
throw new Error(`Failed to read ${curatedEntry.relPath}: ${err.message}`);
|
|
299
1060
|
}
|
|
@@ -362,7 +1123,7 @@ function assertAnchoredPathSafe(anchor, targetPath) {
|
|
|
362
1123
|
current = path2.join(current, part);
|
|
363
1124
|
let stat;
|
|
364
1125
|
try {
|
|
365
|
-
stat =
|
|
1126
|
+
stat = fs3.lstatSync(current);
|
|
366
1127
|
} catch {
|
|
367
1128
|
continue;
|
|
368
1129
|
}
|
|
@@ -375,11 +1136,11 @@ function assertAnchoredPathSafe(anchor, targetPath) {
|
|
|
375
1136
|
function assertCanonicalRootWithinAnchor(anchor, root) {
|
|
376
1137
|
let canonicalAnchor;
|
|
377
1138
|
try {
|
|
378
|
-
canonicalAnchor =
|
|
1139
|
+
canonicalAnchor = fs3.realpathSync(anchor);
|
|
379
1140
|
} catch (err) {
|
|
380
1141
|
throw new Error(`Cannot resolve safety anchor ${anchor}: ${err instanceof Error ? err.message : String(err)}`);
|
|
381
1142
|
}
|
|
382
|
-
const canonicalRoot =
|
|
1143
|
+
const canonicalRoot = fs3.realpathSync(root);
|
|
383
1144
|
if (canonicalRoot !== canonicalAnchor && !canonicalRoot.startsWith(canonicalAnchor + path2.sep)) {
|
|
384
1145
|
throw new Error(`Refusing to operate on ${canonicalRoot}: canonical path escapes safety anchor ` + `${canonicalAnchor} (raced-symlink ancestor detected after resolution)`);
|
|
385
1146
|
}
|
|
@@ -414,11 +1175,11 @@ function validateManifestObject(parsed) {
|
|
|
414
1175
|
}
|
|
415
1176
|
return null;
|
|
416
1177
|
}
|
|
417
|
-
var HAS_O_NOFOLLOW = typeof
|
|
1178
|
+
var HAS_O_NOFOLLOW = typeof fs3.constants.O_NOFOLLOW === "number";
|
|
418
1179
|
function verifyManifestPathIdentity(manifestPath, openedStat) {
|
|
419
1180
|
let linkStat;
|
|
420
1181
|
try {
|
|
421
|
-
linkStat =
|
|
1182
|
+
linkStat = fs3.lstatSync(manifestPath);
|
|
422
1183
|
} catch (err) {
|
|
423
1184
|
return `Cannot verify manifest path after open: ${err instanceof Error ? err.message : String(err)}`;
|
|
424
1185
|
}
|
|
@@ -429,9 +1190,9 @@ function verifyManifestPathIdentity(manifestPath, openedStat) {
|
|
|
429
1190
|
return null;
|
|
430
1191
|
}
|
|
431
1192
|
function openManifestFd(manifestPath) {
|
|
432
|
-
const openFlags =
|
|
1193
|
+
const openFlags = fs3.constants.O_RDONLY | (HAS_O_NOFOLLOW ? fs3.constants.O_NOFOLLOW : 0);
|
|
433
1194
|
try {
|
|
434
|
-
return { kind: "fd", fd:
|
|
1195
|
+
return { kind: "fd", fd: fs3.openSync(manifestPath, openFlags) };
|
|
435
1196
|
} catch (err) {
|
|
436
1197
|
if (err.code === "ENOENT")
|
|
437
1198
|
return { kind: "result", result: { kind: "absent" } };
|
|
@@ -474,7 +1235,7 @@ function readManifestStrict(agentsRoot) {
|
|
|
474
1235
|
return opened.result;
|
|
475
1236
|
const { fd } = opened;
|
|
476
1237
|
try {
|
|
477
|
-
const stat =
|
|
1238
|
+
const stat = fs3.fstatSync(fd);
|
|
478
1239
|
if (!stat.isFile())
|
|
479
1240
|
return {
|
|
480
1241
|
kind: "malformed",
|
|
@@ -485,14 +1246,14 @@ function readManifestStrict(agentsRoot) {
|
|
|
485
1246
|
if (identityError !== null)
|
|
486
1247
|
return { kind: "malformed", error: identityError };
|
|
487
1248
|
}
|
|
488
|
-
return parseManifestContents(
|
|
1249
|
+
return parseManifestContents(fs3.readFileSync(fd, "utf-8"));
|
|
489
1250
|
} catch (err) {
|
|
490
1251
|
return {
|
|
491
1252
|
kind: "malformed",
|
|
492
1253
|
error: `Cannot read manifest: ${err instanceof Error ? err.message : String(err)}`
|
|
493
1254
|
};
|
|
494
1255
|
} finally {
|
|
495
|
-
|
|
1256
|
+
fs3.closeSync(fd);
|
|
496
1257
|
}
|
|
497
1258
|
}
|
|
498
1259
|
function writeManifest(agentsRoot, manifest) {
|
|
@@ -501,7 +1262,7 @@ function writeManifest(agentsRoot, manifest) {
|
|
|
501
1262
|
}
|
|
502
1263
|
function statOrNull(p) {
|
|
503
1264
|
try {
|
|
504
|
-
return
|
|
1265
|
+
return fs3.lstatSync(p);
|
|
505
1266
|
} catch {
|
|
506
1267
|
return null;
|
|
507
1268
|
}
|
|
@@ -509,7 +1270,7 @@ function statOrNull(p) {
|
|
|
509
1270
|
function assertRealAgentsRoot(agentsRoot) {
|
|
510
1271
|
let stat = null;
|
|
511
1272
|
try {
|
|
512
|
-
stat =
|
|
1273
|
+
stat = fs3.lstatSync(agentsRoot);
|
|
513
1274
|
} catch {}
|
|
514
1275
|
if (stat !== null) {
|
|
515
1276
|
if (stat.isSymbolicLink())
|
|
@@ -517,7 +1278,7 @@ function assertRealAgentsRoot(agentsRoot) {
|
|
|
517
1278
|
if (!stat.isDirectory())
|
|
518
1279
|
throw new Error(`Refusing to write under ${agentsRoot}: not a directory`);
|
|
519
1280
|
}
|
|
520
|
-
return
|
|
1281
|
+
return fs3.realpathSync(agentsRoot);
|
|
521
1282
|
}
|
|
522
1283
|
function safeFilePathOrThrow(agentsRoot, filename) {
|
|
523
1284
|
if (path2.basename(filename) !== filename || path2.isAbsolute(filename)) {
|
|
@@ -539,7 +1300,7 @@ function safeFilePath(agentsRoot, filename) {
|
|
|
539
1300
|
function acquireLock(agentsRoot) {
|
|
540
1301
|
const lockPath = path2.join(agentsRoot, LOCK_FILENAME);
|
|
541
1302
|
try {
|
|
542
|
-
return { ok: true, fd:
|
|
1303
|
+
return { ok: true, fd: fs3.openSync(lockPath, "wx") };
|
|
543
1304
|
} catch (err) {
|
|
544
1305
|
if (err.code === "EEXIST") {
|
|
545
1306
|
return {
|
|
@@ -557,10 +1318,10 @@ function acquireLock(agentsRoot) {
|
|
|
557
1318
|
}
|
|
558
1319
|
function releaseLock(agentsRoot, fd) {
|
|
559
1320
|
try {
|
|
560
|
-
|
|
1321
|
+
fs3.closeSync(fd);
|
|
561
1322
|
} catch {}
|
|
562
1323
|
try {
|
|
563
|
-
|
|
1324
|
+
fs3.unlinkSync(path2.join(agentsRoot, LOCK_FILENAME));
|
|
564
1325
|
} catch {}
|
|
565
1326
|
}
|
|
566
1327
|
function withLock(agentsRoot, fn) {
|
|
@@ -582,11 +1343,11 @@ function atomicWriteString(destPath, content) {
|
|
|
582
1343
|
const parentDir = path2.dirname(destPath);
|
|
583
1344
|
const tmpPath = path2.join(parentDir, `.${path2.basename(destPath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
584
1345
|
try {
|
|
585
|
-
|
|
586
|
-
|
|
1346
|
+
fs3.writeFileSync(tmpPath, content, { flag: "w", mode: 420 });
|
|
1347
|
+
fs3.renameSync(tmpPath, destPath);
|
|
587
1348
|
} catch (err) {
|
|
588
1349
|
try {
|
|
589
|
-
|
|
1350
|
+
fs3.unlinkSync(tmpPath);
|
|
590
1351
|
} catch {}
|
|
591
1352
|
throw err;
|
|
592
1353
|
}
|
|
@@ -595,7 +1356,7 @@ function snapshotFiles(filePaths) {
|
|
|
595
1356
|
const backup = new Map;
|
|
596
1357
|
for (const p of filePaths) {
|
|
597
1358
|
try {
|
|
598
|
-
backup.set(p,
|
|
1359
|
+
backup.set(p, fs3.readFileSync(p, "utf-8"));
|
|
599
1360
|
} catch (err) {
|
|
600
1361
|
if (err.code === "ENOENT") {
|
|
601
1362
|
backup.set(p, undefined);
|
|
@@ -614,10 +1375,10 @@ function restoreFromBackup(backup) {
|
|
|
614
1375
|
for (const [p, content] of backup) {
|
|
615
1376
|
try {
|
|
616
1377
|
if (content === undefined) {
|
|
617
|
-
if (
|
|
618
|
-
|
|
1378
|
+
if (fs3.existsSync(p))
|
|
1379
|
+
fs3.unlinkSync(p);
|
|
619
1380
|
} else {
|
|
620
|
-
|
|
1381
|
+
fs3.writeFileSync(p, content, "utf-8");
|
|
621
1382
|
}
|
|
622
1383
|
} catch {
|
|
623
1384
|
failed.push(p);
|
|
@@ -766,11 +1527,11 @@ function generateEntries(repoRoot, configOptions) {
|
|
|
766
1527
|
function findPackageRoot() {
|
|
767
1528
|
const thisFile = fileURLToPath(import.meta.url);
|
|
768
1529
|
const dir = path2.resolve(path2.dirname(thisFile), "..", "..");
|
|
769
|
-
if (
|
|
1530
|
+
if (fs3.existsSync(path2.join(dir, "package.json")))
|
|
770
1531
|
return dir;
|
|
771
1532
|
let candidate = path2.dirname(thisFile);
|
|
772
1533
|
for (let i = 0;i < 8; i++) {
|
|
773
|
-
if (
|
|
1534
|
+
if (fs3.existsSync(path2.join(candidate, "package.json")))
|
|
774
1535
|
return candidate;
|
|
775
1536
|
const parent = path2.dirname(candidate);
|
|
776
1537
|
if (parent === candidate)
|
|
@@ -780,7 +1541,7 @@ function findPackageRoot() {
|
|
|
780
1541
|
throw new Error("Could not locate package root (no package.json found)");
|
|
781
1542
|
}
|
|
782
1543
|
function planExistingEntry(absRoot, filename, hash, owned) {
|
|
783
|
-
const diskContent =
|
|
1544
|
+
const diskContent = fs3.readFileSync(path2.join(absRoot, filename), "utf-8");
|
|
784
1545
|
const diskHash = crypto2.createHash("sha256").update(diskContent).digest("hex");
|
|
785
1546
|
if (owned.has(filename)) {
|
|
786
1547
|
return diskHash !== hash ? { action: "update", filename } : { action: "skip", filename };
|
|
@@ -796,7 +1557,7 @@ function planEntryActions(absRoot, entries, owned) {
|
|
|
796
1557
|
for (const entry of entries) {
|
|
797
1558
|
if (entry.status === "excluded-critical" || !entry.content)
|
|
798
1559
|
continue;
|
|
799
|
-
const exists =
|
|
1560
|
+
const exists = fs3.existsSync(path2.join(absRoot, entry.filename));
|
|
800
1561
|
actions.push(exists ? planExistingEntry(absRoot, entry.filename, entry.hash, owned) : { action: "create", filename: entry.filename });
|
|
801
1562
|
}
|
|
802
1563
|
return actions;
|
|
@@ -809,7 +1570,7 @@ function planRemoveActions(absRoot, manifest, currentFilenames) {
|
|
|
809
1570
|
if (currentFilenames.has(mEntry.filename))
|
|
810
1571
|
continue;
|
|
811
1572
|
const safe = safeFilePath(absRoot, mEntry.filename);
|
|
812
|
-
if (safe !== null &&
|
|
1573
|
+
if (safe !== null && fs3.existsSync(safe))
|
|
813
1574
|
removes.push({ action: "remove", filename: mEntry.filename });
|
|
814
1575
|
}
|
|
815
1576
|
return removes;
|
|
@@ -852,13 +1613,13 @@ function classifyOneExportEntry(absRoot, entry, owned) {
|
|
|
852
1613
|
const safePath = safeFilePath(absRoot, entry.filename);
|
|
853
1614
|
if (safePath === null)
|
|
854
1615
|
return { kind: "refuse", reason: "Filename would escape agents root" };
|
|
855
|
-
if (
|
|
1616
|
+
if (fs3.existsSync(safePath) && !owned.has(entry.filename))
|
|
856
1617
|
return {
|
|
857
1618
|
kind: "refuse",
|
|
858
1619
|
reason: "File exists but is not owned by a previous export (user file)"
|
|
859
1620
|
};
|
|
860
|
-
if (
|
|
861
|
-
const diskHash = crypto2.createHash("sha256").update(
|
|
1621
|
+
if (fs3.existsSync(safePath)) {
|
|
1622
|
+
const diskHash = crypto2.createHash("sha256").update(fs3.readFileSync(safePath, "utf-8")).digest("hex");
|
|
862
1623
|
if (diskHash === entry.hash)
|
|
863
1624
|
return { kind: "skip", safePath };
|
|
864
1625
|
}
|
|
@@ -904,19 +1665,19 @@ function makeExportError(error) {
|
|
|
904
1665
|
function prepareExportRoot(agentsRoot) {
|
|
905
1666
|
let preStat = null;
|
|
906
1667
|
try {
|
|
907
|
-
preStat =
|
|
1668
|
+
preStat = fs3.lstatSync(agentsRoot);
|
|
908
1669
|
} catch {}
|
|
909
1670
|
if (preStat?.isSymbolicLink())
|
|
910
1671
|
return makeExportError(`Refusing to write under ${agentsRoot}: not a real directory (symlink detected)`);
|
|
911
1672
|
try {
|
|
912
|
-
|
|
1673
|
+
fs3.mkdirSync(agentsRoot, { recursive: true });
|
|
913
1674
|
return assertRealAgentsRoot(agentsRoot);
|
|
914
1675
|
} catch (err) {
|
|
915
1676
|
return makeExportError(err instanceof Error ? err.message : String(err));
|
|
916
1677
|
}
|
|
917
1678
|
}
|
|
918
1679
|
function assertHashMatchesOrThrow(filePath, expectedHash, filename, verb) {
|
|
919
|
-
const diskContent =
|
|
1680
|
+
const diskContent = fs3.readFileSync(filePath, "utf-8");
|
|
920
1681
|
const diskHash = crypto2.createHash("sha256").update(diskContent).digest("hex");
|
|
921
1682
|
if (diskHash !== expectedHash) {
|
|
922
1683
|
throw new Error(`Refusing to ${verb} "${filename}": content on disk does not match the ` + `manifest hash (drifted or tampered). Resolve manually or re-export/refresh first.`);
|
|
@@ -930,7 +1691,7 @@ function findStalePaths(absRoot, prevManifest, currentFilenames) {
|
|
|
930
1691
|
if (currentFilenames.has(mEntry.filename))
|
|
931
1692
|
continue;
|
|
932
1693
|
const safe = safeFilePath(absRoot, mEntry.filename);
|
|
933
|
-
if (safe === null || !
|
|
1694
|
+
if (safe === null || !fs3.existsSync(safe))
|
|
934
1695
|
continue;
|
|
935
1696
|
assertHashMatchesOrThrow(safe, mEntry.hash, mEntry.filename, "remove stale file");
|
|
936
1697
|
stale.push({ path: safe, filename: mEntry.filename, hash: mEntry.hash });
|
|
@@ -984,9 +1745,9 @@ function exportPersonas(agentsRoot, configOptions) {
|
|
|
984
1745
|
], [
|
|
985
1746
|
...toWrite.map(({ entry, safePath }) => () => atomicWriteString(safePath, entry.content ?? "")),
|
|
986
1747
|
...staleOwned.map(({ path: p, filename, hash }) => () => {
|
|
987
|
-
if (
|
|
1748
|
+
if (fs3.existsSync(p)) {
|
|
988
1749
|
assertHashMatchesOrThrow(p, hash, filename, "remove stale file");
|
|
989
|
-
|
|
1750
|
+
fs3.unlinkSync(p);
|
|
990
1751
|
}
|
|
991
1752
|
}),
|
|
992
1753
|
() => writeManifest(absRoot, newManifest())
|
|
@@ -1014,13 +1775,13 @@ function classifyOneRefreshEntry(absRoot, entry, owned) {
|
|
|
1014
1775
|
const safePath = safeFilePath(absRoot, entry.filename);
|
|
1015
1776
|
if (safePath === null)
|
|
1016
1777
|
return { kind: "ignore" };
|
|
1017
|
-
const exists =
|
|
1778
|
+
const exists = fs3.existsSync(safePath);
|
|
1018
1779
|
if (exists && !owned.has(entry.filename))
|
|
1019
1780
|
return { kind: "skip-unowned", safePath };
|
|
1020
1781
|
if (!exists && !owned.has(entry.filename))
|
|
1021
1782
|
return { kind: "skip-unexported" };
|
|
1022
1783
|
if (exists) {
|
|
1023
|
-
const diskHash = crypto2.createHash("sha256").update(
|
|
1784
|
+
const diskHash = crypto2.createHash("sha256").update(fs3.readFileSync(safePath, "utf-8")).digest("hex");
|
|
1024
1785
|
if (diskHash === entry.hash)
|
|
1025
1786
|
return { kind: "keep", safePath };
|
|
1026
1787
|
}
|
|
@@ -1049,7 +1810,7 @@ function classifyRefreshEntries(absRoot, entries, owned) {
|
|
|
1049
1810
|
}
|
|
1050
1811
|
function validateRefreshRoot(agentsRoot) {
|
|
1051
1812
|
const absRoot = path2.resolve(agentsRoot);
|
|
1052
|
-
if (!
|
|
1813
|
+
if (!fs3.existsSync(absRoot))
|
|
1053
1814
|
return {
|
|
1054
1815
|
status: "error",
|
|
1055
1816
|
updated: 0,
|
|
@@ -1057,7 +1818,7 @@ function validateRefreshRoot(agentsRoot) {
|
|
|
1057
1818
|
error: `Agents root does not exist: ${absRoot}. Run export first.`
|
|
1058
1819
|
};
|
|
1059
1820
|
try {
|
|
1060
|
-
const stat =
|
|
1821
|
+
const stat = fs3.lstatSync(absRoot);
|
|
1061
1822
|
if (stat.isSymbolicLink())
|
|
1062
1823
|
return {
|
|
1063
1824
|
status: "error",
|
|
@@ -1154,7 +1915,7 @@ function collectCleanupPaths(absRoot, manifest) {
|
|
|
1154
1915
|
const targets = [];
|
|
1155
1916
|
for (const entry of manifest.files) {
|
|
1156
1917
|
const safe = safeFilePath(absRoot, entry.filename);
|
|
1157
|
-
if (safe === null || !
|
|
1918
|
+
if (safe === null || !fs3.existsSync(safe))
|
|
1158
1919
|
continue;
|
|
1159
1920
|
assertHashMatchesOrThrow(safe, entry.hash, entry.filename, "delete");
|
|
1160
1921
|
targets.push({ path: safe, filename: entry.filename, hash: entry.hash });
|
|
@@ -1162,7 +1923,7 @@ function collectCleanupPaths(absRoot, manifest) {
|
|
|
1162
1923
|
const manifestPath = path2.join(absRoot, MANIFEST_FILENAME);
|
|
1163
1924
|
return {
|
|
1164
1925
|
targets,
|
|
1165
|
-
manifestPath:
|
|
1926
|
+
manifestPath: fs3.existsSync(manifestPath) ? manifestPath : null
|
|
1166
1927
|
};
|
|
1167
1928
|
}
|
|
1168
1929
|
function cleanup(agentsRoot, configOptions) {
|
|
@@ -1172,7 +1933,7 @@ function cleanup(agentsRoot, configOptions) {
|
|
|
1172
1933
|
throw new Error(`Refusing to operate on ${absRootCandidate}: not a real directory (symlink detected)`);
|
|
1173
1934
|
const anchor = anchorForScope(configOptions, absRootCandidate);
|
|
1174
1935
|
assertAnchoredPathSafe(anchor, absRootCandidate);
|
|
1175
|
-
if (!
|
|
1936
|
+
if (!fs3.existsSync(absRootCandidate))
|
|
1176
1937
|
return { status: "ok" };
|
|
1177
1938
|
const absRoot = assertCanonicalRootWithinAnchor(anchor, absRootCandidate);
|
|
1178
1939
|
const lockResult = withLock(absRoot, () => {
|
|
@@ -1189,15 +1950,15 @@ function cleanup(agentsRoot, configOptions) {
|
|
|
1189
1950
|
];
|
|
1190
1951
|
const tx2 = runWithRollback(pathsToWatch, [
|
|
1191
1952
|
...targets.map(({ path: p, filename, hash }) => () => {
|
|
1192
|
-
if (
|
|
1953
|
+
if (fs3.existsSync(p)) {
|
|
1193
1954
|
assertHashMatchesOrThrow(p, hash, filename, "delete");
|
|
1194
|
-
|
|
1955
|
+
fs3.unlinkSync(p);
|
|
1195
1956
|
}
|
|
1196
1957
|
}),
|
|
1197
1958
|
...manifestPath ? [
|
|
1198
1959
|
() => {
|
|
1199
|
-
if (
|
|
1200
|
-
|
|
1960
|
+
if (fs3.existsSync(manifestPath))
|
|
1961
|
+
fs3.unlinkSync(manifestPath);
|
|
1201
1962
|
}
|
|
1202
1963
|
] : []
|
|
1203
1964
|
]);
|
|
@@ -1217,7 +1978,7 @@ function cleanup(agentsRoot, configOptions) {
|
|
|
1217
1978
|
}
|
|
1218
1979
|
|
|
1219
1980
|
// src/lib/setup.ts
|
|
1220
|
-
import
|
|
1981
|
+
import fs4 from "fs";
|
|
1221
1982
|
import path3 from "path";
|
|
1222
1983
|
var SYSTEMATIC_PACKAGE_NAME = "@fro.bot/systematic";
|
|
1223
1984
|
var PI_PACKAGE_IDENTIFIER = `npm:${SYSTEMATIC_PACKAGE_NAME}`;
|
|
@@ -1228,12 +1989,12 @@ function createSetupError(message) {
|
|
|
1228
1989
|
return error;
|
|
1229
1990
|
}
|
|
1230
1991
|
var DEFAULT_OPS = {
|
|
1231
|
-
writeFileSync:
|
|
1232
|
-
renameSync:
|
|
1233
|
-
unlinkSync:
|
|
1234
|
-
chmodSync:
|
|
1992
|
+
writeFileSync: fs4.writeFileSync,
|
|
1993
|
+
renameSync: fs4.renameSync,
|
|
1994
|
+
unlinkSync: fs4.unlinkSync,
|
|
1995
|
+
chmodSync: fs4.chmodSync
|
|
1235
1996
|
};
|
|
1236
|
-
function
|
|
1997
|
+
function isRecord2(value) {
|
|
1237
1998
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1238
1999
|
}
|
|
1239
2000
|
function errorMessage(error) {
|
|
@@ -1253,7 +2014,7 @@ function isPiSystematicIdentifier(identifier) {
|
|
|
1253
2014
|
}
|
|
1254
2015
|
function lstatOrNull(targetPath) {
|
|
1255
2016
|
try {
|
|
1256
|
-
return
|
|
2017
|
+
return fs4.lstatSync(targetPath);
|
|
1257
2018
|
} catch (error) {
|
|
1258
2019
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1259
2020
|
return null;
|
|
@@ -1262,8 +2023,8 @@ function lstatOrNull(targetPath) {
|
|
|
1262
2023
|
}
|
|
1263
2024
|
}
|
|
1264
2025
|
function assertRealpathUnderCwd(dir, cwd) {
|
|
1265
|
-
const realDir =
|
|
1266
|
-
const realCwd =
|
|
2026
|
+
const realDir = fs4.realpathSync(dir);
|
|
2027
|
+
const realCwd = fs4.realpathSync(cwd);
|
|
1267
2028
|
const relative2 = path3.relative(realCwd, realDir);
|
|
1268
2029
|
if (relative2.startsWith("..") || path3.isAbsolute(relative2)) {
|
|
1269
2030
|
throw createSetupError(`Refusing to write under ${dir}: resolved directory escapes the project root`);
|
|
@@ -1278,7 +2039,7 @@ function assertParentTrusted(parentDir, cwd) {
|
|
|
1278
2039
|
assertRealpathUnderCwd(parentDir, cwd);
|
|
1279
2040
|
return;
|
|
1280
2041
|
}
|
|
1281
|
-
|
|
2042
|
+
fs4.mkdirSync(parentDir, { recursive: true });
|
|
1282
2043
|
assertRealpathUnderCwd(parentDir, cwd);
|
|
1283
2044
|
}
|
|
1284
2045
|
var OPENCODE_TARGET_CANDIDATES = [
|
|
@@ -1300,7 +2061,7 @@ function resolveOpenCodeTargetPath(cwd) {
|
|
|
1300
2061
|
return path3.join(cwd, "opencode.jsonc");
|
|
1301
2062
|
}
|
|
1302
2063
|
var IS_WINDOWS = process.platform === "win32";
|
|
1303
|
-
var OPEN_FLAGS = IS_WINDOWS ?
|
|
2064
|
+
var OPEN_FLAGS = IS_WINDOWS ? fs4.constants.O_RDONLY : fs4.constants.O_RDONLY | fs4.constants.O_NOFOLLOW | fs4.constants.O_NONBLOCK;
|
|
1304
2065
|
function assertWindowsPreOpenTrust(targetPath) {
|
|
1305
2066
|
const preStat = lstatOrNull(targetPath);
|
|
1306
2067
|
if (preStat && (preStat.isSymbolicLink() || !preStat.isFile())) {
|
|
@@ -1312,7 +2073,7 @@ function openTrustedExisting(targetPath) {
|
|
|
1312
2073
|
assertWindowsPreOpenTrust(targetPath);
|
|
1313
2074
|
let fd;
|
|
1314
2075
|
try {
|
|
1315
|
-
fd =
|
|
2076
|
+
fd = fs4.openSync(targetPath, OPEN_FLAGS);
|
|
1316
2077
|
} catch (error) {
|
|
1317
2078
|
if (error instanceof Error && "code" in error) {
|
|
1318
2079
|
if (error.code === "ENOENT")
|
|
@@ -1324,13 +2085,13 @@ function openTrustedExisting(targetPath) {
|
|
|
1324
2085
|
throw error;
|
|
1325
2086
|
}
|
|
1326
2087
|
try {
|
|
1327
|
-
const stat =
|
|
2088
|
+
const stat = fs4.fstatSync(fd);
|
|
1328
2089
|
if (!stat.isFile()) {
|
|
1329
2090
|
throw createSetupError(`Refusing to read ${targetPath}: not a regular file`);
|
|
1330
2091
|
}
|
|
1331
|
-
return { bytes:
|
|
2092
|
+
return { bytes: fs4.readFileSync(fd), mode: stat.mode & 511 };
|
|
1332
2093
|
} finally {
|
|
1333
|
-
|
|
2094
|
+
fs4.closeSync(fd);
|
|
1334
2095
|
}
|
|
1335
2096
|
}
|
|
1336
2097
|
function atomicWrite(targetPath, content, originalBytes, mode, ops) {
|
|
@@ -1362,7 +2123,7 @@ function makeTempPath(parentDir, basename2) {
|
|
|
1362
2123
|
return path3.join(parentDir, `.${basename2}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
1363
2124
|
}
|
|
1364
2125
|
function cleanupTemp(tempPath, ops) {
|
|
1365
|
-
if (!
|
|
2126
|
+
if (!fs4.existsSync(tempPath))
|
|
1366
2127
|
return;
|
|
1367
2128
|
try {
|
|
1368
2129
|
ops.unlinkSync(tempPath);
|
|
@@ -1407,7 +2168,7 @@ function setupOpenCode(cwd, ops) {
|
|
|
1407
2168
|
const rawText = existing.bytes.toString("utf8");
|
|
1408
2169
|
assertNoDuplicateTopLevelKeys(rawText, targetPath, ["plugin", "plugins"], "OpenCode config");
|
|
1409
2170
|
const parsed = parseOpenCodeJsonc(rawText, targetPath);
|
|
1410
|
-
if (!
|
|
2171
|
+
if (!isRecord2(parsed)) {
|
|
1411
2172
|
throw createSetupError(`Invalid OpenCode config in ${targetPath}: root must be an object`);
|
|
1412
2173
|
}
|
|
1413
2174
|
if (Object.hasOwn(parsed, "plugins")) {
|
|
@@ -1439,12 +2200,12 @@ function setupOpenCode(cwd, ops) {
|
|
|
1439
2200
|
function getPiEntryIdentifier(entry) {
|
|
1440
2201
|
if (typeof entry === "string")
|
|
1441
2202
|
return entry;
|
|
1442
|
-
if (
|
|
2203
|
+
if (isRecord2(entry) && typeof entry.source === "string")
|
|
1443
2204
|
return entry.source;
|
|
1444
2205
|
return null;
|
|
1445
2206
|
}
|
|
1446
2207
|
function assertPiTaggedEntryUsable(entry, targetPath) {
|
|
1447
|
-
if (!
|
|
2208
|
+
if (!isRecord2(entry))
|
|
1448
2209
|
return;
|
|
1449
2210
|
if (entry.autoload === false) {
|
|
1450
2211
|
throw createSetupError(`Invalid Pi settings in ${targetPath}: the existing \`${SYSTEMATIC_PACKAGE_NAME}\` package entry has \`autoload: false\`, which may prevent Systematic from loading; remove that flag or the entry before re-running setup`);
|
|
@@ -1471,7 +2232,7 @@ function setupPi(cwd, ops) {
|
|
|
1471
2232
|
} catch (error) {
|
|
1472
2233
|
throw createSetupError(`Invalid Pi settings in ${targetPath}: unable to parse JSON (${errorMessage(error)})`);
|
|
1473
2234
|
}
|
|
1474
|
-
if (!
|
|
2235
|
+
if (!isRecord2(parsed)) {
|
|
1475
2236
|
throw createSetupError(`Invalid Pi settings in ${targetPath}: root must be an object`);
|
|
1476
2237
|
}
|
|
1477
2238
|
const existingPackages = parsed.packages;
|
|
@@ -1513,19 +2274,27 @@ function setupHarness(harness, cwd, opsOverride) {
|
|
|
1513
2274
|
}
|
|
1514
2275
|
|
|
1515
2276
|
// src/cli.ts
|
|
1516
|
-
|
|
2277
|
+
function readPackageMetadata(packageRoot) {
|
|
1517
2278
|
try {
|
|
1518
|
-
const packageJsonPath = path4.
|
|
1519
|
-
if (!
|
|
1520
|
-
return
|
|
1521
|
-
const content =
|
|
2279
|
+
const packageJsonPath = path4.join(packageRoot, "package.json");
|
|
2280
|
+
if (!fs5.existsSync(packageJsonPath))
|
|
2281
|
+
return {};
|
|
2282
|
+
const content = fs5.readFileSync(packageJsonPath, "utf8");
|
|
1522
2283
|
const parsed = JSON.parse(content);
|
|
1523
|
-
|
|
2284
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2285
|
+
return {};
|
|
2286
|
+
}
|
|
2287
|
+
const record = parsed;
|
|
2288
|
+
return {
|
|
2289
|
+
...typeof record.name === "string" ? { name: record.name } : {},
|
|
2290
|
+
...typeof record.version === "string" ? { version: record.version } : {}
|
|
2291
|
+
};
|
|
1524
2292
|
} catch {
|
|
1525
|
-
return
|
|
2293
|
+
return {};
|
|
1526
2294
|
}
|
|
1527
|
-
}
|
|
1528
|
-
var
|
|
2295
|
+
}
|
|
2296
|
+
var PACKAGE_ROOT = path4.resolve(import.meta.dirname, "..");
|
|
2297
|
+
var VERSION = readPackageMetadata(PACKAGE_ROOT).version ?? "unknown";
|
|
1529
2298
|
var HELP = `
|
|
1530
2299
|
systematic - OpenCode plugin for systematic engineering workflows
|
|
1531
2300
|
|
|
@@ -1534,6 +2303,7 @@ Usage:
|
|
|
1534
2303
|
|
|
1535
2304
|
Commands:
|
|
1536
2305
|
list [type] List available skills, agents, or commands
|
|
2306
|
+
capabilities Read-only standalone-CLI observation; not a host-runtime or canonical-registry view
|
|
1537
2307
|
config [subcommand] Configuration management
|
|
1538
2308
|
show Show configuration
|
|
1539
2309
|
path Print config file locations
|
|
@@ -1550,6 +2320,7 @@ Options:
|
|
|
1550
2320
|
|
|
1551
2321
|
Examples:
|
|
1552
2322
|
systematic list skills
|
|
2323
|
+
systematic capabilities
|
|
1553
2324
|
systematic list agents
|
|
1554
2325
|
systematic config show
|
|
1555
2326
|
systematic setup --harness opencode
|
|
@@ -1572,6 +2343,161 @@ Scope:
|
|
|
1572
2343
|
project <cwd>/.pi/agents (default)
|
|
1573
2344
|
global $PI_CODING_AGENT_DIR/agents or ~/.pi/agent/agents
|
|
1574
2345
|
`;
|
|
2346
|
+
function defaultCapabilityRoots() {
|
|
2347
|
+
const configDir = process.env.XDG_CONFIG_HOME ? path4.join(process.env.XDG_CONFIG_HOME, "opencode") : path4.join(os2.homedir(), ".config/opencode");
|
|
2348
|
+
return {
|
|
2349
|
+
agentsRoot: path4.join(PACKAGE_ROOT, "agents"),
|
|
2350
|
+
configDir,
|
|
2351
|
+
cwd: process.cwd(),
|
|
2352
|
+
homeDir: os2.homedir(),
|
|
2353
|
+
opencodeConfigDirOverride: process.env.OPENCODE_CONFIG_DIR?.trim() || undefined,
|
|
2354
|
+
packageRoot: PACKAGE_ROOT
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
function sourceErrorForSkillIssue(issue) {
|
|
2358
|
+
return issue === "read-failed" ? "source-read-failed" : "source-malformed";
|
|
2359
|
+
}
|
|
2360
|
+
function discoverySummaryFact(discoveryId, count, winningRoots) {
|
|
2361
|
+
return {
|
|
2362
|
+
count,
|
|
2363
|
+
discoveryId,
|
|
2364
|
+
factId: "discovery-summary",
|
|
2365
|
+
kind: "discovery",
|
|
2366
|
+
sourceId: `discovery:${discoveryId}`,
|
|
2367
|
+
status: "available",
|
|
2368
|
+
winningRoots
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
function unavailableDiscoveryFact(discoveryId, errorCode) {
|
|
2372
|
+
return {
|
|
2373
|
+
errorCode,
|
|
2374
|
+
factId: "discovery-summary",
|
|
2375
|
+
sourceId: `discovery:${discoveryId}`,
|
|
2376
|
+
status: "unavailable"
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
function discoverySourceIssueFact(errorCode) {
|
|
2380
|
+
return {
|
|
2381
|
+
errorCode,
|
|
2382
|
+
factId: "discovery-source-issue",
|
|
2383
|
+
kind: "status",
|
|
2384
|
+
sourceId: "discovery:skills",
|
|
2385
|
+
status: "unavailable"
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
function collectSkillFacts(roots) {
|
|
2389
|
+
let issue;
|
|
2390
|
+
try {
|
|
2391
|
+
const skills = discoverSkills({
|
|
2392
|
+
configDir: roots.configDir,
|
|
2393
|
+
homeDir: roots.homeDir,
|
|
2394
|
+
onIssue: (nextIssue) => {
|
|
2395
|
+
issue ??= nextIssue;
|
|
2396
|
+
},
|
|
2397
|
+
opencodeConfigDirOverride: roots.opencodeConfigDirOverride,
|
|
2398
|
+
startDir: roots.cwd
|
|
2399
|
+
});
|
|
2400
|
+
const winningRoots = [...new Set(skills.map((skill) => skill.root))].sort();
|
|
2401
|
+
const facts = [discoverySummaryFact("skills", skills.length, winningRoots)];
|
|
2402
|
+
if (issue !== undefined) {
|
|
2403
|
+
facts.push(discoverySourceIssueFact(sourceErrorForSkillIssue(issue)));
|
|
2404
|
+
}
|
|
2405
|
+
return facts;
|
|
2406
|
+
} catch {
|
|
2407
|
+
return [discoverySourceIssueFact("source-malformed")];
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
function collectAgentFact(agentsRoot) {
|
|
2411
|
+
try {
|
|
2412
|
+
const catalog = buildAgentCatalog(agentsRoot);
|
|
2413
|
+
return discoverySummaryFact("agents", catalog.length, ["agents"]);
|
|
2414
|
+
} catch {
|
|
2415
|
+
return unavailableDiscoveryFact("agents", "structural-invalid");
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
function collectDiscoveryFacts(roots) {
|
|
2419
|
+
return [
|
|
2420
|
+
{
|
|
2421
|
+
factId: "host-runtime",
|
|
2422
|
+
limitationCode: "host-runtime-unobservable",
|
|
2423
|
+
status: "unknown"
|
|
2424
|
+
},
|
|
2425
|
+
...collectSkillFacts(roots),
|
|
2426
|
+
collectAgentFact(roots.agentsRoot)
|
|
2427
|
+
];
|
|
2428
|
+
}
|
|
2429
|
+
function collectConfigMetadata(roots, injected) {
|
|
2430
|
+
if (injected !== undefined)
|
|
2431
|
+
return injected;
|
|
2432
|
+
try {
|
|
2433
|
+
return loadConfigWithSources(roots.cwd, {
|
|
2434
|
+
customConfigDir: roots.opencodeConfigDirOverride ?? null,
|
|
2435
|
+
homeDir: roots.homeDir,
|
|
2436
|
+
invalidSource: "report",
|
|
2437
|
+
userConfigDir: roots.configDir,
|
|
2438
|
+
warningSink: () => {
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
}).metadata;
|
|
2442
|
+
} catch {
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
function resolveCapabilityRootPath(root) {
|
|
2447
|
+
try {
|
|
2448
|
+
return fs5.realpathSync(root);
|
|
2449
|
+
} catch {
|
|
2450
|
+
return path4.resolve(root);
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
function capabilityRoots(roots) {
|
|
2454
|
+
return [
|
|
2455
|
+
{ id: "agents", path: resolveCapabilityRootPath(roots.agentsRoot) },
|
|
2456
|
+
{ id: "cwd", path: resolveCapabilityRootPath(roots.cwd) },
|
|
2457
|
+
{ id: "package", path: resolveCapabilityRootPath(roots.packageRoot) },
|
|
2458
|
+
{
|
|
2459
|
+
id: "skills",
|
|
2460
|
+
path: resolveCapabilityRootPath(path4.join(roots.packageRoot, "skills"))
|
|
2461
|
+
},
|
|
2462
|
+
{ id: "user", path: resolveCapabilityRootPath(roots.homeDir) }
|
|
2463
|
+
];
|
|
2464
|
+
}
|
|
2465
|
+
function capabilityPackage(packageRoot) {
|
|
2466
|
+
const metadata = readPackageMetadata(packageRoot);
|
|
2467
|
+
return {
|
|
2468
|
+
name: metadata.name ?? "unknown",
|
|
2469
|
+
version: metadata.version ?? "unknown"
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
function runCapabilities(options) {
|
|
2473
|
+
const outputSink = options.outputSink ?? ((value) => console.log(value));
|
|
2474
|
+
const errorSink = options.errorSink ?? ((message) => console.error(message));
|
|
2475
|
+
const isFullArgv = options.argv.length === 2 && options.argv[0] === "systematic" && options.argv[1] === "capabilities";
|
|
2476
|
+
const isCommandOnlyArgv = options.argv.length === 1 && options.argv[0] === "capabilities";
|
|
2477
|
+
if (!isFullArgv && !isCommandOnlyArgv) {
|
|
2478
|
+
errorSink("Usage: systematic capabilities");
|
|
2479
|
+
return 2;
|
|
2480
|
+
}
|
|
2481
|
+
const builderArgv = isFullArgv ? options.argv : ["systematic", "capabilities"];
|
|
2482
|
+
try {
|
|
2483
|
+
buildCapabilitySnapshot({
|
|
2484
|
+
argv: builderArgv,
|
|
2485
|
+
clock: options.clock,
|
|
2486
|
+
config: collectConfigMetadata(options.roots, options.config),
|
|
2487
|
+
facts: collectDiscoveryFacts(options.roots),
|
|
2488
|
+
outputSink,
|
|
2489
|
+
package: capabilityPackage(options.roots.packageRoot),
|
|
2490
|
+
roots: capabilityRoots(options.roots)
|
|
2491
|
+
});
|
|
2492
|
+
return 0;
|
|
2493
|
+
} catch {
|
|
2494
|
+
errorSink("Capabilities diagnostic unavailable");
|
|
2495
|
+
return 1;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
function runCapabilitiesCli(options) {
|
|
2499
|
+
return runCapabilities(options);
|
|
2500
|
+
}
|
|
1575
2501
|
function isHarness(value) {
|
|
1576
2502
|
return value === "opencode" || value === "pi";
|
|
1577
2503
|
}
|
|
@@ -1637,15 +2563,15 @@ function configShow() {
|
|
|
1637
2563
|
`);
|
|
1638
2564
|
console.log(` User config: ${paths.userConfig}`);
|
|
1639
2565
|
console.log(` Project config: ${paths.projectConfig}`);
|
|
1640
|
-
if (
|
|
2566
|
+
if (fs5.existsSync(paths.projectConfig)) {
|
|
1641
2567
|
console.log(`
|
|
1642
2568
|
Project configuration:`);
|
|
1643
|
-
console.log(
|
|
2569
|
+
console.log(fs5.readFileSync(paths.projectConfig, "utf-8"));
|
|
1644
2570
|
}
|
|
1645
|
-
if (
|
|
2571
|
+
if (fs5.existsSync(paths.userConfig)) {
|
|
1646
2572
|
console.log(`
|
|
1647
2573
|
User configuration:`);
|
|
1648
|
-
console.log(
|
|
2574
|
+
console.log(fs5.readFileSync(paths.userConfig, "utf-8"));
|
|
1649
2575
|
}
|
|
1650
2576
|
}
|
|
1651
2577
|
function configPath() {
|
|
@@ -1807,46 +2733,64 @@ function piSubagentsCommand(rest) {
|
|
|
1807
2733
|
process.exit(1);
|
|
1808
2734
|
}
|
|
1809
2735
|
}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
switch (command) {
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
configShow();
|
|
1827
|
-
break;
|
|
1828
|
-
case "path":
|
|
1829
|
-
configPath();
|
|
1830
|
-
break;
|
|
1831
|
-
default:
|
|
1832
|
-
console.error(`Unknown config subcommand: ${args[1]}`);
|
|
1833
|
-
console.log("Available: show, path");
|
|
1834
|
-
process.exit(1);
|
|
2736
|
+
function runLegacyCli(args) {
|
|
2737
|
+
const command = args[0];
|
|
2738
|
+
switch (command) {
|
|
2739
|
+
case "list":
|
|
2740
|
+
listItems(args[1] || "skills");
|
|
2741
|
+
break;
|
|
2742
|
+
case "capabilities": {
|
|
2743
|
+
const status = runCapabilitiesCli({
|
|
2744
|
+
argv: ["systematic", ...args],
|
|
2745
|
+
errorSink: console.error,
|
|
2746
|
+
outputSink: console.log,
|
|
2747
|
+
roots: defaultCapabilityRoots()
|
|
2748
|
+
});
|
|
2749
|
+
if (status !== 0)
|
|
2750
|
+
process.exit(status);
|
|
2751
|
+
break;
|
|
1835
2752
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
2753
|
+
case "setup":
|
|
2754
|
+
setupCommand(args.slice(1));
|
|
2755
|
+
break;
|
|
2756
|
+
case "pi-subagents":
|
|
2757
|
+
piSubagentsCommand(args.slice(1));
|
|
2758
|
+
break;
|
|
2759
|
+
case "config":
|
|
2760
|
+
switch (args[1]) {
|
|
2761
|
+
case "show":
|
|
2762
|
+
case undefined:
|
|
2763
|
+
configShow();
|
|
2764
|
+
break;
|
|
2765
|
+
case "path":
|
|
2766
|
+
configPath();
|
|
2767
|
+
break;
|
|
2768
|
+
default:
|
|
2769
|
+
console.error(`Unknown config subcommand: ${args[1]}`);
|
|
2770
|
+
console.log("Available: show, path");
|
|
2771
|
+
process.exit(1);
|
|
2772
|
+
}
|
|
2773
|
+
break;
|
|
2774
|
+
case "version":
|
|
2775
|
+
case "--version":
|
|
2776
|
+
case "-v":
|
|
2777
|
+
console.log(`systematic v${VERSION}`);
|
|
2778
|
+
break;
|
|
2779
|
+
case "help":
|
|
2780
|
+
case "--help":
|
|
2781
|
+
case "-h":
|
|
2782
|
+
case undefined:
|
|
2783
|
+
console.log(HELP);
|
|
2784
|
+
break;
|
|
2785
|
+
default:
|
|
2786
|
+
console.error(`Unknown command: ${command}`);
|
|
2787
|
+
console.log(HELP);
|
|
2788
|
+
process.exit(1);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
if (import.meta.main) {
|
|
2792
|
+
runLegacyCli(process.argv.slice(2));
|
|
1852
2793
|
}
|
|
2794
|
+
export {
|
|
2795
|
+
runCapabilitiesCli
|
|
2796
|
+
};
|