@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.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-1mb4baxr.js";
16
+ } from "./index-hjbt4p5s.js";
15
17
 
16
18
  // src/cli.ts
17
- import fs4 from "fs";
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 fs2 from "fs";
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 fs from "fs";
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 = fs.readFileSync(fullPath, "utf-8");
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 = fs2.lstatSync(current);
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 = fs2.realpathSync(anchor);
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 = fs2.realpathSync(root);
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 fs2.constants.O_NOFOLLOW === "number";
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 = fs2.lstatSync(manifestPath);
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 = fs2.constants.O_RDONLY | (HAS_O_NOFOLLOW ? fs2.constants.O_NOFOLLOW : 0);
1193
+ const openFlags = fs3.constants.O_RDONLY | (HAS_O_NOFOLLOW ? fs3.constants.O_NOFOLLOW : 0);
433
1194
  try {
434
- return { kind: "fd", fd: fs2.openSync(manifestPath, openFlags) };
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 = fs2.fstatSync(fd);
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(fs2.readFileSync(fd, "utf-8"));
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
- fs2.closeSync(fd);
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 fs2.lstatSync(p);
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 = fs2.lstatSync(agentsRoot);
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 fs2.realpathSync(agentsRoot);
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: fs2.openSync(lockPath, "wx") };
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
- fs2.closeSync(fd);
1321
+ fs3.closeSync(fd);
561
1322
  } catch {}
562
1323
  try {
563
- fs2.unlinkSync(path2.join(agentsRoot, LOCK_FILENAME));
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
- fs2.writeFileSync(tmpPath, content, { flag: "w", mode: 420 });
586
- fs2.renameSync(tmpPath, destPath);
1346
+ fs3.writeFileSync(tmpPath, content, { flag: "w", mode: 420 });
1347
+ fs3.renameSync(tmpPath, destPath);
587
1348
  } catch (err) {
588
1349
  try {
589
- fs2.unlinkSync(tmpPath);
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, fs2.readFileSync(p, "utf-8"));
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 (fs2.existsSync(p))
618
- fs2.unlinkSync(p);
1378
+ if (fs3.existsSync(p))
1379
+ fs3.unlinkSync(p);
619
1380
  } else {
620
- fs2.writeFileSync(p, content, "utf-8");
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 (fs2.existsSync(path2.join(dir, "package.json")))
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 (fs2.existsSync(path2.join(candidate, "package.json")))
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 = fs2.readFileSync(path2.join(absRoot, filename), "utf-8");
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 = fs2.existsSync(path2.join(absRoot, entry.filename));
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 && fs2.existsSync(safe))
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 (fs2.existsSync(safePath) && !owned.has(entry.filename))
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 (fs2.existsSync(safePath)) {
861
- const diskHash = crypto2.createHash("sha256").update(fs2.readFileSync(safePath, "utf-8")).digest("hex");
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 = fs2.lstatSync(agentsRoot);
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
- fs2.mkdirSync(agentsRoot, { recursive: true });
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 = fs2.readFileSync(filePath, "utf-8");
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 || !fs2.existsSync(safe))
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 (fs2.existsSync(p)) {
1748
+ if (fs3.existsSync(p)) {
988
1749
  assertHashMatchesOrThrow(p, hash, filename, "remove stale file");
989
- fs2.unlinkSync(p);
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 = fs2.existsSync(safePath);
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(fs2.readFileSync(safePath, "utf-8")).digest("hex");
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 (!fs2.existsSync(absRoot))
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 = fs2.lstatSync(absRoot);
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 || !fs2.existsSync(safe))
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: fs2.existsSync(manifestPath) ? manifestPath : null
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 (!fs2.existsSync(absRootCandidate))
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 (fs2.existsSync(p)) {
1953
+ if (fs3.existsSync(p)) {
1193
1954
  assertHashMatchesOrThrow(p, hash, filename, "delete");
1194
- fs2.unlinkSync(p);
1955
+ fs3.unlinkSync(p);
1195
1956
  }
1196
1957
  }),
