@pieai/pro-gov 0.3.5 → 0.3.7

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.
Files changed (28) hide show
  1. package/README.md +27 -4
  2. package/assets/docs/reference/adoption/adoption-playbook.md +20 -3
  3. package/assets/docs/reference/adoption/project-relationship.md +7 -5
  4. package/assets/docs/reference/adoption/public-release-checklist.md +24 -1
  5. package/assets/public-agent-assets/README.md +40 -0
  6. package/assets/public-agent-assets/bundles/.gitkeep +0 -0
  7. package/assets/public-agent-assets/bundles/base-governance.json +11 -0
  8. package/assets/public-agent-assets/commands/pie-commands/README.md +3 -0
  9. package/assets/public-agent-assets/registry.json +132 -0
  10. package/assets/public-agent-assets/rules/pie-rules/.gitkeep +0 -0
  11. package/assets/public-agent-assets/rules/pie-rules/ai-in-the-loop.md +66 -0
  12. package/assets/public-agent-assets/rules/pie-rules/rule-evolution-methodology.md +74 -0
  13. package/assets/public-agent-assets/skills/dokobot/.gitkeep +0 -0
  14. package/assets/public-agent-assets/skills/npx-skills/README.md +3 -0
  15. package/assets/public-agent-assets/skills/pie-skills/.gitkeep +0 -0
  16. package/assets/public-agent-assets/skills/pie-skills/beginner-friendly-docs/SKILL.md +225 -0
  17. package/assets/public-agent-assets/skills/pie-skills/doc-cross-validator/SKILL.md +194 -0
  18. package/assets/starter/.github/workflows/docs-check.yml +2 -4
  19. package/assets/starter/docs/governance/agents-routing/doc-only-v0.9.md +2 -3
  20. package/assets/starter/docs/governance/agents-routing/engineering-runtime-v0.9.md +2 -3
  21. package/assets/starter/docs/governance/boundary.md +2 -2
  22. package/assets/starter/docs/governance/ssot-v0.9.md +2 -2
  23. package/assets/starter/lefthook.template.yml +2 -2
  24. package/cli-guide.md +18 -3
  25. package/dist/cli.js +845 -91
  26. package/package.json +5 -5
  27. package/assets/docs/reference/adoption/downstream-project-registry.md +0 -85
  28. package/assets/starter/.gemini/settings.json +0 -5
package/dist/cli.js CHANGED
@@ -48,8 +48,8 @@ function isPlatformMetadata(name) {
48
48
  }
49
49
 
50
50
  // src/commands/assets.ts
51
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
52
- import { dirname as dirname5 } from "node:path";
51
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
52
+ import { dirname as dirname5, join as join11 } from "node:path";
53
53
 
54
54
  // src/asset-bundles/bundles.ts
55
55
  import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync } from "node:fs";
@@ -202,7 +202,6 @@ import { existsSync as existsSync4, lstatSync } from "node:fs";
202
202
  import { isAbsolute, join as join4, posix } from "node:path";
