@fro.bot/systematic 3.4.0 → 3.5.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
@@ -6,18 +6,1219 @@ import {
6
6
  findCommandsInDir,
7
7
  findSkillsInDir,
8
8
  getConfigPaths,
9
+ loadConfigWithSources,
9
10
  modify,
10
11
  parse,
12
+ parseFrontmatter,
11
13
  parseTree
12
- } from "./index-0vm17gwv.js";
14
+ } from "./index-1mb4baxr.js";
13
15
 
14
16
  // src/cli.ts
15
- import fs2 from "fs";
16
- import path2 from "path";
17
+ import fs4 from "fs";
18
+ import path4 from "path";
19
+
20
+ // src/lib/pi-subagents-export.ts
21
+ import * as crypto2 from "crypto";
22
+ import * as fs2 from "fs";
23
+ import * as os from "os";
24
+ import * as path2 from "path";
25
+ import { fileURLToPath } from "url";
26
+
27
+ // src/lib/pi-subagents-personas.ts
28
+ import * as crypto from "crypto";
29
+ import * as fs from "fs";
30
+ import * as path from "path";
31
+ var CURATED_PERSONAS = [
32
+ {
33
+ relPath: "agents/research/best-practices-researcher.md",
34
+ rationale: "Self-contained research persona. INFO: mentions skills in prose as discovery hints, but is operationally independent \u2014 no load-skill directive, no parent-dispatch assumption."
35
+ },
36
+ {
37
+ relPath: "agents/research/framework-docs-researcher.md",
38
+ rationale: "Self-contained framework documentation researcher. No Systematic-only coupling; uses standard tools (Context7, web search, file reads)."
39
+ },
40
+ {
41
+ relPath: "agents/research/git-history-analyzer.md",
42
+ rationale: "Self-contained git archaeology persona. Uses only standard git CLI + file tools. No skill or orchestration dependencies."
43
+ },
44
+ {
45
+ relPath: "agents/research/issue-intelligence-analyst.md",
46
+ rationale: "Self-contained GitHub issue analysis persona. Requires only gh CLI + standard file tools. No Systematic-only coupling."
47
+ },
48
+ {
49
+ relPath: "agents/research/repo-research-analyst.md",
50
+ rationale: "Self-contained repository research persona. Uses standard file/search/glob tools. No Systematic-specific orchestration."
51
+ },
52
+ {
53
+ relPath: "agents/review/adversarial-reviewer.md",
54
+ rationale: "Self-contained adversarial code reviewer. Fully portable \u2014 no Systematic tools or skills required."
55
+ },
56
+ {
57
+ relPath: "agents/review/api-contract-reviewer.md",
58
+ rationale: "Self-contained API contract reviewer. No Systematic-only coupling."
59
+ },
60
+ {
61
+ relPath: "agents/review/architecture-strategist.md",
62
+ rationale: "Self-contained architectural reviewer. No Systematic-only coupling."
63
+ },
64
+ {
65
+ relPath: "agents/review/cli-readiness-reviewer.md",
66
+ rationale: "Self-contained CLI readiness reviewer. No Systematic-only coupling."
67
+ },
68
+ {
69
+ relPath: "agents/review/code-simplicity-reviewer.md",
70
+ rationale: "Self-contained code simplicity reviewer. No Systematic-only coupling."
71
+ },
72
+ {
73
+ relPath: "agents/review/correctness-reviewer.md",
74
+ rationale: "Self-contained correctness reviewer. No Systematic-only coupling."
75
+ },
76
+ {
77
+ relPath: "agents/review/data-migrations-reviewer.md",
78
+ rationale: "Self-contained data migrations reviewer. No Systematic-only coupling."
79
+ },
80
+ {
81
+ relPath: "agents/review/deployment-verification-agent.md",
82
+ rationale: "Self-contained deployment verification persona. No Systematic-only coupling."
83
+ },
84
+ {
85
+ relPath: "agents/review/maintainability-reviewer.md",
86
+ rationale: "Self-contained maintainability reviewer. No Systematic-only coupling."
87
+ },
88
+ {
89
+ relPath: "agents/review/pattern-recognition-specialist.md",
90
+ rationale: "Self-contained pattern recognition reviewer. No Systematic-only coupling."
91
+ },
92
+ {
93
+ relPath: "agents/review/performance-reviewer.md",
94
+ rationale: "Self-contained performance reviewer. No Systematic-only coupling."
95
+ },
96
+ {
97
+ relPath: "agents/review/previous-comments-reviewer.md",
98
+ rationale: "Self-contained previous-comments reviewer. Reviews prior feedback. No Systematic-only coupling."
99
+ },
100
+ {
101
+ relPath: "agents/review/reliability-reviewer.md",
102
+ rationale: "Self-contained reliability reviewer. No Systematic-only coupling."
103
+ },
104
+ {
105
+ relPath: "agents/review/security-reviewer.md",
106
+ rationale: "Self-contained security reviewer. No Systematic-only coupling."
107
+ },
108
+ {
109
+ relPath: "agents/review/testing-reviewer.md",
110
+ rationale: "Self-contained testing reviewer. No Systematic-only coupling."
111
+ },
112
+ {
113
+ relPath: "agents/document-review/adversarial-document-reviewer.md",
114
+ rationale: "Self-contained document reviewer. Challenges premises in plans/specs. No Systematic-only coupling."
115
+ },
116
+ {
117
+ relPath: "agents/document-review/coherence-reviewer.md",
118
+ rationale: "Self-contained document coherence reviewer. No Systematic-only coupling."
119
+ },
120
+ {
121
+ relPath: "agents/document-review/design-lens-reviewer.md",
122
+ rationale: "Self-contained design lens reviewer. No Systematic-only coupling."
123
+ },
124
+ {
125
+ relPath: "agents/document-review/feasibility-reviewer.md",
126
+ rationale: "Self-contained feasibility reviewer. No Systematic-only coupling."
127
+ },
128
+ {
129
+ relPath: "agents/document-review/product-lens-reviewer.md",
130
+ rationale: "Self-contained product lens reviewer. No Systematic-only coupling."
131
+ },
132
+ {
133
+ relPath: "agents/document-review/scope-guardian-reviewer.md",
134
+ rationale: "Self-contained scope guardian reviewer. No Systematic-only coupling."
135
+ },
136
+ {
137
+ relPath: "agents/document-review/security-lens-reviewer.md",
138
+ rationale: "Self-contained security lens reviewer. No Systematic-only coupling."
139
+ },
140
+ {
141
+ relPath: "agents/workflow/spec-flow-analyzer.md",
142
+ rationale: "Self-contained spec flow analyzer. Analyzes specs for user flow completeness. No Systematic orchestration coupling."
143
+ },
144
+ {
145
+ relPath: "agents/workflow/bug-reproduction-validator.md",
146
+ rationale: "Self-contained bug reproduction specialist. WARNING: mentions agent-browser skill in prose as an optional tool hint, but does not require Systematic skill loading to operate."
147
+ }
148
+ ];
149
+ function sanitizeName(name) {
150
+ return name.toLowerCase().replace(/[/\\]/g, "").replace(/\./g, "").replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
151
+ }
152
+ function foldIdentity(value) {
153
+ return value.normalize("NFC").toLowerCase();
154
+ }
155
+ var CRITICAL_PATTERNS = [
156
+ {
157
+ pattern: /dispatched by a parent/i,
158
+ reason: 'References parent-dispatch assumption ("dispatched by a parent") \u2014 this persona is designed to be orchestrated by Systematic.'
159
+ },
160
+ {
161
+ pattern: /report(?:ing)? back to the orchestrator/i,
162
+ reason: "References orchestrator-report assumption \u2014 this persona is designed to operate within Systematic's orchestration loop."
163
+ },
164
+ {
165
+ pattern: /\bload\s+(?:the\s+)?systematic:/i,
166
+ reason: 'Contains explicit Systematic skill load directive ("Load systematic:...") \u2014 requires Systematic skill loading infrastructure.'
167
+ },
168
+ {
169
+ pattern: /load\s+`systematic:/i,
170
+ reason: "Contains explicit Systematic skill load directive with backtick form \u2014 requires Systematic skill loading infrastructure."
171
+ },
172
+ {
173
+ pattern: /\.\.(\/|\\)\.\.(?:\/|\\)skills\//,
174
+ reason: "Directly references Systematic internal skill paths (../../skills/...) \u2014 not portable outside Systematic."
175
+ }
176
+ ];
177
+ var WARNING_PATTERNS = [
178
+ {
179
+ pattern: /\bagent-browser\s+(?:CLI|skill)\b/i,
180
+ reason: "References agent-browser CLI/skill as an optional tool hint. Behavior may differ if agent-browser is not available, but persona remains operable without it."
181
+ },
182
+ {
183
+ pattern: /use\s+(?:the\s+)?agent-browser\s+skill/i,
184
+ reason: "Suggests using the agent-browser skill. Persona operates without it but visual verification steps would be skipped."
185
+ }
186
+ ];
187
+ function classifyCompatibility(content) {
188
+ const reasons = [];
189
+ let severity = "info";
190
+ for (const { pattern, reason } of CRITICAL_PATTERNS) {
191
+ if (pattern.test(content)) {
192
+ reasons.push(reason);
193
+ severity = "critical";
194
+ }
195
+ }
196
+ if (severity !== "critical") {
197
+ for (const { pattern, reason } of WARNING_PATTERNS) {
198
+ if (pattern.test(content)) {
199
+ reasons.push(reason);
200
+ severity = "warning";
201
+ }
202
+ }
203
+ }
204
+ return { severity, reasons };
205
+ }
206
+ function generatePersonaContent(sourceRef, rawContent) {
207
+ const { data, body, hadFrontmatter } = parseFrontmatter(rawContent);
208
+ const rawName = data.name;
209
+ if (!hadFrontmatter || rawName === undefined || rawName === null) {
210
+ throw new Error(`Persona "${sourceRef}" is missing frontmatter "name" \u2014 cannot generate pi-subagents export.`);
211
+ }
212
+ if (typeof rawName !== "string") {
213
+ throw new Error(`Persona "${sourceRef}" has a frontmatter "name" of type ${typeof rawName} (expected a non-empty string).`);
214
+ }
215
+ const trimmedName = rawName.trim();
216
+ if (trimmedName === "") {
217
+ throw new Error(`Persona "${sourceRef}" has an empty frontmatter "name" \u2014 cannot generate pi-subagents export.`);
218
+ }
219
+ const sanitized = sanitizeName(trimmedName);
220
+ if (sanitized === "") {
221
+ throw new Error(`Persona "${sourceRef}" name "${trimmedName}" sanitizes to an empty string \u2014 cannot produce a valid filename. Rename the persona to use alphanumeric characters.`);
222
+ }
223
+ const compat = classifyCompatibility(rawContent);
224
+ if (compat.severity === "critical")
225
+ return null;
226
+ const description = typeof data.description === "string" ? data.description : undefined;
227
+ const emittedFrontmatter = {};
228
+ if (description !== undefined && description.trim() !== "") {
229
+ emittedFrontmatter.description = description;
230
+ }
231
+ let frontmatterBlock;
232
+ if (Object.keys(emittedFrontmatter).length === 0) {
233
+ frontmatterBlock = `---
234
+ ---`;
235
+ } else {
236
+ const lines = Object.entries(emittedFrontmatter).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(`
237
+ `);
238
+ frontmatterBlock = `---
239
+ ${lines}
240
+ ---`;
241
+ }
242
+ const trimmedBody = body.replace(/^\n+/, "");
243
+ return `${frontmatterBlock}
244
+
245
+ ${trimmedBody}`;
246
+ }
247
+ function generatePersonaManifest(sourceRelPath, rawContent, _repoRoot) {
248
+ const { data } = parseFrontmatter(rawContent);
249
+ const rawName = data.name;
250
+ if (typeof rawName !== "string" || rawName.trim() === "") {
251
+ throw new Error(`Persona "${sourceRelPath}" is missing or has an empty frontmatter "name".`);
252
+ }
253
+ const sanitized = sanitizeName(rawName.trim());
254
+ if (sanitized === "") {
255
+ throw new Error(`Persona "${sourceRelPath}" name "${rawName.trim()}" sanitizes to an empty string \u2014 cannot produce a valid filename. Rename the persona to use alphanumeric characters.`);
256
+ }
257
+ const filename = `systematic-${sanitized}.md`;
258
+ const compat = classifyCompatibility(rawContent);
259
+ if (compat.severity === "critical") {
260
+ return {
261
+ filename,
262
+ status: "excluded-critical",
263
+ sourceRelPath,
264
+ hash: "",
265
+ reason: compat.reasons.join("; ")
266
+ };
267
+ }
268
+ const content = generatePersonaContent(sourceRelPath, rawContent);
269
+ if (content === null) {
270
+ return {
271
+ filename,
272
+ status: "excluded-critical",
273
+ sourceRelPath,
274
+ hash: "",
275
+ reason: "Compatibility screening rejected persona."
276
+ };
277
+ }
278
+ const hash = crypto.createHash("sha256").update(content).digest("hex");
279
+ const status = compat.severity === "warning" ? "exported-with-warning" : "exported";
280
+ return {
281
+ filename,
282
+ status,
283
+ sourceRelPath,
284
+ hash,
285
+ content,
286
+ reason: compat.reasons.length > 0 ? compat.reasons.join("; ") : undefined
287
+ };
288
+ }
289
+ function generateAll(repoRoot) {
290
+ const entries = [];
291
+ const byFoldedFilename = new Map;
292
+ for (const curatedEntry of CURATED_PERSONAS) {
293
+ const fullPath = path.join(repoRoot, curatedEntry.relPath);
294
+ let rawContent;
295
+ try {
296
+ rawContent = fs.readFileSync(fullPath, "utf-8");
297
+ } catch (err) {
298
+ throw new Error(`Failed to read ${curatedEntry.relPath}: ${err.message}`);
299
+ }
300
+ const entry = generatePersonaManifest(curatedEntry.relPath, rawContent, repoRoot);
301
+ entries.push(entry);
302
+ const folded = foldIdentity(entry.filename);
303
+ const existing = byFoldedFilename.get(folded) ?? [];
304
+ existing.push(curatedEntry.relPath);
305
+ byFoldedFilename.set(folded, existing);
306
+ }
307
+ const collisions = [...byFoldedFilename.entries()].filter(([, sources]) => sources.length > 1);
308
+ if (collisions.length > 0) {
309
+ const msg = collisions.map(([filename, sources]) => ` "${filename}": ${sources.join(", ")}`).join(`
310
+ `);
311
+ throw new Error(`Filename collision(s) detected in curated persona list:
312
+ ${msg}
313
+ Update CURATED_PERSONAS to resolve conflicts.`);
314
+ }
315
+ const criticals = entries.filter((e) => e.status === "excluded-critical");
316
+ if (criticals.length > 0) {
317
+ const msg = criticals.map((e) => ` ${e.sourceRelPath}: ${e.reason}`).join(`
318
+ `);
319
+ throw new Error(`Critical compatibility coupling detected in curated personas:
320
+ ${msg}
321
+ Remove these personas from CURATED_PERSONAS or fix the coupling.`);
322
+ }
323
+ return entries;
324
+ }
325
+
326
+ // src/lib/pi-subagents-export.ts
327
+ var MANIFEST_FILENAME = ".systematic-personas.json";
328
+ var LOCK_FILENAME = ".systematic-personas.lock";
329
+ var GENERATED_NAMESPACE_PATTERN = /^systematic-[a-z0-9-]+\.md$/;
330
+ function resolveAgentsRoot(scope, cwd) {
331
+ if (scope === "project")
332
+ return path2.join(cwd, ".pi", "agents");
333
+ const envDir = process.env.PI_CODING_AGENT_DIR;
334
+ const base = envDir ? envDir : path2.join(os.homedir(), ".pi", "agent");
335
+ return path2.join(base, "agents");
336
+ }
337
+ function resolveAnchor(scope, cwd) {
338
+ if (scope === "project")
339
+ return path2.resolve(cwd);
340
+ const envDir = process.env.PI_CODING_AGENT_DIR;
341
+ if (envDir)
342
+ return path2.resolve(path2.dirname(envDir));
343
+ return path2.resolve(os.homedir());
344
+ }
345
+ function anchorForScope(configOptions, agentsRootFallback) {
346
+ const resolvedAgentsRoot = path2.resolve(agentsRootFallback);
347
+ if (configOptions) {
348
+ return resolveAnchor(configOptions.scope, configOptions.cwd);
349
+ }
350
+ return path2.dirname(resolvedAgentsRoot);
351
+ }
352
+ function assertAnchoredPathSafe(anchor, targetPath) {
353
+ const resolvedAnchor = path2.resolve(anchor);
354
+ const resolvedTarget = path2.resolve(targetPath);
355
+ if (resolvedTarget !== resolvedAnchor && !resolvedTarget.startsWith(resolvedAnchor + path2.sep)) {
356
+ throw new Error(`Refusing to operate on ${resolvedTarget}: outside safety anchor ${resolvedAnchor}`);
357
+ }
358
+ const rel = path2.relative(resolvedAnchor, resolvedTarget);
359
+ const parts = rel === "" ? [] : rel.split(path2.sep);
360
+ let current = resolvedAnchor;
361
+ for (const part of parts) {
362
+ current = path2.join(current, part);
363
+ let stat;
364
+ try {
365
+ stat = fs2.lstatSync(current);
366
+ } catch {
367
+ continue;
368
+ }
369
+ if (stat.isSymbolicLink())
370
+ throw new Error(`Refusing to operate through ${current}: symlink detected`);
371
+ if (!stat.isDirectory())
372
+ throw new Error(`Refusing to operate through ${current}: not a directory`);
373
+ }
374
+ }
375
+ function assertCanonicalRootWithinAnchor(anchor, root) {
376
+ let canonicalAnchor;
377
+ try {
378
+ canonicalAnchor = fs2.realpathSync(anchor);
379
+ } catch (err) {
380
+ throw new Error(`Cannot resolve safety anchor ${anchor}: ${err instanceof Error ? err.message : String(err)}`);
381
+ }
382
+ const canonicalRoot = fs2.realpathSync(root);
383
+ if (canonicalRoot !== canonicalAnchor && !canonicalRoot.startsWith(canonicalAnchor + path2.sep)) {
384
+ throw new Error(`Refusing to operate on ${canonicalRoot}: canonical path escapes safety anchor ` + `${canonicalAnchor} (raced-symlink ancestor detected after resolution)`);
385
+ }
386
+ return canonicalRoot;
387
+ }
388
+ function validateManifestEntry(entry, seenFilenames) {
389
+ if (typeof entry !== "object" || entry === null)
390
+ return "Manifest files entry must be an object";
391
+ const e = entry;
392
+ if (typeof e.filename !== "string" || typeof e.hash !== "string")
393
+ return "Manifest files entry missing required string fields";
394
+ if (seenFilenames.has(e.filename))
395
+ return `Manifest contains duplicate filename: "${e.filename}"`;
396
+ seenFilenames.add(e.filename);
397
+ if (path2.basename(e.filename) !== e.filename || path2.isAbsolute(e.filename))
398
+ return `Unsafe filename in manifest: "${e.filename}" \u2014 contains traversal or path separators (invalid filename)`;
399
+ if (!GENERATED_NAMESPACE_PATTERN.test(e.filename))
400
+ return `Manifest entry "${e.filename}" is not a generated namespace file (must match systematic-*.md; invalid filename)`;
401
+ return null;
402
+ }
403
+ function validateManifestObject(parsed) {
404
+ if (typeof parsed !== "object" || parsed === null)
405
+ return "Manifest root must be an object";
406
+ const obj = parsed;
407
+ if (!Array.isArray(obj.files))
408
+ return 'Manifest "files" field must be an array';
409
+ const seenFilenames = new Set;
410
+ for (const entry of obj.files) {
411
+ const error = validateManifestEntry(entry, seenFilenames);
412
+ if (error !== null)
413
+ return error;
414
+ }
415
+ return null;
416
+ }
417
+ var HAS_O_NOFOLLOW = typeof fs2.constants.O_NOFOLLOW === "number";
418
+ function verifyManifestPathIdentity(manifestPath, openedStat) {
419
+ let linkStat;
420
+ try {
421
+ linkStat = fs2.lstatSync(manifestPath);
422
+ } catch (err) {
423
+ return `Cannot verify manifest path after open: ${err instanceof Error ? err.message : String(err)}`;
424
+ }
425
+ if (linkStat.isSymbolicLink())
426
+ return `Refusing to read manifest through symlink: ${manifestPath}`;
427
+ if (linkStat.dev !== openedStat.dev || linkStat.ino !== openedStat.ino)
428
+ return `Manifest path identity changed during read (possible tamper): ${manifestPath}`;
429
+ return null;
430
+ }
431
+ function openManifestFd(manifestPath) {
432
+ const openFlags = fs2.constants.O_RDONLY | (HAS_O_NOFOLLOW ? fs2.constants.O_NOFOLLOW : 0);
433
+ try {
434
+ return { kind: "fd", fd: fs2.openSync(manifestPath, openFlags) };
435
+ } catch (err) {
436
+ if (err.code === "ENOENT")
437
+ return { kind: "result", result: { kind: "absent" } };
438
+ if (err.code === "ELOOP")
439
+ return {
440
+ kind: "result",
441
+ result: {
442
+ kind: "malformed",
443
+ error: `Refusing to read manifest through symlink: ${manifestPath}`
444
+ }
445
+ };
446
+ return {
447
+ kind: "result",
448
+ result: {
449
+ kind: "malformed",
450
+ error: `Cannot read manifest: ${err instanceof Error ? err.message : String(err)}`
451
+ }
452
+ };
453
+ }
454
+ }
455
+ function parseManifestContents(raw) {
456
+ let parsed;
457
+ try {
458
+ parsed = JSON.parse(raw);
459
+ } catch (err) {
460
+ return {
461
+ kind: "malformed",
462
+ error: `Manifest JSON parse error: ${err instanceof Error ? err.message : String(err)}`
463
+ };
464
+ }
465
+ const validationError = validateManifestObject(parsed);
466
+ if (validationError !== null)
467
+ return { kind: "malformed", error: validationError };
468
+ return { kind: "ok", manifest: parsed };
469
+ }
470
+ function readManifestStrict(agentsRoot) {
471
+ const manifestPath = path2.join(agentsRoot, MANIFEST_FILENAME);
472
+ const opened = openManifestFd(manifestPath);
473
+ if (opened.kind === "result")
474
+ return opened.result;
475
+ const { fd } = opened;
476
+ try {
477
+ const stat = fs2.fstatSync(fd);
478
+ if (!stat.isFile())
479
+ return {
480
+ kind: "malformed",
481
+ error: `Manifest path exists but is not a regular file: ${manifestPath}`
482
+ };
483
+ if (!HAS_O_NOFOLLOW) {
484
+ const identityError = verifyManifestPathIdentity(manifestPath, stat);
485
+ if (identityError !== null)
486
+ return { kind: "malformed", error: identityError };
487
+ }
488
+ return parseManifestContents(fs2.readFileSync(fd, "utf-8"));
489
+ } catch (err) {
490
+ return {
491
+ kind: "malformed",
492
+ error: `Cannot read manifest: ${err instanceof Error ? err.message : String(err)}`
493
+ };
494
+ } finally {
495
+ fs2.closeSync(fd);
496
+ }
497
+ }
498
+ function writeManifest(agentsRoot, manifest) {
499
+ atomicWriteString(path2.join(agentsRoot, MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}
500
+ `);
501
+ }
502
+ function statOrNull(p) {
503
+ try {
504
+ return fs2.lstatSync(p);
505
+ } catch {
506
+ return null;
507
+ }
508
+ }
509
+ function assertRealAgentsRoot(agentsRoot) {
510
+ let stat = null;
511
+ try {
512
+ stat = fs2.lstatSync(agentsRoot);
513
+ } catch {}
514
+ if (stat !== null) {
515
+ if (stat.isSymbolicLink())
516
+ throw new Error(`Refusing to write under ${agentsRoot}: not a real directory (symlink detected)`);
517
+ if (!stat.isDirectory())
518
+ throw new Error(`Refusing to write under ${agentsRoot}: not a directory`);
519
+ }
520
+ return fs2.realpathSync(agentsRoot);
521
+ }
522
+ function safeFilePathOrThrow(agentsRoot, filename) {
523
+ if (path2.basename(filename) !== filename || path2.isAbsolute(filename)) {
524
+ throw new Error(`Unsafe filename in manifest: "${filename}" \u2014 contains traversal or path separators (invalid filename)`);
525
+ }
526
+ const resolved = path2.resolve(agentsRoot, filename);
527
+ if (!resolved.startsWith(agentsRoot + path2.sep) && resolved !== agentsRoot) {
528
+ throw new Error(`Unsafe filename in manifest: "${filename}" resolves outside agents root (traversal)`);
529
+ }
530
+ return resolved;
531
+ }
532
+ function safeFilePath(agentsRoot, filename) {
533
+ try {
534
+ return safeFilePathOrThrow(agentsRoot, filename);
535
+ } catch {
536
+ return null;
537
+ }
538
+ }
539
+ function acquireLock(agentsRoot) {
540
+ const lockPath = path2.join(agentsRoot, LOCK_FILENAME);
541
+ try {
542
+ return { ok: true, fd: fs2.openSync(lockPath, "wx") };
543
+ } catch (err) {
544
+ if (err.code === "EEXIST") {
545
+ return {
546
+ ok: false,
547
+ contention: true,
548
+ error: `Another export/refresh/cleanup operation appears to be in progress ` + `(lock file exists: ${lockPath}). If no other operation is running ` + `(e.g. after a crash), verify manually and remove the lock file yourself.`
549
+ };
550
+ }
551
+ return {
552
+ ok: false,
553
+ contention: false,
554
+ error: err instanceof Error ? err.message : String(err)
555
+ };
556
+ }
557
+ }
558
+ function releaseLock(agentsRoot, fd) {
559
+ try {
560
+ fs2.closeSync(fd);
561
+ } catch {}
562
+ try {
563
+ fs2.unlinkSync(path2.join(agentsRoot, LOCK_FILENAME));
564
+ } catch {}
565
+ }
566
+ function withLock(agentsRoot, fn) {
567
+ const acquired = acquireLock(agentsRoot);
568
+ if (!acquired.ok) {
569
+ return {
570
+ locked: false,
571
+ error: acquired.error,
572
+ isContention: acquired.contention
573
+ };
574
+ }
575
+ try {
576
+ return { locked: true, result: fn() };
577
+ } finally {
578
+ releaseLock(agentsRoot, acquired.fd);
579
+ }
580
+ }
581
+ function atomicWriteString(destPath, content) {
582
+ const parentDir = path2.dirname(destPath);
583
+ const tmpPath = path2.join(parentDir, `.${path2.basename(destPath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
584
+ try {
585
+ fs2.writeFileSync(tmpPath, content, { flag: "w", mode: 420 });
586
+ fs2.renameSync(tmpPath, destPath);
587
+ } catch (err) {
588
+ try {
589
+ fs2.unlinkSync(tmpPath);
590
+ } catch {}
591
+ throw err;
592
+ }
593
+ }
594
+ function snapshotFiles(filePaths) {
595
+ const backup = new Map;
596
+ for (const p of filePaths) {
597
+ try {
598
+ backup.set(p, fs2.readFileSync(p, "utf-8"));
599
+ } catch (err) {
600
+ if (err.code === "ENOENT") {
601
+ backup.set(p, undefined);
602
+ continue;
603
+ }
604
+ return {
605
+ ok: false,
606
+ error: `Cannot snapshot ${p}: ${err instanceof Error ? err.message : String(err)}`
607
+ };
608
+ }
609
+ }
610
+ return { ok: true, backup };
611
+ }
612
+ function restoreFromBackup(backup) {
613
+ const failed = [];
614
+ for (const [p, content] of backup) {
615
+ try {
616
+ if (content === undefined) {
617
+ if (fs2.existsSync(p))
618
+ fs2.unlinkSync(p);
619
+ } else {
620
+ fs2.writeFileSync(p, content, "utf-8");
621
+ }
622
+ } catch {
623
+ failed.push(p);
624
+ }
625
+ }
626
+ return failed;
627
+ }
628
+ function rollbackMessage(failed) {
629
+ return failed.length === 0 ? "rolled back" : `partial rollback (restoration failed for: ${failed.map((p) => path2.basename(p)).join(", ")})`;
630
+ }
631
+ function runWithRollback(pathsToWatch, ops) {
632
+ const snapshot = snapshotFiles(pathsToWatch);
633
+ if (!snapshot.ok) {
634
+ return { ok: false, error: snapshot.error, rollbackFailed: [] };
635
+ }
636
+ const backup = snapshot.backup;
637
+ for (const op of ops) {
638
+ try {
639
+ op();
640
+ } catch (err) {
641
+ const rollbackFailed = restoreFromBackup(backup);
642
+ const rb = rollbackMessage(rollbackFailed);
643
+ return {
644
+ ok: false,
645
+ error: `${err instanceof Error ? err.message : String(err)} (${rb})`,
646
+ rollbackFailed
647
+ };
648
+ }
649
+ }
650
+ return { ok: true, rollbackFailed: [] };
651
+ }
652
+ function ownedFilenames(manifest) {
653
+ if (manifest === null)
654
+ return new Set;
655
+ const s = new Set;
656
+ for (const entry of manifest.files) {
657
+ if (path2.basename(entry.filename) === entry.filename && !path2.isAbsolute(entry.filename))
658
+ s.add(entry.filename);
659
+ }
660
+ return s;
661
+ }
662
+ function parseSourceCategoryAndKey(sourceRelPath) {
663
+ const parts = sourceRelPath.split("/");
664
+ if (parts.length >= 3 && parts[0] === "agents") {
665
+ const fileName2 = parts.at(-1) ?? "";
666
+ return {
667
+ category: parts[1],
668
+ agentKey: fileName2.replace(/\.md$/, "")
669
+ };
670
+ }
671
+ const fileName = parts.at(-1) ?? sourceRelPath;
672
+ return { category: undefined, agentKey: fileName.replace(/\.md$/, "") };
673
+ }
674
+ function lookupOverlay(map, category, agentKey) {
675
+ if (!map)
676
+ return;
677
+ if (category) {
678
+ const qualified = map[`${category}/${agentKey}`];
679
+ if (qualified)
680
+ return qualified;
681
+ }
682
+ return map[agentKey];
683
+ }
684
+ function resolveModel(config, category, agentKey) {
685
+ const agentOverlay = lookupOverlay(config.agents, category, agentKey);
686
+ if (agentOverlay && Object.hasOwn(agentOverlay, "model")) {
687
+ const value = agentOverlay.model;
688
+ return typeof value === "string" ? value : undefined;
689
+ }
690
+ const categoryOverlay = category ? config.categories?.[category] : undefined;
691
+ if (categoryOverlay && Object.hasOwn(categoryOverlay, "model")) {
692
+ const value = categoryOverlay.model;
693
+ return typeof value === "string" ? value : undefined;
694
+ }
695
+ return;
696
+ }
697
+ var PI_SUBAGENTS_FRONTMATTER_FIELDS = [
698
+ "thinking",
699
+ "max_turns",
700
+ "tools",
701
+ "skills"
702
+ ];
703
+ function resolvePiSubagentsFields(config, category, agentKey) {
704
+ const categoryOverlay = category ? config.pi_subagents?.categories?.[category] : undefined;
705
+ const agentOverlay = lookupOverlay(config.pi_subagents?.agents, category, agentKey);
706
+ const result = {};
707
+ for (const field of PI_SUBAGENTS_FRONTMATTER_FIELDS) {
708
+ if (agentOverlay && Object.hasOwn(agentOverlay, field)) {
709
+ result[field] = agentOverlay[field];
710
+ } else if (categoryOverlay && Object.hasOwn(categoryOverlay, field)) {
711
+ result[field] = categoryOverlay[field];
712
+ }
713
+ }
714
+ return result;
715
+ }
716
+ function applyConfigToContent(content, config, category, agentKey) {
717
+ const { data, body } = parseFrontmatter(content);
718
+ const model = resolveModel(config, category, agentKey);
719
+ const piSubagentsFields = resolvePiSubagentsFields(config, category, agentKey);
720
+ const frontmatter = {};
721
+ if (typeof data.description === "string") {
722
+ frontmatter.description = data.description;
723
+ }
724
+ if (model !== undefined)
725
+ frontmatter.model = model;
726
+ for (const field of PI_SUBAGENTS_FRONTMATTER_FIELDS) {
727
+ if (Object.hasOwn(piSubagentsFields, field)) {
728
+ frontmatter[field] = piSubagentsFields[field];
729
+ }
730
+ }
731
+ let frontmatterBlock;
732
+ if (Object.keys(frontmatter).length === 0) {
733
+ frontmatterBlock = `---
734
+ ---`;
735
+ } else {
736
+ const lines = Object.entries(frontmatter).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(`
737
+ `);
738
+ frontmatterBlock = `---
739
+ ${lines}
740
+ ---`;
741
+ }
742
+ const trimmedBody = body.replace(/^\n+/, "");
743
+ return `${frontmatterBlock}
744
+
745
+ ${trimmedBody}`;
746
+ }
747
+ function applyConfigToEntries(entries, config) {
748
+ return entries.map((entry) => {
749
+ if (entry.status === "excluded-critical" || !entry.content)
750
+ return entry;
751
+ const { category, agentKey } = parseSourceCategoryAndKey(entry.sourceRelPath);
752
+ const content = applyConfigToContent(entry.content, config, category, agentKey);
753
+ const hash = crypto2.createHash("sha256").update(content).digest("hex");
754
+ return { ...entry, content, hash };
755
+ });
756
+ }
757
+ function generateEntries(repoRoot, configOptions) {
758
+ const entries = generateAll(repoRoot);
759
+ if (!configOptions)
760
+ return entries;
761
+ const config = loadConfigWithSources(configOptions.cwd, {
762
+ includeProject: configOptions.scope === "project"
763
+ }).config;
764
+ return applyConfigToEntries(entries, config);
765
+ }
766
+ function findPackageRoot() {
767
+ const thisFile = fileURLToPath(import.meta.url);
768
+ const dir = path2.resolve(path2.dirname(thisFile), "..", "..");
769
+ if (fs2.existsSync(path2.join(dir, "package.json")))
770
+ return dir;
771
+ let candidate = path2.dirname(thisFile);
772
+ for (let i = 0;i < 8; i++) {
773
+ if (fs2.existsSync(path2.join(candidate, "package.json")))
774
+ return candidate;
775
+ const parent = path2.dirname(candidate);
776
+ if (parent === candidate)
777
+ break;
778
+ candidate = parent;
779
+ }
780
+ throw new Error("Could not locate package root (no package.json found)");
781
+ }
782
+ function planExistingEntry(absRoot, filename, hash, owned) {
783
+ const diskContent = fs2.readFileSync(path2.join(absRoot, filename), "utf-8");
784
+ const diskHash = crypto2.createHash("sha256").update(diskContent).digest("hex");
785
+ if (owned.has(filename)) {
786
+ return diskHash !== hash ? { action: "update", filename } : { action: "skip", filename };
787
+ }
788
+ return {
789
+ action: "refuse",
790
+ filename,
791
+ reason: "File exists but is not owned by a previous export"
792
+ };
793
+ }
794
+ function planEntryActions(absRoot, entries, owned) {
795
+ const actions = [];
796
+ for (const entry of entries) {
797
+ if (entry.status === "excluded-critical" || !entry.content)
798
+ continue;
799
+ const exists = fs2.existsSync(path2.join(absRoot, entry.filename));
800
+ actions.push(exists ? planExistingEntry(absRoot, entry.filename, entry.hash, owned) : { action: "create", filename: entry.filename });
801
+ }
802
+ return actions;
803
+ }
804
+ function planRemoveActions(absRoot, manifest, currentFilenames) {
805
+ if (!manifest)
806
+ return [];
807
+ const removes = [];
808
+ for (const mEntry of manifest.files) {
809
+ if (currentFilenames.has(mEntry.filename))
810
+ continue;
811
+ const safe = safeFilePath(absRoot, mEntry.filename);
812
+ if (safe !== null && fs2.existsSync(safe))
813
+ removes.push({ action: "remove", filename: mEntry.filename });
814
+ }
815
+ return removes;
816
+ }
817
+ function preview(agentsRoot, configOptions) {
818
+ const absRoot = path2.resolve(agentsRoot);
819
+ const manifestResult = readManifestStrict(absRoot);
820
+ if (manifestResult.kind === "malformed") {
821
+ return {
822
+ status: "error",
823
+ error: `Malformed manifest: ${manifestResult.error}`,
824
+ agentsRoot: absRoot,
825
+ actions: []
826
+ };
827
+ }
828
+ const manifest = manifestResult.kind === "ok" ? manifestResult.manifest : null;
829
+ let entries;
830
+ try {
831
+ entries = generateEntries(findPackageRoot(), configOptions);
832
+ } catch (err) {
833
+ return {
834
+ status: "error",
835
+ error: `Generator failed: ${err instanceof Error ? err.message : String(err)}`,
836
+ agentsRoot: absRoot,
837
+ actions: []
838
+ };
839
+ }
840
+ const owned = ownedFilenames(manifest);
841
+ const currentFilenames = new Set(entries.filter((e) => e.status !== "excluded-critical" && e.content).map((e) => e.filename));
842
+ return {
843
+ status: "ok",
844
+ agentsRoot: absRoot,
845
+ actions: [
846
+ ...planEntryActions(absRoot, entries, owned),
847
+ ...planRemoveActions(absRoot, manifest, currentFilenames)
848
+ ]
849
+ };
850
+ }
851
+ function classifyOneExportEntry(absRoot, entry, owned) {
852
+ const safePath = safeFilePath(absRoot, entry.filename);
853
+ if (safePath === null)
854
+ return { kind: "refuse", reason: "Filename would escape agents root" };
855
+ if (fs2.existsSync(safePath) && !owned.has(entry.filename))
856
+ return {
857
+ kind: "refuse",
858
+ reason: "File exists but is not owned by a previous export (user file)"
859
+ };
860
+ if (fs2.existsSync(safePath)) {
861
+ const diskHash = crypto2.createHash("sha256").update(fs2.readFileSync(safePath, "utf-8")).digest("hex");
862
+ if (diskHash === entry.hash)
863
+ return { kind: "skip", safePath };
864
+ }
865
+ return { kind: "write", safePath };
866
+ }
867
+ function classifyExportEntries(absRoot, entries, owned) {
868
+ const toWrite = [];
869
+ const toSkip = [];
870
+ const refused = [];
871
+ const currentFilenames = new Set;
872
+ for (const entry of entries) {
873
+ if (entry.status === "excluded-critical" || !entry.content)
874
+ continue;
875
+ currentFilenames.add(entry.filename);
876
+ const result = classifyOneExportEntry(absRoot, entry, owned);
877
+ if (result.kind === "refuse") {
878
+ refused.push({
879
+ filename: entry.filename,
880
+ reason: result.reason ?? "refused"
881
+ });
882
+ } else if (result.safePath !== undefined && result.kind === "skip") {
883
+ toSkip.push({ entry, safePath: result.safePath });
884
+ } else if (result.safePath !== undefined) {
885
+ toWrite.push({ entry, safePath: result.safePath });
886
+ }
887
+ }
888
+ return { toWrite, toSkip, refused, currentFilenames };
889
+ }
890
+ function buildNewManifestFiles(toWrite, toSkip) {
891
+ const toFileEntry = (e) => ({
892
+ filename: e.filename,
893
+ hash: e.hash,
894
+ status: e.status === "exported-with-warning" ? "exported-with-warning" : "exported"
895
+ });
896
+ return [
897
+ ...toSkip.map(({ entry }) => toFileEntry(entry)),
898
+ ...toWrite.map(({ entry }) => toFileEntry(entry))
899
+ ];
900
+ }
901
+ function makeExportError(error) {
902
+ return { status: "error", written: 0, skipped: 0, refused: [], error };
903
+ }
904
+ function prepareExportRoot(agentsRoot) {
905
+ let preStat = null;
906
+ try {
907
+ preStat = fs2.lstatSync(agentsRoot);
908
+ } catch {}
909
+ if (preStat?.isSymbolicLink())
910
+ return makeExportError(`Refusing to write under ${agentsRoot}: not a real directory (symlink detected)`);
911
+ try {
912
+ fs2.mkdirSync(agentsRoot, { recursive: true });
913
+ return assertRealAgentsRoot(agentsRoot);
914
+ } catch (err) {
915
+ return makeExportError(err instanceof Error ? err.message : String(err));
916
+ }
917
+ }
918
+ function assertHashMatchesOrThrow(filePath, expectedHash, filename, verb) {
919
+ const diskContent = fs2.readFileSync(filePath, "utf-8");
920
+ const diskHash = crypto2.createHash("sha256").update(diskContent).digest("hex");
921
+ if (diskHash !== expectedHash) {
922
+ 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.`);
923
+ }
924
+ }
925
+ function findStalePaths(absRoot, prevManifest, currentFilenames) {
926
+ if (!prevManifest)
927
+ return [];
928
+ const stale = [];
929
+ for (const mEntry of prevManifest.files) {
930
+ if (currentFilenames.has(mEntry.filename))
931
+ continue;
932
+ const safe = safeFilePath(absRoot, mEntry.filename);
933
+ if (safe === null || !fs2.existsSync(safe))
934
+ continue;
935
+ assertHashMatchesOrThrow(safe, mEntry.hash, mEntry.filename, "remove stale file");
936
+ stale.push({ path: safe, filename: mEntry.filename, hash: mEntry.hash });
937
+ }
938
+ return stale;
939
+ }
940
+ function exportPersonas(agentsRoot, configOptions) {
941
+ let entries;
942
+ try {
943
+ entries = generateEntries(findPackageRoot(), configOptions);
944
+ } catch (err) {
945
+ return makeExportError(`Failed to generate personas: ${err instanceof Error ? err.message : String(err)}`);
946
+ }
947
+ const anchor = anchorForScope(configOptions, agentsRoot);
948
+ try {
949
+ assertAnchoredPathSafe(anchor, path2.resolve(agentsRoot));
950
+ } catch (err) {
951
+ return makeExportError(err instanceof Error ? err.message : String(err));
952
+ }
953
+ const rootOrErr = prepareExportRoot(agentsRoot);
954
+ if (typeof rootOrErr !== "string")
955
+ return rootOrErr;
956
+ let absRoot;
957
+ try {
958
+ absRoot = assertCanonicalRootWithinAnchor(anchor, rootOrErr);
959
+ } catch (err) {
960
+ return makeExportError(err instanceof Error ? err.message : String(err));
961
+ }
962
+ const lockResult = withLock(absRoot, () => {
963
+ const manifestResult = readManifestStrict(absRoot);
964
+ if (manifestResult.kind === "malformed")
965
+ return makeExportError(`Malformed manifest \u2014 refusing to export: ${manifestResult.error}`);
966
+ const prevManifest = manifestResult.kind === "ok" ? manifestResult.manifest : null;
967
+ const owned = ownedFilenames(prevManifest);
968
+ const { toWrite, toSkip, refused, currentFilenames } = classifyExportEntries(absRoot, entries, owned);
969
+ let staleOwned;
970
+ try {
971
+ staleOwned = findStalePaths(absRoot, prevManifest, currentFilenames);
972
+ } catch (err) {
973
+ return makeExportError(err instanceof Error ? err.message : String(err));
974
+ }
975
+ const newManifest = () => ({
976
+ generatedAt: new Date().toISOString(),
977
+ agentsRoot: absRoot,
978
+ files: buildNewManifestFiles(toWrite, toSkip)
979
+ });
980
+ const tx = runWithRollback([
981
+ ...toWrite.map((x) => x.safePath),
982
+ ...staleOwned.map((x) => x.path),
983
+ path2.join(absRoot, MANIFEST_FILENAME)
984
+ ], [
985
+ ...toWrite.map(({ entry, safePath }) => () => atomicWriteString(safePath, entry.content ?? "")),
986
+ ...staleOwned.map(({ path: p, filename, hash }) => () => {
987
+ if (fs2.existsSync(p)) {
988
+ assertHashMatchesOrThrow(p, hash, filename, "remove stale file");
989
+ fs2.unlinkSync(p);
990
+ }
991
+ }),
992
+ () => writeManifest(absRoot, newManifest())
993
+ ]);
994
+ if (!tx.ok)
995
+ return {
996
+ status: "error",
997
+ written: 0,
998
+ skipped: toSkip.length,
999
+ refused,
1000
+ error: tx.error
1001
+ };
1002
+ return {
1003
+ status: "ok",
1004
+ written: toWrite.length,
1005
+ skipped: toSkip.length,
1006
+ refused
1007
+ };
1008
+ });
1009
+ if (!lockResult.locked)
1010
+ return makeExportError(lockResult.error);
1011
+ return lockResult.result;
1012
+ }
1013
+ function classifyOneRefreshEntry(absRoot, entry, owned) {
1014
+ const safePath = safeFilePath(absRoot, entry.filename);
1015
+ if (safePath === null)
1016
+ return { kind: "ignore" };
1017
+ const exists = fs2.existsSync(safePath);
1018
+ if (exists && !owned.has(entry.filename))
1019
+ return { kind: "skip-unowned", safePath };
1020
+ if (!exists && !owned.has(entry.filename))
1021
+ return { kind: "skip-unexported" };
1022
+ if (exists) {
1023
+ const diskHash = crypto2.createHash("sha256").update(fs2.readFileSync(safePath, "utf-8")).digest("hex");
1024
+ if (diskHash === entry.hash)
1025
+ return { kind: "keep", safePath };
1026
+ }
1027
+ return { kind: "update", safePath };
1028
+ }
1029
+ function classifyRefreshEntries(absRoot, entries, owned) {
1030
+ const toUpdate = [];
1031
+ const toKeep = [];
1032
+ let skippedUnowned = 0;
1033
+ for (const entry of entries) {
1034
+ if (entry.status === "excluded-critical" || !entry.content)
1035
+ continue;
1036
+ const result = classifyOneRefreshEntry(absRoot, entry, owned);
1037
+ if (result.kind === "skip-unowned") {
1038
+ skippedUnowned++;
1039
+ continue;
1040
+ }
1041
+ if (result.kind === "ignore" || result.kind === "skip-unexported")
1042
+ continue;
1043
+ if (result.safePath !== undefined && result.kind === "keep")
1044
+ toKeep.push({ entry, safePath: result.safePath });
1045
+ else if (result.safePath !== undefined)
1046
+ toUpdate.push({ entry, safePath: result.safePath });
1047
+ }
1048
+ return { toUpdate, toKeep, skippedUnowned };
1049
+ }
1050
+ function validateRefreshRoot(agentsRoot) {
1051
+ const absRoot = path2.resolve(agentsRoot);
1052
+ if (!fs2.existsSync(absRoot))
1053
+ return {
1054
+ status: "error",
1055
+ updated: 0,
1056
+ skippedUnowned: 0,
1057
+ error: `Agents root does not exist: ${absRoot}. Run export first.`
1058
+ };
1059
+ try {
1060
+ const stat = fs2.lstatSync(absRoot);
1061
+ if (stat.isSymbolicLink())
1062
+ return {
1063
+ status: "error",
1064
+ updated: 0,
1065
+ skippedUnowned: 0,
1066
+ error: `Refusing to write under ${absRoot}: not a real directory (symlink)`
1067
+ };
1068
+ } catch (err) {
1069
+ return {
1070
+ status: "error",
1071
+ updated: 0,
1072
+ skippedUnowned: 0,
1073
+ error: err instanceof Error ? err.message : String(err)
1074
+ };
1075
+ }
1076
+ return absRoot;
1077
+ }
1078
+ function refresh(agentsRoot, configOptions) {
1079
+ let entries;
1080
+ try {
1081
+ entries = generateEntries(findPackageRoot(), configOptions);
1082
+ } catch (err) {
1083
+ return {
1084
+ status: "error",
1085
+ updated: 0,
1086
+ skippedUnowned: 0,
1087
+ error: `Failed to generate personas: ${err instanceof Error ? err.message : String(err)}`
1088
+ };
1089
+ }
1090
+ const absRootCandidate = path2.resolve(agentsRoot);
1091
+ const anchor = anchorForScope(configOptions, absRootCandidate);
1092
+ try {
1093
+ assertAnchoredPathSafe(anchor, absRootCandidate);
1094
+ } catch (err) {
1095
+ return {
1096
+ status: "error",
1097
+ updated: 0,
1098
+ skippedUnowned: 0,
1099
+ error: err instanceof Error ? err.message : String(err)
1100
+ };
1101
+ }
1102
+ const rootOrErr = validateRefreshRoot(agentsRoot);
1103
+ if (typeof rootOrErr !== "string")
1104
+ return rootOrErr;
1105
+ let absRoot;
1106
+ try {
1107
+ absRoot = assertCanonicalRootWithinAnchor(anchor, rootOrErr);
1108
+ } catch (err) {
1109
+ return {
1110
+ status: "error",
1111
+ updated: 0,
1112
+ skippedUnowned: 0,
1113
+ error: err instanceof Error ? err.message : String(err)
1114
+ };
1115
+ }
1116
+ const lockResult = withLock(absRoot, () => {
1117
+ const manifestResult = readManifestStrict(absRoot);
1118
+ if (manifestResult.kind === "malformed")
1119
+ return {
1120
+ status: "error",
1121
+ updated: 0,
1122
+ skippedUnowned: 0,
1123
+ error: `Malformed manifest \u2014 refusing to refresh: ${manifestResult.error}`
1124
+ };
1125
+ const manifest = manifestResult.kind === "ok" ? manifestResult.manifest : null;
1126
+ const { toUpdate, toKeep, skippedUnowned } = classifyRefreshEntries(absRoot, entries, ownedFilenames(manifest));
1127
+ const needsManifest = manifest !== null || toUpdate.length + toKeep.length > 0;
1128
+ const newManifest = () => ({
1129
+ generatedAt: new Date().toISOString(),
1130
+ agentsRoot: absRoot,
1131
+ files: buildNewManifestFiles(toUpdate, toKeep)
1132
+ });
1133
+ const tx = runWithRollback([
1134
+ ...toUpdate.map((x) => x.safePath),
1135
+ path2.join(absRoot, MANIFEST_FILENAME)
1136
+ ], [
1137
+ ...toUpdate.map(({ entry, safePath }) => () => atomicWriteString(safePath, entry.content ?? "")),
1138
+ ...needsManifest ? [() => writeManifest(absRoot, newManifest())] : []
1139
+ ]);
1140
+ if (!tx.ok)
1141
+ return { status: "error", updated: 0, skippedUnowned, error: tx.error };
1142
+ return { status: "ok", updated: toUpdate.length, skippedUnowned };
1143
+ });
1144
+ if (!lockResult.locked)
1145
+ return {
1146
+ status: "error",
1147
+ updated: 0,
1148
+ skippedUnowned: 0,
1149
+ error: lockResult.error
1150
+ };
1151
+ return lockResult.result;
1152
+ }
1153
+ function collectCleanupPaths(absRoot, manifest) {
1154
+ const targets = [];
1155
+ for (const entry of manifest.files) {
1156
+ const safe = safeFilePath(absRoot, entry.filename);
1157
+ if (safe === null || !fs2.existsSync(safe))
1158
+ continue;
1159
+ assertHashMatchesOrThrow(safe, entry.hash, entry.filename, "delete");
1160
+ targets.push({ path: safe, filename: entry.filename, hash: entry.hash });
1161
+ }
1162
+ const manifestPath = path2.join(absRoot, MANIFEST_FILENAME);
1163
+ return {
1164
+ targets,
1165
+ manifestPath: fs2.existsSync(manifestPath) ? manifestPath : null
1166
+ };
1167
+ }
1168
+ function cleanup(agentsRoot, configOptions) {
1169
+ const absRootCandidate = path2.resolve(agentsRoot);
1170
+ const preStat = statOrNull(absRootCandidate);
1171
+ if (preStat?.isSymbolicLink())
1172
+ throw new Error(`Refusing to operate on ${absRootCandidate}: not a real directory (symlink detected)`);
1173
+ const anchor = anchorForScope(configOptions, absRootCandidate);
1174
+ assertAnchoredPathSafe(anchor, absRootCandidate);
1175
+ if (!fs2.existsSync(absRootCandidate))
1176
+ return { status: "ok" };
1177
+ const absRoot = assertCanonicalRootWithinAnchor(anchor, absRootCandidate);
1178
+ const lockResult = withLock(absRoot, () => {
1179
+ const manifestResult = readManifestStrict(absRoot);
1180
+ if (manifestResult.kind === "absent")
1181
+ return { kind: "absent" };
1182
+ if (manifestResult.kind === "malformed")
1183
+ throw new Error(`Malformed manifest \u2014 refusing to cleanup: ${manifestResult.error}`);
1184
+ const manifest = manifestResult.manifest;
1185
+ const { targets, manifestPath } = collectCleanupPaths(absRoot, manifest);
1186
+ const pathsToWatch = [
1187
+ ...targets.map((t) => t.path),
1188
+ ...manifestPath ? [manifestPath] : []
1189
+ ];
1190
+ const tx2 = runWithRollback(pathsToWatch, [
1191
+ ...targets.map(({ path: p, filename, hash }) => () => {
1192
+ if (fs2.existsSync(p)) {
1193
+ assertHashMatchesOrThrow(p, hash, filename, "delete");
1194
+ fs2.unlinkSync(p);
1195
+ }
1196
+ }),
1197
+ ...manifestPath ? [
1198
+ () => {
1199
+ if (fs2.existsSync(manifestPath))
1200
+ fs2.unlinkSync(manifestPath);
1201
+ }
1202
+ ] : []
1203
+ ]);
1204
+ return { kind: "tx", tx: tx2 };
1205
+ });
1206
+ if (!lockResult.locked) {
1207
+ if (lockResult.isContention)
1208
+ throw new Error(lockResult.error);
1209
+ return { status: "error", error: lockResult.error };
1210
+ }
1211
+ if (lockResult.result.kind === "absent")
1212
+ return { status: "ok" };
1213
+ const tx = lockResult.result.tx;
1214
+ if (!tx.ok)
1215
+ return { status: "error", error: tx.error };
1216
+ return { status: "ok" };
1217
+ }
17
1218
 