1197
1958
  ...manifestPath ? [
1198
1959
  () => {
1199
- if (fs2.existsSync(manifestPath))
1200
- fs2.unlinkSync(manifestPath);
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 fs3 from "fs";
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: fs3.writeFileSync,
1232
- renameSync: fs3.renameSync,
1233
- unlinkSync: fs3.unlinkSync,
1234
- chmodSync: fs3.chmodSync
1992
+ writeFileSync: fs4.writeFileSync,
1993
+ renameSync: fs4.renameSync,
1994
+ unlinkSync: fs4.unlinkSync,
1995
+ chmodSync: fs4.chmodSync
1235
1996
  };
1236
- function isRecord(value) {
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 fs3.lstatSync(targetPath);
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 = fs3.realpathSync(dir);
1266
- const realCwd = fs3.realpathSync(cwd);
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
- fs3.mkdirSync(parentDir, { recursive: true });
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 ? fs3.constants.O_RDONLY : fs3.constants.O_RDONLY | fs3.constants.O_NOFOLLOW | fs3.constants.O_NONBLOCK;
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 = fs3.openSync(targetPath, OPEN_FLAGS);
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 = fs3.fstatSync(fd);
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: fs3.readFileSync(fd), mode: stat.mode & 511 };
2092
+ return { bytes: fs4.readFileSync(fd), mode: stat.mode & 511 };
1332
2093
  } finally {
1333
- fs3.closeSync(fd);
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 (!fs3.existsSync(tempPath))
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 (!isRecord(parsed)) {
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 (isRecord(entry) && typeof entry.source === "string")
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 (!isRecord(entry))
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 (!isRecord(parsed)) {
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
- var getPackageVersion = () => {
2277
+ function readPackageMetadata(packageRoot) {
1517
2278
  try {
1518
- const packageJsonPath = path4.resolve(import.meta.dirname, "..", "package.json");
1519
- if (!fs4.existsSync(packageJsonPath))
1520
- return "unknown";
1521
- const content = fs4.readFileSync(packageJsonPath, "utf8");
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
- return parsed.version ?? "unknown";
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 "unknown";
2293
+ return {};
1526
2294
  }
1527
- };
1528
- var VERSION = getPackageVersion();
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 (fs4.existsSync(paths.projectConfig)) {
2566
+ if (fs5.existsSync(paths.projectConfig)) {
1641
2567
  console.log(`
1642
2568
  Project configuration:`);
1643
- console.log(fs4.readFileSync(paths.projectConfig, "utf-8"));
2569
+ console.log(fs5.readFileSync(paths.projectConfig, "utf-8"));
1644
2570
  }
1645
- if (fs4.existsSync(paths.userConfig)) {
2571
+ if (fs5.existsSync(paths.userConfig)) {
1646
2572
  console.log(`
1647
2573
  User configuration:`);
1648
- console.log(fs4.readFileSync(paths.userConfig, "utf-8"));
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
- var args = process.argv.slice(2);
1811
- var command = args[0];
1812
- switch (command) {
1813
- case "list":
1814
- listItems(args[1] || "skills");
1815
- break;
1816
- case "setup":
1817
- setupCommand(args.slice(1));
1818
- break;
1819
- case "pi-subagents":
1820
- piSubagentsCommand(args.slice(1));
1821
- break;
1822
- case "config":
1823
- switch (args[1]) {
1824
- case "show":
1825
- case undefined:
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
- break;
1837
- case "version":
1838
- case "--version":
1839
- case "-v":
1840
- console.log(`systematic v${VERSION}`);
1841
- break;
1842
- case "help":
1843
- case "--help":
1844
- case "-h":
1845
- case undefined:
1846
- console.log(HELP);
1847
- break;
1848
- default:
1849
- console.error(`Unknown command: ${command}`);
1850
- console.log(HELP);
1851
- process.exit(1);
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
+ };