203
203
  var supportedFamilies = /* @__PURE__ */ new Set([
204
204
  "pie-skills",
205
- "dokobot",
206
205
  "npx-skills",
207
206
  "pie-rules",
208
207
  "pie-commands"
@@ -214,6 +213,8 @@ var supportedVisibilities = /* @__PURE__ */ new Set([
214
213
  "third-party"
215
214
  ]);
216
215
  var supportedSourceKinds = /* @__PURE__ */ new Set(["local", "local-pack", "npx"]);
216
+ var supportedSkillPlacements = /* @__PURE__ */ new Set(["auto", "manual"]);
217
+ var supportedSkillScopes = /* @__PURE__ */ new Set(["project", "user"]);
217
218
  var supportedHosts = /* @__PURE__ */ new Set([
218
219
  "codex",
219
220
  "claude-code",
@@ -269,6 +270,20 @@ function validateAssetRegistry(registry, options = {}) {
269
270
  });
270
271
  }
271
272
  }
273
+ if (asset.kind === "skill" && !supportedSkillPlacements.has(asset.defaultPlacement)) {
274
+ issues.push({
275
+ type: "unsupported-skill-placement",
276
+ id: asset.id,
277
+ message: `Skill asset must declare defaultPlacement auto or manual: ${asset.id}`
278
+ });
279
+ }
280
+ if (asset.defaultScope && asset.kind !== "skill" || asset.kind === "skill" && asset.defaultScope && !supportedSkillScopes.has(asset.defaultScope)) {
281
+ issues.push({
282
+ type: "unsupported-skill-scope",
283
+ id: asset.id,
284
+ message: `Skill asset defaultScope must be project or user: ${asset.id}`
285
+ });
286
+ }
272
287
  if (!isSafeRegistrySourcePath(asset.sourcePath)) {
273
288
  issues.push({
274
289
  type: "unsafe-source-path",
@@ -363,7 +378,9 @@ function createAgentAssetLockEntries(registry, agentAssetsDir, assetIds) {
363
378
  })).sort((a, b) => a.id.localeCompare(b.id));
364
379
  }
365
380
  function hashAgentAssetContent(asset, agentAssetsDir) {
366
- const sourceAbsolutePath = join5(agentAssetsDir, asset.sourcePath);
381
+ return hashAssetPathContent(join5(agentAssetsDir, asset.sourcePath));
382
+ }
383
+ function hashAssetPathContent(sourceAbsolutePath) {
367
384
  const hash = createHash2("sha256");
368
385
  for (const filePath of listFiles3(sourceAbsolutePath)) {
369
386
  const relativePath = toUnixPath3(relative3(sourceAbsolutePath, filePath));
@@ -377,7 +394,12 @@ function hashAgentAssetContent(asset, agentAssetsDir) {
377
394
  function findDefaultAgentAssetsDir() {
378
395
  const packageRoot2 = findPackageRoot(dirname2(fileURLToPath2(import.meta.url)));
379
396
  const repoRoot = join5(packageRoot2, "..", "..");
380
- const candidates = [join5(packageRoot2, "assets/agent-assets"), join5(repoRoot, "agent-assets")];
397
+ const candidates = [
398
+ join5(packageRoot2, "assets/agent-assets"),
399
+ join5(repoRoot, "agent-assets"),
400
+ join5(packageRoot2, "assets/public-agent-assets"),
401
+ join5(repoRoot, "public-agent-assets")
402
+ ];
381
403
  return candidates.find((candidate) => existsSync5(join5(candidate, "registry.json"))) ?? candidates[0];
382
404
  }
383
405
  function findPackageRoot(startDir) {
@@ -400,6 +422,7 @@ function listFiles3(absolutePath) {
400
422
  if (stats.isFile()) return [absolutePath];
401
423
  const files = [];
402
424
  for (const entry of readdirSync4(absolutePath, { withFileTypes: true })) {
425
+ if (shouldIgnoreAssetHashEntry(entry.name)) continue;
403
426
  const entryPath = join5(absolutePath, entry.name);
404
427
  if (entry.isDirectory()) {
405
428
  files.push(...listFiles3(entryPath));
@@ -409,13 +432,112 @@ function listFiles3(absolutePath) {
409
432
  }
410
433
  return files.sort();
411
434
  }
435
+ function shouldIgnoreAssetHashEntry(name) {
436
+ return name === "__pycache__" || name === ".DS_Store" || name === "Thumbs.db" || name === "node_modules" || name.endsWith(".pyc") || name.endsWith(".pyo");
437
+ }
412
438
  function toUnixPath3(path) {
413
439
  return path.replaceAll("\\", "/");
414
440
  }
415
441
 
442
+ // src/asset-registry/public-promotion.ts
443
+ import { existsSync as existsSync6 } from "node:fs";
444
+ import { isAbsolute as isAbsolute2, join as join6, posix as posix2 } from "node:path";
445
+ function checkPublicAssetPromotions(options) {
446
+ const issues = [];
447
+ let checked = 0;
448
+ for (const asset of options.registry.assets) {
449
+ if (!needsPromotionCheck(asset)) continue;
450
+ checked += 1;
451
+ if (!asset.promotion) {
452
+ issues.push({
453
+ type: "missing-promotion",
454
+ id: asset.id,
455
+ message: `Public asset is missing promotion metadata: ${asset.id}`
456
+ });
457
+ continue;
458
+ }
459
+ const publicPathResult = resolveSafePath(options.publicAgentAssetsDir, asset.sourcePath);
460
+ if (!publicPathResult.ok) {
461
+ issues.push({
462
+ type: "unsafe-public-source-path",
463
+ id: asset.id,
464
+ path: asset.sourcePath,
465
+ message: `Public asset source path is unsafe: ${asset.sourcePath}`
466
+ });
467
+ continue;
468
+ }
469
+ const privatePathResult = resolveSafePath(
470
+ options.privateAgentAssetsDir,
471
+ asset.promotion.privateSourcePath
472
+ );
473
+ if (!privatePathResult.ok) {
474
+ issues.push({
475
+ type: "unsafe-private-source-path",
476
+ id: asset.id,
477
+ path: asset.promotion.privateSourcePath,
478
+ message: `Private promotion source path is unsafe: ${asset.promotion.privateSourcePath}`
479
+ });
480
+ continue;
481
+ }
482
+ if (!existsSync6(privatePathResult.path)) {
483
+ issues.push({
484
+ type: "missing-private-source",
485
+ id: asset.id,
486
+ path: asset.promotion.privateSourcePath,
487
+ message: `Private promotion source does not exist: ${asset.promotion.privateSourcePath}`
488
+ });
489
+ } else {
490
+ const actualPrivateHash = hashAssetPathContent(privatePathResult.path);
491
+ if (actualPrivateHash !== asset.promotion.privateSourceHash) {
492
+ issues.push({
493
+ type: "source-drift",
494
+ id: asset.id,
495
+ path: asset.promotion.privateSourcePath,
496
+ expected: asset.promotion.privateSourceHash,
497
+ actual: actualPrivateHash,
498
+ message: `Private source changed after public promotion: ${asset.id}`
499
+ });
500
+ }
501
+ }
502
+ if (!existsSync6(publicPathResult.path)) {
503
+ issues.push({
504
+ type: "missing-public-source",
505
+ id: asset.id,
506
+ path: asset.sourcePath,
507
+ message: `Public promoted asset does not exist: ${asset.sourcePath}`
508
+ });
509
+ } else {
510
+ const actualPublicHash = hashAssetPathContent(publicPathResult.path);
511
+ if (actualPublicHash !== asset.promotion.publicHash) {
512
+ issues.push({
513
+ type: "public-drift",
514
+ id: asset.id,
515
+ path: asset.sourcePath,
516
+ expected: asset.promotion.publicHash,
517
+ actual: actualPublicHash,
518
+ message: `Public promoted asset changed after review: ${asset.id}`
519
+ });
520
+ }
521
+ }
522
+ }
523
+ return { checked, issues };
524
+ }
525
+ function needsPromotionCheck(asset) {
526
+ return asset.visibility === "public" && asset.publishable;
527
+ }
528
+ function resolveSafePath(root, sourcePath) {
529
+ if (!sourcePath || isAbsolute2(sourcePath) || sourcePath.startsWith("/")) return { ok: false };
530
+ const normalized = posix2.normalize(sourcePath.replaceAll("\\", "/"));
531
+ if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
532
+ return { ok: false };
533
+ }
534
+ if (normalized.split("/").includes("..")) return { ok: false };
535
+ return { ok: true, path: join6(root, normalized) };
536
+ }
537
+
416
538
  // src/asset-targets/apply.ts
417
- import { existsSync as existsSync6, lstatSync as lstatSync2, mkdirSync as mkdirSync2, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
418
- import { dirname as dirname3, join as join6, resolve } from "node:path";
539
+ import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync2, realpathSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
540
+ import { dirname as dirname3, join as join7, relative as relative4, resolve } from "node:path";
419
541
  function applyAssetInstallPlan(plan) {
420
542
  const appliedActions = [];
421
543
  for (const action of plan.actions) {
@@ -425,7 +547,7 @@ function applyAssetInstallPlan(plan) {
425
547
  return { appliedActions };
426
548
  }
427
549
  function applyAction(targetDir, action) {
428
- const targetAbsolutePath = join6(targetDir, action.targetPath);
550
+ const targetAbsolutePath = join7(targetDir, action.targetPath);
429
551
  if (action.type === "create-dir") {
430
552
  mkdirSync2(targetAbsolutePath, { recursive: true });
431
553
  return;
@@ -437,15 +559,16 @@ function applyAction(targetDir, action) {
437
559
  }
438
560
  mkdirSync2(dirname3(targetAbsolutePath), { recursive: true });
439
561
  const sourceAbsolutePath = resolve(action.sourcePath);
562
+ const symlinkTarget = relative4(realpathSync(dirname3(targetAbsolutePath)), realpathSync(sourceAbsolutePath)) || ".";
440
563
  if (action.type === "symlink") {
441
564
  if (pathExistsEvenIfDanglingSymlink2(targetAbsolutePath)) {
442
565
  throw new Error(`Refusing to overwrite unmanaged target: ${action.targetPath}`);
443
566
  }
444
- symlinkSync(sourceAbsolutePath, targetAbsolutePath);
567
+ symlinkSync(symlinkTarget, targetAbsolutePath);
445
568
  return;
446
569
  }
447
570
  if (!pathExistsEvenIfDanglingSymlink2(targetAbsolutePath)) {
448
- symlinkSync(sourceAbsolutePath, targetAbsolutePath);
571
+ symlinkSync(symlinkTarget, targetAbsolutePath);
449
572
  return;
450
573
  }
451
574
  const stats = lstatSync2(targetAbsolutePath);
@@ -453,23 +576,23 @@ function applyAction(targetDir, action) {
453
576
  throw new Error(`Refusing to overwrite unmanaged target: ${action.targetPath}`);
454
577
  }
455
578
  unlinkSync(targetAbsolutePath);
456
- symlinkSync(sourceAbsolutePath, targetAbsolutePath);
579
+ symlinkSync(symlinkTarget, targetAbsolutePath);
457
580
  }
458
581
  function pathExistsEvenIfDanglingSymlink2(path) {
459
582
  try {
460
583
  lstatSync2(path);
461
584
  return true;
462
585
  } catch {
463
- return existsSync6(path);
586
+ return existsSync7(path);
464
587
  }
465
588
  }
466
589
 
467
590
  // src/asset-targets/check.ts
468
- import { existsSync as existsSync7, lstatSync as lstatSync3, readFileSync as readFileSync4 } from "node:fs";
469
- import { join as join7 } from "node:path";
591
+ import { existsSync as existsSync8, lstatSync as lstatSync3, readFileSync as readFileSync4 } from "node:fs";
592
+ import { basename, join as join8 } from "node:path";
470
593
  function checkInstalledAssets(options) {
471
- const lockfilePath = join7(options.targetDir, ".pro-gov/assets.lock.json");
472
- if (!existsSync7(lockfilePath)) {
594
+ const lockfilePath = join8(options.targetDir, ".pro-gov/assets.lock.json");
595
+ if (!existsSync8(lockfilePath)) {
473
596
  return {
474
597
  targetDir: options.targetDir,
475
598
  issues: [
@@ -483,22 +606,35 @@ function checkInstalledAssets(options) {
483
606
  const registryById = new Map(options.registry.assets.map((asset) => [asset.id, asset]));
484
607
  const lockfile = JSON.parse(readFileSync4(lockfilePath, "utf8"));
485
608
  const issues = [];
609
+ const strictRegistry = options.strictRegistry ?? false;
486
610
  for (const entry of lockfile.assets ?? []) {
487
611
  const asset = registryById.get(entry.id);
488
- const targetAbsolutePath = join7(options.targetDir, entry.targetPath);
489
- const sourceAbsolutePath = join7(options.agentAssetsDir, entry.sourcePath);
490
- if (!asset) {
612
+ const targetAbsolutePath = join8(options.targetDir, entry.targetPath);
613
+ if (!asset && strictRegistry) {
491
614
  issues.push({
492
615
  type: "unknown-asset",
493
616
  id: entry.id,
494
617
  targetPath: entry.targetPath,
495
618
  message: `Lockfile references unknown asset: ${entry.id}`
496
619
  });
497
- continue;
498
620
  }
499
- const hostFolderIssue = checkHostFolder(lockfile.host, asset.kind, entry.targetPath, entry.id);
500
- if (hostFolderIssue) {
501
- issues.push(hostFolderIssue);
621
+ if (asset?.kind === "skill" && asset.defaultScope === "user") {
622
+ issues.push({
623
+ type: "user-scoped-asset-in-project-lock",
624
+ id: entry.id,
625
+ targetPath: entry.targetPath,
626
+ message: `User-scoped skill is still locked into this project; move it to the user skill roots: ${entry.id}`
627
+ });
628
+ }
629
+ if (asset) {
630
+ const hostFolderIssue = checkHostFolder(lockfile.host, asset.kind, entry.targetPath, entry.id);
631
+ if (hostFolderIssue) {
632
+ issues.push(hostFolderIssue);
633
+ }
634
+ const placementDriftIssue = checkRegistryPlacement(lockfile, asset, entry.targetPath);
635
+ if (placementDriftIssue) {
636
+ issues.push(placementDriftIssue);
637
+ }
502
638
  }
503
639
  if (!pathExistsEvenIfDanglingSymlink3(targetAbsolutePath)) {
504
640
  issues.push({
@@ -519,7 +655,7 @@ function checkInstalledAssets(options) {
519
655
  });
520
656
  continue;
521
657
  }
522
- if (!existsSync7(targetAbsolutePath)) {
658
+ if (!existsSync8(targetAbsolutePath)) {
523
659
  issues.push({
524
660
  type: "dangling-symlink",
525
661
  id: entry.id,
@@ -528,7 +664,19 @@ function checkInstalledAssets(options) {
528
664
  });
529
665
  continue;
530
666
  }
531
- if (!existsSync7(sourceAbsolutePath)) {
667
+ const currentTargetHash = hashAssetPathContent(targetAbsolutePath);
668
+ const targetHashMatchesLock = currentTargetHash === entry.contentHash;
669
+ if (!targetHashMatchesLock) {
670
+ issues.push({
671
+ type: "hash-drift",
672
+ id: entry.id,
673
+ targetPath: entry.targetPath,
674
+ message: `Managed asset hash drifted: ${entry.id}`
675
+ });
676
+ }
677
+ if (!asset || !strictRegistry) continue;
678
+ const sourceAbsolutePath = join8(options.agentAssetsDir, asset.sourcePath);
679
+ if (!existsSync8(sourceAbsolutePath)) {
532
680
  issues.push({
533
681
  type: "missing-source",
534
682
  id: entry.id,
@@ -537,8 +685,8 @@ function checkInstalledAssets(options) {
537
685
  });
538
686
  continue;
539
687
  }
540
- const currentHash = hashAgentAssetContent(asset, options.agentAssetsDir);
541
- if (currentHash !== entry.contentHash) {
688
+ const currentSourceHash = hashAgentAssetContent(asset, options.agentAssetsDir);
689
+ if (targetHashMatchesLock && currentSourceHash !== entry.contentHash) {
542
690
  issues.push({
543
691
  type: "hash-drift",
544
692
  id: entry.id,
@@ -547,12 +695,43 @@ function checkInstalledAssets(options) {
547
695
  });
548
696
  }
549
697
  }
698
+ issues.push(...checkDuplicateSkillPlacements(options.targetDir, options.registry));
550
699
  return { targetDir: options.targetDir, issues };
551
700
  }
701
+ function checkRegistryPlacement(lockfile, asset, targetPath) {
702
+ if (lockfile.placement !== "registry") return void 0;
703
+ if (asset.kind !== "skill") return void 0;
704
+ const expectedPath = expectedRegistrySkillTargetPath(lockfile.host, asset.sourcePath, asset.defaultPlacement);
705
+ if (!expectedPath || targetPath === expectedPath) return void 0;
706
+ return {
707
+ type: "skill-placement-drift",
708
+ id: asset.id,
709
+ targetPath,
710
+ message: `Managed skill target ${targetPath} does not match registry placement; expected ${expectedPath}`
711
+ };
712
+ }
713
+ function checkDuplicateSkillPlacements(targetDir, registry) {
714
+ const issues = [];
715
+ for (const asset of registry.assets) {
716
+ if (asset.kind !== "skill") continue;
717
+ const skillName = basename(asset.sourcePath);
718
+ const autoPath = `.agents/skills/${skillName}`;
719
+ const manualPath = `.agents/manual-skills/${skillName}`;
720
+ if (pathExistsEvenIfDanglingSymlink3(join8(targetDir, autoPath)) && pathExistsEvenIfDanglingSymlink3(join8(targetDir, manualPath))) {
721
+ issues.push({
722
+ type: "duplicate-skill-placement",
723
+ id: asset.id,
724
+ targetPath: `${autoPath} + ${manualPath}`,
725
+ message: `Skill is linked in both auto and manual locations: ${skillName}`
726
+ });
727
+ }
728
+ }
729
+ return issues;
730
+ }
552
731
  function checkHostFolder(host, kind, targetPath, id) {
553
732
  if (kind !== "skill") return void 0;
554
- const expectedPrefix = expectedSkillTargetPrefix(host);
555
- if (!expectedPrefix) {
733
+ const expectedPrefixes = expectedSkillTargetPrefixes(host);
734
+ if (!expectedPrefixes) {
556
735
  return {
557
736
  type: "unsupported-host-folder",
558
737
  id,
@@ -560,20 +739,30 @@ function checkHostFolder(host, kind, targetPath, id) {
560
739
  message: `Lockfile host is unsupported for managed skill target: ${host ?? "missing"}`
561
740
  };
562
741
  }
563
- if (!targetPath.startsWith(expectedPrefix)) {
742
+ if (!expectedPrefixes.some((prefix) => targetPath.startsWith(prefix))) {
564
743
  return {
565
744
  type: "unsupported-host-folder",
566
745
  id,
567
746
  targetPath,
568
- message: `Managed skill target ${targetPath} does not match host ${host}; expected ${expectedPrefix}`
747
+ message: `Managed skill target ${targetPath} does not match host ${host}; expected ${expectedPrefixes.join(" or ")}`
569
748
  };
570
749
  }
571
750
  return void 0;
572
751
  }
573
- function expectedSkillTargetPrefix(host) {
574
- if (host === "claude-code") return ".claude/skills/";
752
+ function expectedSkillTargetPrefixes(host) {
753
+ if (host === "claude-code") return [".claude/skills/"];
575
754
  if (host === "codex" || host === "gemini-cli" || host === "antigravity") {
576
- return ".agents/skills/";
755
+ return [".agents/skills/", ".agents/manual-skills/"];
756
+ }
757
+ return void 0;
758
+ }
759
+ function expectedRegistrySkillTargetPath(host, sourcePath, placement) {
760
+ const skillName = basename(sourcePath);
761
+ if (host === "claude-code") {
762
+ return placement === "manual" ? void 0 : `.claude/skills/${skillName}`;
763
+ }
764
+ if (host === "codex" || host === "gemini-cli" || host === "antigravity") {
765
+ return placement === "manual" ? `.agents/manual-skills/${skillName}` : `.agents/skills/${skillName}`;
577
766
  }
578
767
  return void 0;
579
768
  }
@@ -587,9 +776,10 @@ function pathExistsEvenIfDanglingSymlink3(path) {
587
776
  }
588
777
 
589
778
  // src/asset-targets/install-plan.ts
590
- import { existsSync as existsSync8, lstatSync as lstatSync4, readFileSync as readFileSync5 } from "node:fs";
591
- import { basename, dirname as dirname4, join as join8 } from "node:path";
779
+ import { existsSync as existsSync9, lstatSync as lstatSync4, readFileSync as readFileSync5 } from "node:fs";
780
+ import { basename as basename2, dirname as dirname4, join as join9 } from "node:path";
592
781
  function createAssetInstallPlan(options) {
782
+ const placement = options.placement ?? "registry";
593
783
  const assetsById = new Map(options.registry.assets.map((asset) => [asset.id, asset]));
594
784
  const bundlesById = new Map(options.bundles.map((bundle) => [bundle.id, bundle]));
595
785
  const assetIds = resolveBundleAssetIds(options.bundleIds, bundlesById);
@@ -604,17 +794,19 @@ function createAssetInstallPlan(options) {
604
794
  const lockEntries = createAgentAssetLockEntries(options.registry, options.agentAssetsDir, assetIds);
605
795
  const managedTargets = readManagedTargets(options.targetDir);
606
796
  const assetActions = assets.map(
607
- (asset) => createAssetAction(asset, options.agentAssetsDir, options.targetDir, options.host, managedTargets)
797
+ (asset) => createAssetAction(asset, options.agentAssetsDir, options.targetDir, options.host, placement, managedTargets)
608
798
  );
609
799
  const manifest = {
610
800
  schemaVersion: 1,
611
801
  host: options.host,
802
+ placement,
612
803
  bundleIds: [...options.bundleIds],
613
804
  assetIds
614
805
  };
615
806
  const lockfile = {
616
807
  schemaVersion: 1,
617
808
  host: options.host,
809
+ placement,
618
810
  bundleIds: [...options.bundleIds],
619
811
  assets: lockEntries.map((entry) => {
620
812
  const action = assetActions.find((candidate) => "assetId" in candidate && candidate.assetId === entry.id);
@@ -643,6 +835,7 @@ function createAssetInstallPlan(options) {
643
835
  dryRun: true,
644
836
  targetDir: options.targetDir,
645
837
  host: options.host,
838
+ placement,
646
839
  bundleIds: [...options.bundleIds],
647
840
  assetIds,
648
841
  actions: [...createDirectoryActions([...assetActions, ...writeActions]), ...assetActions, ...writeActions]
@@ -659,10 +852,15 @@ function resolveBundleAssetIds(bundleIds, bundlesById) {
659
852
  }
660
853
  return [...ids].sort();
661
854
  }
662
- function createAssetAction(asset, agentAssetsDir, targetDir, host, managedTargets) {
663
- const sourcePath = join8(agentAssetsDir, asset.sourcePath);
664
- const targetPath = resolveHostTargetPath(asset, host);
665
- const targetAbsolutePath = join8(targetDir, targetPath);
855
+ function createAssetAction(asset, agentAssetsDir, targetDir, host, placement, managedTargets) {
856
+ if (asset.kind === "skill" && asset.defaultScope === "user") {
857
+ throw new Error(
858
+ `User-scoped asset ${asset.id} must be linked at the user level, not installed into a project target.`
859
+ );
860
+ }
861
+ const sourcePath = join9(agentAssetsDir, asset.sourcePath);
862
+ const targetPath = resolveHostTargetPath(asset, host, placement);
863
+ const targetAbsolutePath = join9(targetDir, targetPath);
666
864
  const targetExists = pathExistsEvenIfDanglingSymlink4(targetAbsolutePath);
667
865
  if (targetExists) {
668
866
  const stats = lstatSync4(targetAbsolutePath);
@@ -683,17 +881,28 @@ function createAssetAction(asset, agentAssetsDir, targetDir, host, managedTarget
683
881
  targetPath
684
882
  };
685
883
  }
686
- function resolveHostTargetPath(asset, host) {
884
+ function resolveHostTargetPath(asset, host, placement) {
687
885
  if (asset.kind === "skill") {
886
+ const effectivePlacement = resolveSkillPlacement(asset, placement);
688
887
  if (host === "claude-code") {
689
- return `.claude/skills/${basename(asset.sourcePath)}`;
888
+ if (placement === "manual") {
889
+ throw new Error("Manual skill placement is only supported for .agents hosts");
890
+ }
891
+ return `.claude/skills/${basename2(asset.sourcePath)}`;
892
+ }
893
+ if (effectivePlacement === "manual") {
894
+ return `.agents/manual-skills/${basename2(asset.sourcePath)}`;
690
895
  }
691
- return `.agents/skills/${basename(asset.sourcePath)}`;
896
+ return `.agents/skills/${basename2(asset.sourcePath)}`;
692
897
  }
693
898
  if (asset.kind === "rule") {
694
- return `.pro-gov/agent-assets/rules/${basename(asset.sourcePath)}`;
899
+ return `.pro-gov/agent-assets/rules/${basename2(asset.sourcePath)}`;
695
900
  }
696
- return `.pro-gov/agent-assets/commands/${basename(asset.sourcePath)}`;
901
+ return `.pro-gov/agent-assets/commands/${basename2(asset.sourcePath)}`;
902
+ }
903
+ function resolveSkillPlacement(asset, placement) {
904
+ if (placement !== "registry") return placement;
905
+ return asset.defaultPlacement ?? "auto";
697
906
  }
698
907
  function createDirectoryActions(actions) {
699
908
  const directories = /* @__PURE__ */ new Set();
@@ -705,8 +914,8 @@ function createDirectoryActions(actions) {
705
914
  return [...directories].sort().map((targetPath) => ({ type: "create-dir", targetPath }));
706
915
  }
707
916
  function readManagedTargets(targetDir) {
708
- const lockfilePath = join8(targetDir, ".pro-gov/assets.lock.json");
709
- if (!existsSync8(lockfilePath)) return /* @__PURE__ */ new Set();
917
+ const lockfilePath = join9(targetDir, ".pro-gov/assets.lock.json");
918
+ if (!existsSync9(lockfilePath)) return /* @__PURE__ */ new Set();
710
919
  try {
711
920
  const lockfile = JSON.parse(readFileSync5(lockfilePath, "utf8"));
712
921
  return new Set((lockfile.assets ?? []).map((asset) => asset.targetPath).filter(Boolean));
@@ -724,8 +933,8 @@ function pathExistsEvenIfDanglingSymlink4(path) {
724
933
  }
725
934
 
726
935
  // src/asset-targets/recommend.ts
727
- import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync6 } from "node:fs";
728
- import { join as join9 } from "node:path";
936
+ import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync6 } from "node:fs";
937
+ import { join as join10 } from "node:path";
729
938
  var frontendPackages = /* @__PURE__ */ new Set([
730
939
  "@vitejs/plugin-react",
731
940
  "astro",
@@ -737,23 +946,23 @@ var frontendPackages = /* @__PURE__ */ new Set([
737
946
  "vite",
738
947
  "vue"
739
948
  ]);
740
- var agentEntryCandidates = ["AGENTS.md", "CLAUDE.md", ".gemini/settings.json", "GEMINI.md"];
949
+ var agentEntryCandidates = ["AGENTS.md", "CLAUDE.md"];
741
950
  function discoverTargetSignals(targetDir) {
742
- const packageJson = readJson(join9(targetDir, "package.json"));
951
+ const packageJson = readJson(join10(targetDir, "package.json"));
743
952
  const dependencyNames = packageJson ? Object.keys({ ...packageJson.dependencies, ...packageJson.devDependencies }) : [];
744
953
  const frontendSignals = dependencyNames.filter((name) => frontendPackages.has(name)).sort();
745
- const hasAgentEntry = agentEntryCandidates.some((file) => existsSync9(join9(targetDir, file)));
954
+ const hasAgentEntry = agentEntryCandidates.some((file) => existsSync10(join10(targetDir, file)));
746
955
  const researchSignals = [
747
- existsSync9(join9(targetDir, "docs/research")) ? "docs/research" : "",
748
- existsSync9(join9(targetDir, "research")) ? "research" : "",
956
+ existsSync10(join10(targetDir, "docs/research")) ? "docs/research" : "",
957
+ existsSync10(join10(targetDir, "research")) ? "research" : "",
749
958
  hasBookChildDirectory(targetDir, "research") ? "books/*/research" : "",
750
- textFileIncludes(join9(targetDir, "README.md"), ["research", "\u8C03\u7814"]) ? "README research" : ""
959
+ textFileIncludes(join10(targetDir, "README.md"), ["research", "\u8C03\u7814"]) ? "README research" : ""
751
960
  ].filter(Boolean);
752
961
  const writingSignals = [
753
- existsSync9(join9(targetDir, "chapters")) ? "chapters" : "",
754
- existsSync9(join9(targetDir, "src/chapters")) ? "src/chapters" : "",
962
+ existsSync10(join10(targetDir, "chapters")) ? "chapters" : "",
963
+ existsSync10(join10(targetDir, "src/chapters")) ? "src/chapters" : "",
755
964
  hasBookChildDirectory(targetDir, "chapters") ? "books/*/chapters" : "",
756
- textFileIncludes(join9(targetDir, "AGENTS.md"), ["writing mode", "novel chapter", "book content"]) ? "AGENTS writing" : ""
965
+ textFileIncludes(join10(targetDir, "AGENTS.md"), ["writing mode", "novel chapter", "book content"]) ? "AGENTS writing" : ""
757
966
  ].filter(Boolean);
758
967
  return {
759
968
  targetDir,
@@ -801,7 +1010,7 @@ function recommendBundlesForTarget(targetDir) {
801
1010
  return recommendations;
802
1011
  }
803
1012
  function readJson(path) {
804
- if (!existsSync9(path)) return void 0;
1013
+ if (!existsSync10(path)) return void 0;
805
1014
  try {
806
1015
  return JSON.parse(readFileSync6(path, "utf8"));
807
1016
  } catch {
@@ -809,16 +1018,16 @@ function readJson(path) {
809
1018
  }
810
1019
  }
811
1020
  function textFileIncludes(path, needles) {
812
- if (!existsSync9(path)) return false;
1021
+ if (!existsSync10(path)) return false;
813
1022
  const contents = readFileSync6(path, "utf8").toLowerCase();
814
1023
  return needles.some((needle) => contents.includes(needle.toLowerCase()));
815
1024
  }
816
1025
  function hasBookChildDirectory(targetDir, childName) {
817
- const booksDir = join9(targetDir, "books");
818
- if (!existsSync9(booksDir)) return false;
1026
+ const booksDir = join10(targetDir, "books");
1027
+ if (!existsSync10(booksDir)) return false;
819
1028
  try {
820
1029
  return readdirSync5(booksDir, { withFileTypes: true }).some(
821
- (entry) => entry.isDirectory() && existsSync9(join9(booksDir, entry.name, childName))
1030
+ (entry) => entry.isDirectory() && existsSync10(join10(booksDir, entry.name, childName))
822
1031
  );
823
1032
  } catch {
824
1033
  return false;
@@ -846,12 +1055,55 @@ function runAssets(args) {
846
1055
  if (subcommand2 === "check") {
847
1056
  return runAssetsCheck(rest);
848
1057
  }
1058
+ if (subcommand2 === "public-check") {
1059
+ return runAssetsPublicCheck(rest);
1060
+ }
849
1061
  if (subcommand2 === "npx") {
850
1062
  return runAssetsNpx(rest);
851
1063
  }
852
1064
  printUsage();
853
1065
  return 1;
854
1066
  }
1067
+ function runAssetsPublicCheck(args) {
1068
+ const options = parsePublicCheckOptions(args);
1069
+ if (!options.ok) {
1070
+ console.error(options.error);
1071
+ printUsage();
1072
+ return 1;
1073
+ }
1074
+ const registryLoad = loadAgentAssetRegistry({ agentAssetsDir: options.value.publicRoot });
1075
+ if (registryLoad.issues.length > 0) {
1076
+ for (const issue of registryLoad.issues) {
1077
+ console.error(`${issue.type}: ${issue.message}`);
1078
+ }
1079
+ return 1;
1080
+ }
1081
+ const result = checkPublicAssetPromotions({
1082
+ publicAgentAssetsDir: options.value.publicRoot,
1083
+ privateAgentAssetsDir: options.value.privateRoot,
1084
+ registry: registryLoad.registry
1085
+ });
1086
+ if (options.value.json) {
1087
+ console.log(
1088
+ JSON.stringify(
1089
+ {
1090
+ publicRoot: options.value.publicRoot,
1091
+ privateRoot: options.value.privateRoot,
1092
+ ...result
1093
+ },
1094
+ null,
1095
+ 2
1096
+ )
1097
+ );
1098
+ } else if (result.issues.length === 0) {
1099
+ console.log(`public assets check passed (${result.checked} checked)`);
1100
+ } else {
1101
+ for (const issue of result.issues) {
1102
+ console.log(`${issue.type}: ${issue.message}`);
1103
+ }
1104
+ }
1105
+ return result.issues.length === 0 ? 0 : 1;
1106
+ }
855
1107
  function runAssetsNpx(args) {
856
1108
  const [operation, ...rest] = args;
857
1109
  if (!operation || operation === "--help" || operation === "-h" || rest.includes("--help")) {
@@ -911,7 +1163,8 @@ function runAssetsCheck(args) {
911
1163
  const result = checkInstalledAssets({
912
1164
  targetDir: options.value.targetDir,
913
1165
  agentAssetsDir: loaded.agentAssetsDir,
914
- registry: loaded.registry
1166
+ registry: loaded.registry,
1167
+ strictRegistry: options.value.strictRegistry
915
1168
  });
916
1169
  if (options.value.json) {
917
1170
  console.log(JSON.stringify(result, null, 2));
@@ -999,13 +1252,15 @@ function runAssetsPlan(args) {
999
1252
  registry: loaded.registry,
1000
1253
  bundles: loadAgentAssetBundles(loaded.agentAssetsDir),
1001
1254
  bundleIds: options.value.bundleIds,
1002
- host: options.value.host
1255
+ host: options.value.host,
1256
+ placement: options.value.placement
1003
1257
  });
1004
1258
  if (options.value.json) {
1005
1259
  console.log(JSON.stringify(plan, null, 2));
1006
1260
  } else {
1007
1261
  console.log(`target: ${plan.targetDir}`);
1008
1262
  console.log(`host: ${plan.host}`);
1263
+ console.log(`placement: ${plan.placement}`);
1009
1264
  console.log(`bundles: ${plan.bundleIds.join(", ")}`);
1010
1265
  console.log(`assets: ${plan.assetIds.length}`);
1011
1266
  console.log(`actions: ${plan.actions.length}`);
@@ -1064,6 +1319,8 @@ function parseTargetJsonOptions(args) {
1064
1319
  if (!targetDir) return { ok: false, error: "Expected --target <path>" };
1065
1320
  options.targetDir = targetDir;
1066
1321
  index += 1;
1322
+ } else if (arg === "--strict-registry") {
1323
+ options.strictRegistry = true;
1067
1324
  } else if (arg === "--json") {
1068
1325
  options.json = true;
1069
1326
  } else {
@@ -1101,6 +1358,16 @@ function parsePlanOptions(args) {
1101
1358
  }
1102
1359
  options.host = host;
1103
1360
  index += 1;
1361
+ } else if (arg === "--placement") {
1362
+ const placement = args[index + 1];
1363
+ if (!isSkillPlacement(placement)) {
1364
+ return {
1365
+ ok: false,
1366
+ error: "Expected --placement auto|manual"
1367
+ };
1368
+ }
1369
+ options.placement = placement;
1370
+ index += 1;
1104
1371
  } else if (arg === "--out") {
1105
1372
  const outPath = args[index + 1];
1106
1373
  if (!outPath) return { ok: false, error: "Expected --out <path>" };
@@ -1167,6 +1434,52 @@ function parseNpxOptions(operation, args) {
1167
1434
  }
1168
1435
  return { ok: true, value: options };
1169
1436
  }
1437
+ function parsePublicCheckOptions(args) {
1438
+ const defaults = getDefaultPublicCheckRoots();
1439
+ const options = {
1440
+ publicRoot: defaults.publicRoot,
1441
+ privateRoot: defaults.privateRoot,
1442
+ json: false
1443
+ };
1444
+ for (let index = 0; index < args.length; index += 1) {
1445
+ const arg = args[index];
1446
+ if (arg === "--public-root") {
1447
+ const publicRoot = args[index + 1];
1448
+ if (!publicRoot) return { ok: false, error: "Expected --public-root <path>" };
1449
+ options.publicRoot = publicRoot;
1450
+ index += 1;
1451
+ } else if (arg === "--private-root") {
1452
+ const privateRoot = args[index + 1];
1453
+ if (!privateRoot) return { ok: false, error: "Expected --private-root <path>" };
1454
+ options.privateRoot = privateRoot;
1455
+ index += 1;
1456
+ } else if (arg === "--json") {
1457
+ options.json = true;
1458
+ } else {
1459
+ return { ok: false, error: `Unknown assets public-check option: ${arg}` };
1460
+ }
1461
+ }
1462
+ if (!existsSync11(options.publicRoot)) {
1463
+ return { ok: false, error: `Public agent assets root does not exist: ${options.publicRoot}` };
1464
+ }
1465
+ if (!existsSync11(options.privateRoot)) {
1466
+ return { ok: false, error: `Private agent assets root does not exist: ${options.privateRoot}` };
1467
+ }
1468
+ return { ok: true, value: options };
1469
+ }
1470
+ function getDefaultPublicCheckRoots() {
1471
+ const defaultRegistryRoot = loadAgentAssetRegistry().agentAssetsDir;
1472
+ if (defaultRegistryRoot.endsWith("public-agent-assets")) {
1473
+ return {
1474
+ privateRoot: join11(dirname5(defaultRegistryRoot), "agent-assets"),
1475
+ publicRoot: defaultRegistryRoot
1476
+ };
1477
+ }
1478
+ return {
1479
+ privateRoot: defaultRegistryRoot,
1480
+ publicRoot: join11(dirname5(defaultRegistryRoot), "public-agent-assets")
1481
+ };
1482
+ }
1170
1483
  function listRegistryAssets(options) {
1171
1484
  const loaded = loadAgentAssetRegistry();
1172
1485
  if (loaded.issues.length > 0) {
@@ -1205,14 +1518,18 @@ function isVisibilityFilter(value) {
1205
1518
  function isHost(value) {
1206
1519
  return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
1207
1520
  }
1521
+ function isSkillPlacement(value) {
1522
+ return value === "auto" || value === "manual";
1523
+ }
1208
1524
  function printUsage() {
1209
1525
  console.error("Usage:");
1210
1526
  console.error(" pro-gov assets list [--registry] [--json] [--visibility public|private|third-party|all]");
1211
1527
  console.error(" pro-gov assets discover [--target <path>] [--json]");
1212
1528
  console.error(" pro-gov assets recommend [--target <path>] [--json]");
1213
- console.error(" pro-gov assets plan --bundle <bundle-id> [--bundle <bundle-id>] [--target <path>] [--host codex|claude-code|gemini-cli|antigravity] [--out <path>] [--json]");
1529
+ console.error(" pro-gov assets plan --bundle <bundle-id> [--bundle <bundle-id>] [--target <path>] [--host codex|claude-code|gemini-cli|antigravity] [--placement auto|manual] [--out <path>] [--json]");
1214
1530
  console.error(" pro-gov assets apply --plan <path>");
1215
1531
  console.error(" pro-gov assets check [--target <path>] [--json]");
1532
+ console.error(" pro-gov assets public-check [--public-root <path>] [--private-root <path>] [--json]");
1216
1533
  console.error(" pro-gov assets npx add <source> [--skill <name>] --plan [--root <path>]");
1217
1534
  console.error(" pro-gov assets npx update [--skill <name>] --plan [--root <path>]");
1218
1535
  }
@@ -1225,12 +1542,11 @@ function printNpxUsage() {
1225
1542
 
1226
1543
  // src/commands/doctor.ts
1227
1544
  import { spawnSync as spawnSync2 } from "node:child_process";
1228
- import { existsSync as existsSync10 } from "node:fs";
1545
+ import { existsSync as existsSync12 } from "node:fs";
1229
1546
  import { createRequire } from "node:module";
1230
- import { dirname as dirname6, join as join10 } from "node:path";
1547
+ import { dirname as dirname6, join as join12 } from "node:path";
1231
1548
  var REQUIRED_ASSETS = [
1232
1549
  "starter/AGENTS.template.md",
1233
- "starter/.gemini/settings.json",
1234
1550
  "starter/docs/governance/ssot-v0.9.md",
1235
1551
  "starter/docs/governance/agents-routing/engineering-runtime-v0.9.md",
1236
1552
  "starter/docs/governance/agents-routing/doc-only-v0.9.md",
@@ -1278,8 +1594,8 @@ function resolveDocGovDependencyCli() {
1278
1594
  try {
1279
1595
  const require2 = createRequire(import.meta.url);
1280
1596
  const packageJsonPath = require2.resolve("@pieai/doc-gov/package.json");
1281
- const cliPath = join10(dirname6(packageJsonPath), "dist/cli.js");
1282
- return existsSync10(cliPath) ? cliPath : null;
1597
+ const cliPath = join12(dirname6(packageJsonPath), "dist/cli.js");
1598
+ return existsSync12(cliPath) ? cliPath : null;
1283
1599
  } catch {
1284
1600
  return null;
1285
1601
  }
@@ -1421,8 +1737,8 @@ function bulletList(values) {
1421
1737
 
1422
1738
  // src/lens/scan.ts
1423
1739
  import { spawnSync as spawnSync3 } from "node:child_process";
1424
- import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync3 } from "node:fs";
1425
- import { join as join11, relative as relative4 } from "node:path";
1740
+ import { existsSync as existsSync13, readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync3 } from "node:fs";
1741
+ import { join as join13, relative as relative5 } from "node:path";
1426
1742
  var ignoredDirectories = /* @__PURE__ */ new Set([
1427
1743
  ".git",
1428
1744
  ".next",
@@ -1438,23 +1754,23 @@ function scanProjectLensTarget(targetDir, options = {}) {
1438
1754
  const packageJson = readPackageJson(targetDir);
1439
1755
  return {
1440
1756
  targetDir,
1441
- aiEntryFiles: ["AGENTS.md", "CLAUDE.md", "GEMINI.md"].filter(
1442
- (file) => existsSync11(join11(targetDir, file))
1757
+ aiEntryFiles: ["AGENTS.md", "CLAUDE.md"].filter(
1758
+ (file) => existsSync13(join13(targetDir, file))
1443
1759
  ),
1444
- aiConfigFiles: [".gemini/settings.json"].filter((file) => existsSync11(join11(targetDir, file))),
1760
+ aiConfigFiles: [],
1445
1761
  packageJson,
1446
1762
  docs: {
1447
- hasDocsDirectory: existsSync11(join11(targetDir, "docs")),
1763
+ hasDocsDirectory: existsSync13(join13(targetDir, "docs")),
1448
1764
  markdownFileCount: markdownFiles.length,
1449
1765
  governanceFiles: markdownFiles.filter((file) => file.startsWith("docs/governance/") || file.startsWith("docs/policy/")).sort()
1450
1766
  },
1451
1767
  git: readGitState(targetDir),
1452
- largeFiles: files.map((file) => ({ path: file, bytes: statSync3(join11(targetDir, file)).size })).filter((file) => file.bytes >= largeFileBytes).sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path)).slice(0, 25)
1768
+ largeFiles: files.map((file) => ({ path: file, bytes: statSync3(join13(targetDir, file)).size })).filter((file) => file.bytes >= largeFileBytes).sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path)).slice(0, 25)
1453
1769
  };
1454
1770
  }
1455
1771
  function readPackageJson(targetDir) {
1456
- const packageJsonPath = join11(targetDir, "package.json");
1457
- if (!existsSync11(packageJsonPath)) return void 0;
1772
+ const packageJsonPath = join13(targetDir, "package.json");
1773
+ if (!existsSync13(packageJsonPath)) return void 0;
1458
1774
  try {
1459
1775
  const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf8"));
1460
1776
  return {
@@ -1491,13 +1807,13 @@ function listProjectFiles(targetDir) {
1491
1807
  return files.sort();
1492
1808
  }
1493
1809
  function collectFiles2(rootDir, currentDir, files) {
1494
- if (!existsSync11(currentDir)) return;
1810
+ if (!existsSync13(currentDir)) return;
1495
1811
  for (const entry of readdirSync6(currentDir, { withFileTypes: true })) {
1496
1812
  if (entry.isDirectory()) {
1497
1813
  if (ignoredDirectories.has(entry.name)) continue;
1498
- collectFiles2(rootDir, join11(currentDir, entry.name), files);
1814
+ collectFiles2(rootDir, join13(currentDir, entry.name), files);
1499
1815
  } else if (entry.isFile()) {
1500
- files.push(toUnixPath4(relative4(rootDir, join11(currentDir, entry.name))));
1816
+ files.push(toUnixPath4(relative5(rootDir, join13(currentDir, entry.name))));
1501
1817
  }
1502
1818
  }
1503
1819
  }
@@ -1591,9 +1907,441 @@ function printUsage2() {
1591
1907
  console.error("Usage: pro-gov lens report --target <path> --out <path>");
1592
1908
  }
1593
1909
 
1910
+ // src/commands/portfolio.ts
1911
+ import { existsSync as existsSync15 } from "node:fs";
1912
+ import { join as join14 } from "node:path";
1913
+
1914
+ // src/portfolio/manifest.ts
1915
+ import { existsSync as existsSync14, readFileSync as readFileSync9 } from "node:fs";
1916
+ function loadPortfolioManifest(configPath) {
1917
+ let parsed;
1918
+ try {
1919
+ parsed = JSON.parse(readFileSync9(configPath, "utf8"));
1920
+ } catch (error) {
1921
+ return {
1922
+ configPath,
1923
+ issues: [
1924
+ {
1925
+ type: "invalid-json",
1926
+ message: error instanceof Error ? error.message : String(error)
1927
+ }
1928
+ ]
1929
+ };
1930
+ }
1931
+ const issues = validatePortfolioManifest(parsed);
1932
+ return {
1933
+ configPath,
1934
+ manifest: issues.length === 0 ? parsed : void 0,
1935
+ issues
1936
+ };
1937
+ }
1938
+ function validatePortfolioManifest(value) {
1939
+ const issues = [];
1940
+ if (!isRecord(value)) {
1941
+ return [
1942
+ {
1943
+ type: "invalid-field",
1944
+ field: "root",
1945
+ message: "Portfolio manifest must be a JSON object."
1946
+ }
1947
+ ];
1948
+ }
1949
+ if (value.schemaVersion !== 1) {
1950
+ issues.push({
1951
+ type: "invalid-field",
1952
+ field: "schemaVersion",
1953
+ message: "Portfolio manifest schemaVersion must be 1."
1954
+ });
1955
+ }
1956
+ if (typeof value.portfolioId !== "string" || value.portfolioId.length === 0) {
1957
+ issues.push({
1958
+ type: "invalid-field",
1959
+ field: "portfolioId",
1960
+ message: "Portfolio manifest portfolioId must be a non-empty string."
1961
+ });
1962
+ }
1963
+ validateAllowedFields(value, "root", ["schemaVersion", "portfolioId", "controlPlane", "executionEngine", "targets"], issues);
1964
+ validateEndpoint(value.controlPlane, "controlPlane", issues);
1965
+ validateEndpoint(value.executionEngine, "executionEngine", issues);
1966
+ if (!Array.isArray(value.targets)) {
1967
+ issues.push({
1968
+ type: "invalid-field",
1969
+ field: "targets",
1970
+ message: "Portfolio manifest targets must be an array."
1971
+ });
1972
+ return issues;
1973
+ }
1974
+ const seenTargetIds = /* @__PURE__ */ new Set();
1975
+ for (const target of value.targets) {
1976
+ if (!isRecord(target)) {
1977
+ issues.push({
1978
+ type: "invalid-field",
1979
+ field: "targets",
1980
+ message: "Portfolio target must be an object."
1981
+ });
1982
+ continue;
1983
+ }
1984
+ validateEndpoint(target, "targets", issues);
1985
+ validateAllowedFields(target, "target", ["id", "path", "profile", "assetBundles"], issues);
1986
+ if (typeof target.id === "string") {
1987
+ if (seenTargetIds.has(target.id)) {
1988
+ issues.push({
1989
+ type: "duplicate-target-id",
1990
+ id: target.id,
1991
+ message: `Duplicate portfolio target id: ${target.id}`
1992
+ });
1993
+ }
1994
+ seenTargetIds.add(target.id);
1995
+ }
1996
+ if (target.profile !== void 0 && (typeof target.profile !== "string" || !isValidProfile(target.profile))) {
1997
+ issues.push({
1998
+ type: "invalid-field",
1999
+ id: typeof target.id === "string" ? target.id : void 0,
2000
+ field: "profile",
2001
+ message: "Portfolio target profile must be engineering-runtime or doc-only."
2002
+ });
2003
+ }
2004
+ validateOptionalStringArray(target.assetBundles, target.id, "assetBundles", issues);
2005
+ if ("sharedRules" in target) {
2006
+ issues.push({
2007
+ type: "invalid-field",
2008
+ id: typeof target.id === "string" ? target.id : void 0,
2009
+ field: "sharedRules",
2010
+ message: "Portfolio target sharedRules is not managed yet; remove it until plan/check supports it."
2011
+ });
2012
+ }
2013
+ }
2014
+ return issues;
2015
+ }
2016
+ function getDefaultPortfolioTargets(manifest) {
2017
+ return manifest?.targets ?? [];
2018
+ }
2019
+ function validateAllowedFields(value, location, allowedFields, issues) {
2020
+ const allowed = new Set(allowedFields);
2021
+ for (const field of Object.keys(value)) {
2022
+ if (allowed.has(field)) continue;
2023
+ issues.push({
2024
+ type: "invalid-field",
2025
+ id: typeof value.id === "string" ? value.id : void 0,
2026
+ field,
2027
+ message: `Unknown portfolio ${location} field: ${field}`
2028
+ });
2029
+ }
2030
+ }
2031
+ function validateEndpoint(value, field, issues) {
2032
+ if (value === void 0) return;
2033
+ if (!isRecord(value)) {
2034
+ issues.push({
2035
+ type: "invalid-field",
2036
+ field,
2037
+ message: `Portfolio ${field} must be an object.`
2038
+ });
2039
+ return;
2040
+ }
2041
+ if (typeof value.id !== "string" || value.id.length === 0) {
2042
+ issues.push({
2043
+ type: "invalid-field",
2044
+ field: `${field}.id`,
2045
+ message: `Portfolio ${field} id must be a non-empty string.`
2046
+ });
2047
+ }
2048
+ if (typeof value.path !== "string" || value.path.length === 0) {
2049
+ issues.push({
2050
+ type: "invalid-field",
2051
+ id: typeof value.id === "string" ? value.id : void 0,
2052
+ field: `${field}.path`,
2053
+ message: `Portfolio ${field} path must be a non-empty string.`
2054
+ });
2055
+ return;
2056
+ }
2057
+ if (!existsSync14(value.path)) {
2058
+ issues.push({
2059
+ type: "missing-path",
2060
+ id: typeof value.id === "string" ? value.id : void 0,
2061
+ field: `${field}.path`,
2062
+ message: `Portfolio ${field} path does not exist: ${value.path}`
2063
+ });
2064
+ }
2065
+ }
2066
+ function validateOptionalStringArray(value, id, field, issues) {
2067
+ if (value === void 0) return;
2068
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
2069
+ issues.push({
2070
+ type: "invalid-field",
2071
+ id: typeof id === "string" ? id : void 0,
2072
+ field,
2073
+ message: `Portfolio target ${field} must be an array of strings.`
2074
+ });
2075
+ }
2076
+ }
2077
+ function isRecord(value) {
2078
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2079
+ }
2080
+
2081
+ // src/commands/portfolio.ts
2082
+ function runPortfolio(args) {
2083
+ const [subcommand2, ...rest] = args;
2084
+ if (subcommand2 === "check") return runPortfolioCheck(rest);
2085
+ if (subcommand2 === "plan") return runPortfolioPlan(rest);
2086
+ if (subcommand2 === "assets-check") return runPortfolioAssetsCheck(rest);
2087
+ printUsage3();
2088
+ return 1;
2089
+ }
2090
+ function runPortfolioCheck(args) {
2091
+ const options = parsePortfolioOptions(args);
2092
+ if (!options.ok) {
2093
+ console.error(options.error);
2094
+ printUsage3();
2095
+ return 1;
2096
+ }
2097
+ const loaded = loadPortfolioManifest(options.value.configPath);
2098
+ const targetIds = getDefaultPortfolioTargets(loaded.manifest).map((target) => target.id);
2099
+ if (options.value.json) {
2100
+ console.log(
2101
+ JSON.stringify(
2102
+ {
2103
+ ok: loaded.issues.length === 0,
2104
+ configPath: loaded.configPath,
2105
+ portfolioId: loaded.manifest?.portfolioId,
2106
+ targetIds,
2107
+ issues: loaded.issues
2108
+ },
2109
+ null,
2110
+ 2
2111
+ )
2112
+ );
2113
+ } else if (loaded.issues.length === 0) {
2114
+ console.log(`portfolio check passed (${targetIds.length} targets)`);
2115
+ } else {
2116
+ for (const issue of loaded.issues) {
2117
+ console.log(`${issue.type}: ${issue.message}`);
2118
+ }
2119
+ }
2120
+ return loaded.issues.length === 0 ? 0 : 1;
2121
+ }
2122
+ function runPortfolioPlan(args) {
2123
+ const options = parsePortfolioOptions(args);
2124
+ if (!options.ok) {
2125
+ console.error(options.error);
2126
+ printUsage3();
2127
+ return 1;
2128
+ }
2129
+ const loaded = loadPortfolioManifest(options.value.configPath);
2130
+ if (loaded.issues.length > 0 || !loaded.manifest) {
2131
+ for (const issue of loaded.issues) {
2132
+ console.error(`${issue.type}: ${issue.message}`);
2133
+ }
2134
+ return 1;
2135
+ }
2136
+ const targets = getDefaultPortfolioTargets(loaded.manifest).filter(
2137
+ (target) => !options.value.targetId || options.value.targetId === "all" || target.id === options.value.targetId
2138
+ );
2139
+ if (targets.length === 0) {
2140
+ console.error(`Unknown portfolio target: ${options.value.targetId}`);
2141
+ return 1;
2142
+ }
2143
+ const loadedAssets = loadAgentAssetRegistry({
2144
+ agentAssetsDir: findPortfolioAgentAssetsDir(loaded.manifest)
2145
+ });
2146
+ if (loadedAssets.issues.length > 0) {
2147
+ for (const issue of loadedAssets.issues) {
2148
+ console.error(`${issue.type}: ${issue.message}`);
2149
+ }
2150
+ return 1;
2151
+ }
2152
+ const bundles = loadAgentAssetBundles(loadedAssets.agentAssetsDir);
2153
+ try {
2154
+ const plans = targets.map((target) => {
2155
+ const bundleIds = target.assetBundles ?? [];
2156
+ if (bundleIds.length === 0) {
2157
+ throw new Error(`Portfolio target ${target.id} has no assetBundles`);
2158
+ }
2159
+ return {
2160
+ id: target.id,
2161
+ path: target.path,
2162
+ plan: createAssetInstallPlan({
2163
+ targetDir: target.path,
2164
+ agentAssetsDir: loadedAssets.agentAssetsDir,
2165
+ registry: loadedAssets.registry,
2166
+ bundles,
2167
+ bundleIds,
2168
+ host: options.value.host
2169
+ })
2170
+ };
2171
+ });
2172
+ if (options.value.json) {
2173
+ console.log(
2174
+ JSON.stringify(
2175
+ {
2176
+ configPath: loaded.configPath,
2177
+ portfolioId: loaded.manifest.portfolioId,
2178
+ targets: plans
2179
+ },
2180
+ null,
2181
+ 2
2182
+ )
2183
+ );
2184
+ } else {
2185
+ console.log(`portfolio plan targets: ${plans.length}`);
2186
+ for (const target of plans) {
2187
+ console.log(`${target.id} ${target.plan.actions.length} actions dry-run`);
2188
+ }
2189
+ }
2190
+ return 0;
2191
+ } catch (error) {
2192
+ console.error(error instanceof Error ? error.message : String(error));
2193
+ return 1;
2194
+ }
2195
+ }
2196
+ function runPortfolioAssetsCheck(args) {
2197
+ const options = parsePortfolioOptions(args);
2198
+ if (!options.ok) {
2199
+ console.error(options.error);
2200
+ printUsage3();
2201
+ return 1;
2202
+ }
2203
+ const loaded = loadPortfolioManifest(options.value.configPath);
2204
+ if (loaded.issues.length > 0 || !loaded.manifest) {
2205
+ if (options.value.json) {
2206
+ console.log(
2207
+ JSON.stringify(
2208
+ {
2209
+ ok: false,
2210
+ configPath: loaded.configPath,
2211
+ issues: loaded.issues,
2212
+ targets: []
2213
+ },
2214
+ null,
2215
+ 2
2216
+ )
2217
+ );
2218
+ } else {
2219
+ for (const issue of loaded.issues) {
2220
+ console.error(`${issue.type}: ${issue.message}`);
2221
+ }
2222
+ }
2223
+ return 1;
2224
+ }
2225
+ const targets = getDefaultPortfolioTargets(loaded.manifest).filter(
2226
+ (target) => !options.value.targetId || options.value.targetId === "all" || target.id === options.value.targetId
2227
+ );
2228
+ if (targets.length === 0) {
2229
+ console.error(`Unknown portfolio target: ${options.value.targetId}`);
2230
+ return 1;
2231
+ }
2232
+ const loadedAssets = loadAgentAssetRegistry({
2233
+ agentAssetsDir: findPortfolioAgentAssetsDir(loaded.manifest)
2234
+ });
2235
+ if (loadedAssets.issues.length > 0) {
2236
+ if (options.value.json) {
2237
+ console.log(
2238
+ JSON.stringify(
2239
+ {
2240
+ ok: false,
2241
+ configPath: loaded.configPath,
2242
+ portfolioId: loaded.manifest.portfolioId,
2243
+ registryIssues: loadedAssets.issues,
2244
+ targets: []
2245
+ },
2246
+ null,
2247
+ 2
2248
+ )
2249
+ );
2250
+ } else {
2251
+ for (const issue of loadedAssets.issues) {
2252
+ console.error(`${issue.type}: ${issue.message}`);
2253
+ }
2254
+ }
2255
+ return 1;
2256
+ }
2257
+ const targetResults = targets.map((target) => {
2258
+ const result = checkInstalledAssets({
2259
+ targetDir: target.path,
2260
+ agentAssetsDir: loadedAssets.agentAssetsDir,
2261
+ registry: loadedAssets.registry,
2262
+ strictRegistry: true
2263
+ });
2264
+ return {
2265
+ id: target.id,
2266
+ path: target.path,
2267
+ issues: result.issues
2268
+ };
2269
+ });
2270
+ const ok = targetResults.every((target) => target.issues.length === 0);
2271
+ if (options.value.json) {
2272
+ console.log(
2273
+ JSON.stringify(
2274
+ {
2275
+ ok,
2276
+ configPath: loaded.configPath,
2277
+ portfolioId: loaded.manifest.portfolioId,
2278
+ agentAssetsDir: loadedAssets.agentAssetsDir,
2279
+ targets: targetResults
2280
+ },
2281
+ null,
2282
+ 2
2283
+ )
2284
+ );
2285
+ } else if (ok) {
2286
+ console.log(`portfolio assets check passed (${targetResults.length} targets)`);
2287
+ } else {
2288
+ for (const target of targetResults) {
2289
+ for (const issue of target.issues) {
2290
+ console.log(`${target.id} ${issue.type}: ${issue.message}`);
2291
+ }
2292
+ }
2293
+ }
2294
+ return ok ? 0 : 1;
2295
+ }
2296
+ function parsePortfolioOptions(args) {
2297
+ const options = {
2298
+ configPath: "",
2299
+ host: "codex",
2300
+ json: false
2301
+ };
2302
+ for (let index = 0; index < args.length; index += 1) {
2303
+ const arg = args[index];
2304
+ if (arg === "--config") {
2305
+ const configPath = args[index + 1];
2306
+ if (!configPath) return { ok: false, error: "Expected --config <path>" };
2307
+ options.configPath = configPath;
2308
+ index += 1;
2309
+ } else if (arg === "--target") {
2310
+ const targetId = args[index + 1];
2311
+ if (!targetId) return { ok: false, error: "Expected --target <id|all>" };
2312
+ options.targetId = targetId;
2313
+ index += 1;
2314
+ } else if (arg === "--host") {
2315
+ const host = args[index + 1];
2316
+ if (!isHost2(host)) return { ok: false, error: "Expected --host codex|claude-code" };
2317
+ options.host = host;
2318
+ index += 1;
2319
+ } else if (arg === "--json") {
2320
+ options.json = true;
2321
+ } else {
2322
+ return { ok: false, error: `Unknown portfolio option: ${arg}` };
2323
+ }
2324
+ }
2325
+ if (!options.configPath) return { ok: false, error: "Expected --config <path>" };
2326
+ return { ok: true, value: options };
2327
+ }
2328
+ function isHost2(value) {
2329
+ return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
2330
+ }
2331
+ function findPortfolioAgentAssetsDir(manifest) {
2332
+ const agentAssetsDir = manifest?.executionEngine?.path ? join14(manifest.executionEngine.path, "agent-assets") : void 0;
2333
+ return agentAssetsDir && existsSync15(join14(agentAssetsDir, "registry.json")) ? agentAssetsDir : void 0;
2334
+ }
2335
+ function printUsage3() {
2336
+ console.error("Usage:");
2337
+ console.error(" pro-gov portfolio check --config <path> [--json]");
2338
+ console.error(" pro-gov portfolio plan --config <path> [--target <id|all>] [--host codex|claude-code|gemini-cli|antigravity] [--json]");
2339
+ console.error(" pro-gov portfolio assets-check --config <path> [--target <id|all>] [--json]");
2340
+ }
2341
+
1594
2342
  // src/commands/sync.ts
1595
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
1596
- import { join as join12 } from "node:path";
2343
+ import { existsSync as existsSync16, readFileSync as readFileSync10 } from "node:fs";
2344
+ import { join as join15 } from "node:path";
1597
2345
  function runSync(args) {
1598
2346
  if (!args.includes("--check")) {
1599
2347
  console.error("pro-gov sync requires --check in this first read-only release.");
@@ -1602,14 +2350,14 @@ function runSync(args) {
1602
2350
  let differences = 0;
1603
2351
  console.log("pro-gov sync check");
1604
2352
  for (const file of planStarterFiles()) {
1605
- const targetPath = join12(process.cwd(), file.targetPath);
1606
- if (!existsSync12(targetPath)) {
2353
+ const targetPath = join15(process.cwd(), file.targetPath);
2354
+ if (!existsSync16(targetPath)) {
1607
2355
  console.log(`missing: ${file.targetPath}`);
1608
2356
  differences += 1;
1609
2357
  continue;
1610
2358
  }
1611
- const source = readFileSync9(file.absoluteSourcePath, "utf8");
1612
- const target = readFileSync9(targetPath, "utf8");
2359
+ const source = readFileSync10(file.absoluteSourcePath, "utf8");
2360
+ const target = readFileSync10(targetPath, "utf8");
1613
2361
  if (source !== target) {
1614
2362
  console.log(`different: ${file.targetPath}`);
1615
2363
  differences += 1;
@@ -1630,8 +2378,12 @@ var COMMANDS = [
1630
2378
  "assets recommend [--target <path>] [--json]",
1631
2379
  "assets plan --bundle <bundle-id> [--target <path>] [--json]",
1632
2380
  "assets apply --plan <path>",
1633
- "assets check [--target <path>] [--json]",
2381
+ "assets check [--target <path>] [--strict-registry] [--json]",
2382
+ "assets public-check [--public-root <path>] [--private-root <path>] [--json]",
1634
2383
  "assets npx add|update ... --plan",
2384
+ "portfolio check --config <path> [--json]",
2385
+ "portfolio plan --config <path> [--target <id|all>] [--json]",
2386
+ "portfolio assets-check --config <path> [--target <id|all>] [--json]",
1635
2387
  "lens scan [--target <path>] [--json]",
1636
2388
  "lens inspect [--target <path>] [--format text|json]",
1637
2389
  "lens report --target <path> --out <path>",
@@ -1647,6 +2399,8 @@ if (!command || command === "--help" || command === "-h") {
1647
2399
  process.exitCode = runAssets(process.argv.slice(3));
1648
2400
  } else if (command === "lens") {
1649
2401
  process.exitCode = runLens(process.argv.slice(3));
2402
+ } else if (command === "portfolio") {
2403
+ process.exitCode = runPortfolio(process.argv.slice(3));
1650
2404
  } else if (command === "init") {
1651
2405
  process.exitCode = runInit(process.argv.slice(3));
1652
2406
  } else if (command === "sync") {