18
1219
  // src/lib/setup.ts
19
- import fs from "fs";
20
- import path from "path";
1220
+ import fs3 from "fs";
1221
+ import path3 from "path";
21
1222
  var SYSTEMATIC_PACKAGE_NAME = "@fro.bot/systematic";
22
1223
  var PI_PACKAGE_IDENTIFIER = `npm:${SYSTEMATIC_PACKAGE_NAME}`;
23
1224
  var SETUP_ERROR_NAME = "SetupError";
@@ -27,10 +1228,10 @@ function createSetupError(message) {
27
1228
  return error;
28
1229
  }
29
1230
  var DEFAULT_OPS = {
30
- writeFileSync: fs.writeFileSync,
31
- renameSync: fs.renameSync,
32
- unlinkSync: fs.unlinkSync,
33
- chmodSync: fs.chmodSync
1231
+ writeFileSync: fs3.writeFileSync,
1232
+ renameSync: fs3.renameSync,
1233
+ unlinkSync: fs3.unlinkSync,
1234
+ chmodSync: fs3.chmodSync
34
1235
  };
35
1236
  function isRecord(value) {
36
1237
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -52,7 +1253,7 @@ function isPiSystematicIdentifier(identifier) {
52
1253
  }
53
1254
  function lstatOrNull(targetPath) {
54
1255
  try {
55
- return fs.lstatSync(targetPath);
1256
+ return fs3.lstatSync(targetPath);
56
1257
  } catch (error) {
57
1258
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
58
1259
  return null;
@@ -61,10 +1262,10 @@ function lstatOrNull(targetPath) {
61
1262
  }
62
1263
  }
63
1264
  function assertRealpathUnderCwd(dir, cwd) {
64
- const realDir = fs.realpathSync(dir);
65
- const realCwd = fs.realpathSync(cwd);
66
- const relative = path.relative(realCwd, realDir);
67
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
1265
+ const realDir = fs3.realpathSync(dir);
1266
+ const realCwd = fs3.realpathSync(cwd);
1267
+ const relative2 = path3.relative(realCwd, realDir);
1268
+ if (relative2.startsWith("..") || path3.isAbsolute(relative2)) {
68
1269
  throw createSetupError(`Refusing to write under ${dir}: resolved directory escapes the project root`);
69
1270
  }
70
1271
  }
@@ -77,7 +1278,7 @@ function assertParentTrusted(parentDir, cwd) {
77
1278
  assertRealpathUnderCwd(parentDir, cwd);
78
1279
  return;
79
1280
  }
80
- fs.mkdirSync(parentDir, { recursive: true });
1281
+ fs3.mkdirSync(parentDir, { recursive: true });
81
1282
  assertRealpathUnderCwd(parentDir, cwd);
82
1283
  }
83
1284
  var OPENCODE_TARGET_CANDIDATES = [
@@ -87,19 +1288,19 @@ var OPENCODE_TARGET_CANDIDATES = [
87
1288
  "opencode.json"
88
1289
  ];
89
1290
  function resolveOpenCodeTargetPath(cwd) {
90
- const opencodeDirStat = lstatOrNull(path.join(cwd, ".opencode"));
1291
+ const opencodeDirStat = lstatOrNull(path3.join(cwd, ".opencode"));
91
1292
  if (opencodeDirStat?.isSymbolicLink()) {
92
- return path.join(cwd, OPENCODE_TARGET_CANDIDATES[0]);
1293
+ return path3.join(cwd, OPENCODE_TARGET_CANDIDATES[0]);
93
1294
  }
94
1295
  for (const candidate of OPENCODE_TARGET_CANDIDATES) {
95
- const candidatePath = path.join(cwd, candidate);
1296
+ const candidatePath = path3.join(cwd, candidate);
96
1297
  if (lstatOrNull(candidatePath) !== null)
97
1298
  return candidatePath;
98
1299
  }
99
- return path.join(cwd, "opencode.jsonc");
1300
+ return path3.join(cwd, "opencode.jsonc");
100
1301
  }
101
1302
  var IS_WINDOWS = process.platform === "win32";
102
- var OPEN_FLAGS = IS_WINDOWS ? fs.constants.O_RDONLY : fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK;
1303
+ var OPEN_FLAGS = IS_WINDOWS ? fs3.constants.O_RDONLY : fs3.constants.O_RDONLY | fs3.constants.O_NOFOLLOW | fs3.constants.O_NONBLOCK;
103
1304
  function assertWindowsPreOpenTrust(targetPath) {
104
1305
  const preStat = lstatOrNull(targetPath);
105
1306
  if (preStat && (preStat.isSymbolicLink() || !preStat.isFile())) {
@@ -111,7 +1312,7 @@ function openTrustedExisting(targetPath) {
111
1312
  assertWindowsPreOpenTrust(targetPath);
112
1313
  let fd;
113
1314
  try {
114
- fd = fs.openSync(targetPath, OPEN_FLAGS);
1315
+ fd = fs3.openSync(targetPath, OPEN_FLAGS);
115
1316
  } catch (error) {
116
1317
  if (error instanceof Error && "code" in error) {
117
1318
  if (error.code === "ENOENT")
@@ -123,13 +1324,13 @@ function openTrustedExisting(targetPath) {
123
1324
  throw error;
124
1325
  }
125
1326
  try {
126
- const stat = fs.fstatSync(fd);
1327
+ const stat = fs3.fstatSync(fd);
127
1328
  if (!stat.isFile()) {
128
1329
  throw createSetupError(`Refusing to read ${targetPath}: not a regular file`);
129
1330
  }
130
- return { bytes: fs.readFileSync(fd), mode: stat.mode & 511 };
1331
+ return { bytes: fs3.readFileSync(fd), mode: stat.mode & 511 };
131
1332
  } finally {
132
- fs.closeSync(fd);
1333
+ fs3.closeSync(fd);
133
1334
  }
134
1335
  }
135
1336
  function atomicWrite(targetPath, content, originalBytes, mode, ops) {
@@ -144,8 +1345,8 @@ function atomicWrite(targetPath, content, originalBytes, mode, ops) {
144
1345
  writeTempAndRename(targetPath, content, mode, preserveMode, ops);
145
1346
  }
146
1347
  function writeTempAndRename(destPath, content, mode, preserveExactMode, ops) {
147
- const parentDir = path.dirname(destPath);
148
- const tempPath = makeTempPath(parentDir, path.basename(destPath));
1348
+ const parentDir = path3.dirname(destPath);
1349
+ const tempPath = makeTempPath(parentDir, path3.basename(destPath));
149
1350
  try {
150
1351
  ops.writeFileSync(tempPath, content, { flag: "wx", mode });
151
1352
  if (preserveExactMode) {
@@ -157,11 +1358,11 @@ function writeTempAndRename(destPath, content, mode, preserveExactMode, ops) {
157
1358
  throw error;
158
1359
  }
159
1360
  }
160
- function makeTempPath(parentDir, basename) {
161
- return path.join(parentDir, `.${basename}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
1361
+ function makeTempPath(parentDir, basename2) {
1362
+ return path3.join(parentDir, `.${basename2}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
162
1363
  }
163
1364
  function cleanupTemp(tempPath, ops) {
164
- if (!fs.existsSync(tempPath))
1365
+ if (!fs3.existsSync(tempPath))
165
1366
  return;
166
1367
  try {
167
1368
  ops.unlinkSync(tempPath);
@@ -195,7 +1396,7 @@ function parseOpenCodeJsonc(rawText, targetPath) {
195
1396
  }
196
1397
  function setupOpenCode(cwd, ops) {
197
1398
  const targetPath = resolveOpenCodeTargetPath(cwd);
198
- assertParentTrusted(path.dirname(targetPath), cwd);
1399
+ assertParentTrusted(path3.dirname(targetPath), cwd);
199
1400
  const existing = openTrustedExisting(targetPath);
200
1401
  if (existing === null) {
201
1402
  const content = `${JSON.stringify({ plugin: [SYSTEMATIC_PACKAGE_NAME] }, null, 2)}
@@ -253,8 +1454,8 @@ function assertPiTaggedEntryUsable(entry, targetPath) {
253
1454
  }
254
1455
  }
255
1456
  function setupPi(cwd, ops) {
256
- const targetPath = path.join(cwd, ".pi", "settings.json");
257
- assertParentTrusted(path.dirname(targetPath), cwd);
1457
+ const targetPath = path3.join(cwd, ".pi", "settings.json");
1458
+ assertParentTrusted(path3.dirname(targetPath), cwd);
258
1459
  const existing = openTrustedExisting(targetPath);
259
1460
  if (existing === null) {
260
1461
  const content = `${JSON.stringify({ packages: [PI_PACKAGE_IDENTIFIER] }, null, 2)}
@@ -314,10 +1515,10 @@ function setupHarness(harness, cwd, opsOverride) {
314
1515
  // src/cli.ts
315
1516
  var getPackageVersion = () => {
316
1517
  try {
317
- const packageJsonPath = path2.resolve(import.meta.dirname, "..", "package.json");
318
- if (!fs2.existsSync(packageJsonPath))
1518
+ const packageJsonPath = path4.resolve(import.meta.dirname, "..", "package.json");
1519
+ if (!fs4.existsSync(packageJsonPath))
319
1520
  return "unknown";
320
- const content = fs2.readFileSync(packageJsonPath, "utf8");
1521
+ const content = fs4.readFileSync(packageJsonPath, "utf8");
321
1522
  const parsed = JSON.parse(content);
322
1523
  return parsed.version ?? "unknown";
323
1524
  } catch {
@@ -337,6 +1538,11 @@ Commands:
337
1538
  show Show configuration
338
1539
  path Print config file locations
339
1540
  setup --harness opencode|pi Configure a harness to load Systematic (project-local only)
1541
+ pi-subagents <subcommand> Manage Systematic persona export for pi-subagents
1542
+ preview [--scope project|global] Show what export would do (no writes)
1543
+ export [--scope project|global] Write persona files to agents dir
1544
+ refresh [--scope project|global] Replace stale owned files from current source
1545
+ cleanup [--scope project|global] Remove all generated files and manifest
340
1546
 
341
1547
  Options:
342
1548
  -h, --help Show this help message
@@ -348,6 +1554,23 @@ Examples:
348
1554
  systematic config show
349
1555
  systematic setup --harness opencode
350
1556
  systematic setup --harness pi
1557
+ systematic pi-subagents preview
1558
+ systematic pi-subagents export
1559
+ systematic pi-subagents export --scope global
1560
+ systematic pi-subagents refresh
1561
+ systematic pi-subagents cleanup
1562
+ `;
1563
+ var PI_SUBAGENTS_USAGE = `Usage: systematic pi-subagents <subcommand> [--scope project|global]
1564
+
1565
+ Subcommands:
1566
+ preview Show what export would do (no writes)
1567
+ export Write persona files to agents dir
1568
+ refresh Replace stale owned files from current source
1569
+ cleanup Remove all generated files and manifest
1570
+
1571
+ Scope:
1572
+ project <cwd>/.pi/agents (default)
1573
+ global $PI_CODING_AGENT_DIR/agents or ~/.pi/agent/agents
351
1574
  `;
352
1575
  function isHarness(value) {
353
1576
  return value === "opencode" || value === "pi";
@@ -376,7 +1599,7 @@ function setupCommand(rest) {
376
1599
  }
377
1600
  }
378
1601
  function listItems(type) {
379
- const packageRoot = path2.resolve(import.meta.dirname, "..");
1602
+ const packageRoot = path4.resolve(import.meta.dirname, "..");
380
1603
  const bundledDir = packageRoot;
381
1604
  let finder;
382
1605
  let subdir;
@@ -397,7 +1620,7 @@ function listItems(type) {
397
1620
  console.error(`Unknown type: ${type}. Use: skills, agents, commands`);
398
1621
  process.exit(1);
399
1622
  }
400
- const items = finder(path2.join(bundledDir, subdir));
1623
+ const items = finder(path4.join(bundledDir, subdir));
401
1624
  if (items.length === 0) {
402
1625
  console.log(`No ${type} found.`);
403
1626
  return;
@@ -414,15 +1637,15 @@ function configShow() {
414
1637
  `);
415
1638
  console.log(` User config: ${paths.userConfig}`);
416
1639
  console.log(` Project config: ${paths.projectConfig}`);
417
- if (fs2.existsSync(paths.projectConfig)) {
1640
+ if (fs4.existsSync(paths.projectConfig)) {
418
1641
  console.log(`
419
1642
  Project configuration:`);
420
- console.log(fs2.readFileSync(paths.projectConfig, "utf-8"));
1643
+ console.log(fs4.readFileSync(paths.projectConfig, "utf-8"));
421
1644
  }
422
- if (fs2.existsSync(paths.userConfig)) {
1645
+ if (fs4.existsSync(paths.userConfig)) {
423
1646
  console.log(`
424
1647
  User configuration:`);
425
- console.log(fs2.readFileSync(paths.userConfig, "utf-8"));
1648
+ console.log(fs4.readFileSync(paths.userConfig, "utf-8"));
426
1649
  }
427
1650
  }
428
1651
  function configPath() {
@@ -431,6 +1654,159 @@ function configPath() {
431
1654
  console.log(` User: ${paths.userConfig}`);
432
1655
  console.log(` Project: ${paths.projectConfig}`);
433
1656
  }
1657
+ function parsePiSubagentsArgs(rest) {
1658
+ let subcommand;
1659
+ let scope = "project";
1660
+ let i = 0;
1661
+ while (i < rest.length) {
1662
+ const arg = rest[i];
1663
+ if (arg === "--scope") {
1664
+ i++;
1665
+ const val = rest[i];
1666
+ if (val !== "project" && val !== "global") {
1667
+ console.error(`Invalid --scope value: ${val ?? ""}. Use: project, global`);
1668
+ process.exit(1);
1669
+ }
1670
+ scope = val;
1671
+ } else if (subcommand === undefined && arg !== undefined && !arg.startsWith("-")) {
1672
+ subcommand = arg;
1673
+ } else if (arg !== undefined) {
1674
+ console.error(`Unknown argument: ${arg}`);
1675
+ console.error(PI_SUBAGENTS_USAGE);
1676
+ process.exit(1);
1677
+ }
1678
+ i++;
1679
+ }
1680
+ return { subcommand, scope };
1681
+ }
1682
+ function printActionLine(a) {
1683
+ if (a.action === "create")
1684
+ console.log(` + ${a.filename} (create)`);
1685
+ else if (a.action === "update")
1686
+ console.log(` ~ ${a.filename} (update)`);
1687
+ else if (a.action === "refuse")
1688
+ console.log(` ! ${a.filename} (refuse: ${a.reason})`);
1689
+ else if (a.action === "remove")
1690
+ console.log(` - ${a.filename} (remove stale)`);
1691
+ else
1692
+ console.log(` = ${a.filename} (up to date)`);
1693
+ }
1694
+ function runPreview(agentsRoot, scope) {
1695
+ const plan = preview(agentsRoot, { scope, cwd: process.cwd() });
1696
+ if (plan.status === "error") {
1697
+ console.error(`Preview failed: ${plan.error ?? "unknown error"}`);
1698
+ process.exit(1);
1699
+ }
1700
+ console.log(`Target: ${plan.agentsRoot}`);
1701
+ console.log("");
1702
+ if (plan.actions.length === 0) {
1703
+ console.log("No actions needed (nothing to export).");
1704
+ return;
1705
+ }
1706
+ const counts = { create: 0, update: 0, refuse: 0, remove: 0, skip: 0 };
1707
+ for (const a of plan.actions) {
1708
+ counts[a.action]++;
1709
+ printActionLine(a);
1710
+ }
1711
+ console.log("");
1712
+ console.log(`Summary: ${counts.create} create, ${counts.update} update, ` + `${counts.skip} skip, ${counts.refuse} refuse, ${counts.remove} remove`);
1713
+ }
1714
+ function runExport(agentsRoot, scope) {
1715
+ const configOptions = { scope, cwd: process.cwd() };
1716
+ const plan = preview(agentsRoot, configOptions);
1717
+ if (plan.status === "error") {
1718
+ console.error(`Export failed (preview step): ${plan.error ?? "unknown error"}`);
1719
+ process.exit(1);
1720
+ }
1721
+ console.log(`Target: ${plan.agentsRoot}`);
1722
+ const hasWork = plan.actions.some((a) => a.action === "create" || a.action === "update" || a.action === "refuse" || a.action === "remove");
1723
+ if (plan.actions.length > 0) {
1724
+ console.log("");
1725
+ const counts = { create: 0, update: 0, refuse: 0, remove: 0, skip: 0 };
1726
+ for (const a of plan.actions) {
1727
+ counts[a.action]++;
1728
+ printActionLine(a);
1729
+ }
1730
+ console.log("");
1731
+ console.log(`Summary: ${counts.create} create, ${counts.update} update, ` + `${counts.skip} skip, ${counts.refuse} refuse, ${counts.remove} remove`);
1732
+ }
1733
+ if (!hasWork) {
1734
+ console.log("All persona files are already up to date. No changes.");
1735
+ return;
1736
+ }
1737
+ const result = exportPersonas(agentsRoot, configOptions);
1738
+ if (result.status === "error") {
1739
+ console.error(`Export failed: ${result.error ?? "unknown error"}`);
1740
+ process.exit(1);
1741
+ }
1742
+ const hadRemovals = plan.actions.some((a) => a.action === "remove");
1743
+ if (result.written === 0 && result.refused.length === 0 && !hadRemovals) {
1744
+ console.log("All persona files are already up to date. No changes.");
1745
+ } else {
1746
+ console.log(`Exported ${result.written} file(s) to ${agentsRoot}/ (${result.skipped} already up to date).`);
1747
+ }
1748
+ if (result.refused.length > 0) {
1749
+ console.log(`
1750
+ Refused (pre-existing unowned files not overwritten):`);
1751
+ for (const r of result.refused)
1752
+ console.log(` ! ${r.filename}: ${r.reason}`);
1753
+ }
1754
+ }
1755
+ function runRefresh(agentsRoot, scope) {
1756
+ const result = refresh(agentsRoot, { scope, cwd: process.cwd() });
1757
+ if (result.status === "error") {
1758
+ console.error(`Refresh failed: ${result.error ?? "unknown error"}`);
1759
+ process.exit(1);
1760
+ }
1761
+ if (result.updated === 0) {
1762
+ console.log("All owned persona files are up to date. No changes.");
1763
+ } else {
1764
+ console.log(`Refreshed: updated ${result.updated} file(s) in ${agentsRoot}/.`);
1765
+ }
1766
+ if (result.skippedUnowned > 0)
1767
+ console.log(` Skipped ${result.skippedUnowned} unowned file(s) (not touched).`);
1768
+ }
1769
+ function runCleanup(agentsRoot, scope) {
1770
+ let result;
1771
+ try {
1772
+ result = cleanup(agentsRoot, { scope, cwd: process.cwd() });
1773
+ } catch (err) {
1774
+ console.error(`Cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
1775
+ process.exit(1);
1776
+ }
1777
+ if (result.status === "error") {
1778
+ console.error(`Cleanup failed: ${result.error ?? "unknown error"}`);
1779
+ process.exit(1);
1780
+ }
1781
+ console.log(`Cleaned up generated persona files from ${agentsRoot}/. Manifest (${MANIFEST_FILENAME}) removed.`);
1782
+ }
1783
+ function piSubagentsCommand(rest) {
1784
+ const { subcommand, scope } = parsePiSubagentsArgs(rest);
1785
+ if (!subcommand) {
1786
+ console.error("Error: pi-subagents requires a subcommand.");
1787
+ console.error(PI_SUBAGENTS_USAGE);
1788
+ process.exit(1);
1789
+ }
1790
+ const agentsRoot = resolveAgentsRoot(scope, process.cwd());
1791
+ switch (subcommand) {
1792
+ case "preview":
1793
+ runPreview(agentsRoot, scope);
1794
+ break;
1795
+ case "export":
1796
+ runExport(agentsRoot, scope);
1797
+ break;
1798
+ case "refresh":
1799
+ runRefresh(agentsRoot, scope);
1800
+ break;
1801
+ case "cleanup":
1802
+ runCleanup(agentsRoot, scope);
1803
+ break;
1804
+ default:
1805
+ console.error(`Unknown pi-subagents subcommand: ${subcommand}`);
1806
+ console.error(PI_SUBAGENTS_USAGE);
1807
+ process.exit(1);
1808
+ }
1809
+ }
434
1810
  var args = process.argv.slice(2);
435
1811
  var command = args[0];
436
1812
  switch (command) {
@@ -440,6 +1816,9 @@ switch (command) {
440
1816
  case "setup":
441
1817
  setupCommand(args.slice(1));
442
1818
  break;
1819
+ case "pi-subagents":
1820
+ piSubagentsCommand(args.slice(1));
1821
+ break;
443
1822
  case "config":
444
1823
  switch (args[1]) {
445
1824
  case "show":