@pieai/pro-gov 0.3.10 → 0.3.12

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
@@ -79,7 +79,14 @@ function createNpxSkillsMaintenancePlan(options) {
79
79
  cpSync(options.npxRoot, tempRoot, { recursive: true, dereference: false });
80
80
  const command2 = buildNpxCommand(options);
81
81
  const runner = options.runner ?? defaultRunner;
82
- const result = runner({ command: command2, cwd: tempRoot });
82
+ const timeoutMs = options.timeoutMs ?? 3e5;
83
+ const result = runner({ command: command2, cwd: tempRoot, timeoutMs });
84
+ if (result.timedOut) {
85
+ throw new Error(`npx skills ${options.operation} timed out after ${timeoutMs}ms`);
86
+ }
87
+ if (result.status === null && result.signal) {
88
+ throw new Error(`npx skills ${options.operation} terminated by ${result.signal}`);
89
+ }
83
90
  if (result.status !== 0) {
84
91
  throw new Error(`npx skills ${options.operation} failed with exit code ${result.status}`);
85
92
  }
@@ -128,12 +135,18 @@ function buildNpxCommand(options) {
128
135
  if (options.skill) command2.push(options.skill);
129
136
  return command2;
130
137
  }
131
- function defaultRunner({ command: command2, cwd }) {
132
- const result = spawnSync(command2[0] ?? "npx", command2.slice(1), { cwd, encoding: "utf8" });
138
+ function defaultRunner({ command: command2, cwd, timeoutMs }) {
139
+ const result = spawnSync(command2[0] ?? "npx", command2.slice(1), {
140
+ cwd,
141
+ encoding: "utf8",
142
+ timeout: timeoutMs
143
+ });
133
144
  return {
134
145
  status: result.status,
135
146
  stdout: result.stdout,
136
- stderr: result.stderr
147
+ stderr: result.stderr,
148
+ signal: result.signal,
149
+ timedOut: result.error?.code === "ETIMEDOUT"
137
150
  };
138
151
  }
139
152
  function snapshotFiles(root) {
@@ -536,8 +549,229 @@ function resolveSafePath(root, sourcePath) {
536
549
  }
537
550
 
538
551
  // src/asset-targets/apply.ts
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";
552
+ import {
553
+ existsSync as existsSync8,
554
+ lstatSync as lstatSync3,
555
+ mkdirSync as mkdirSync2,
556
+ readlinkSync as readlinkSync2,
557
+ realpathSync,
558
+ symlinkSync,
559
+ unlinkSync,
560
+ writeFileSync
561
+ } from "node:fs";
562
+ import { dirname as dirname4, join as join8, relative as relative4, resolve as resolve2 } from "node:path";
563
+
564
+ // src/asset-targets/install-plan.ts
565
+ import { existsSync as existsSync7, lstatSync as lstatSync2, readFileSync as readFileSync4, readlinkSync } from "node:fs";
566
+ import { basename, dirname as dirname3, join as join7, resolve } from "node:path";
567
+ function createAssetInstallPlan(options) {
568
+ const placement = options.placement ?? "registry";
569
+ const assetsById = new Map(options.registry.assets.map((asset) => [asset.id, asset]));
570
+ const bundlesById = new Map(options.bundles.map((bundle) => [bundle.id, bundle]));
571
+ const assetIds = resolveBundleAssetIds(options.bundleIds, bundlesById);
572
+ const assets = assetIds.map((assetId) => {
573
+ const asset = assetsById.get(assetId);
574
+ if (!asset) throw new Error(`Unknown asset id in bundle: ${assetId}`);
575
+ if (asset.kind === "skill" && !asset.hosts.includes(options.host)) {
576
+ throw new Error(`Asset ${assetId} does not support host ${options.host}`);
577
+ }
578
+ return asset;
579
+ });
580
+ const lockEntries = createAgentAssetLockEntries(options.registry, options.agentAssetsDir, assetIds);
581
+ const managedEntries = readManagedEntries(options.targetDir);
582
+ const managedTargets = new Set(managedEntries.map((entry) => entry.targetPath));
583
+ const assetActions = assets.map(
584
+ (asset) => createAssetAction(asset, options.agentAssetsDir, options.targetDir, options.host, placement, managedTargets)
585
+ );
586
+ const manifest = {
587
+ schemaVersion: 1,
588
+ host: options.host,
589
+ placement,
590
+ bundleIds: [...options.bundleIds],
591
+ assetIds
592
+ };
593
+ const lockfile = {
594
+ schemaVersion: 1,
595
+ host: options.host,
596
+ placement,
597
+ bundleIds: [...options.bundleIds],
598
+ assets: lockEntries.map((entry) => {
599
+ const action = assetActions.find((candidate) => "assetId" in candidate && candidate.assetId === entry.id);
600
+ return {
601
+ ...entry,
602
+ targetPath: action && "targetPath" in action ? action.targetPath : ""
603
+ };
604
+ })
605
+ };
606
+ const writeActions = [
607
+ {
608
+ type: "write-file",
609
+ targetPath: ".pro-gov/assets.json",
610
+ content: `${JSON.stringify(manifest, null, 2)}
611
+ `
612
+ },
613
+ {
614
+ type: "write-file",
615
+ targetPath: ".pro-gov/assets.lock.json",
616
+ content: `${JSON.stringify(lockfile, null, 2)}
617
+ `
618
+ }
619
+ ];
620
+ const expectedTargetPaths = new Set(
621
+ assetActions.filter((action) => "assetId" in action).map((action) => action.targetPath)
622
+ );
623
+ const removalActions = createRemovalActions(
624
+ options.targetDir,
625
+ options.agentAssetsDir,
626
+ managedEntries,
627
+ expectedTargetPaths
628
+ );
629
+ return {
630
+ schemaVersion: 1,
631
+ dryRun: true,
632
+ targetDir: options.targetDir,
633
+ host: options.host,
634
+ placement,
635
+ bundleIds: [...options.bundleIds],
636
+ assetIds,
637
+ actions: [
638
+ ...createDirectoryActions([...assetActions, ...writeActions]),
639
+ ...removalActions,
640
+ ...assetActions,
641
+ ...writeActions
642
+ ]
643
+ };
644
+ }
645
+ function resolveBundleAssetIds(bundleIds, bundlesById) {
646
+ const ids = /* @__PURE__ */ new Set();
647
+ for (const bundleId of bundleIds) {
648
+ const bundle = bundlesById.get(bundleId);
649
+ if (!bundle) throw new Error(`Unknown bundle id: ${bundleId}`);
650
+ for (const assetId of bundle.assets) {
651
+ ids.add(assetId);
652
+ }
653
+ }
654
+ return [...ids].sort();
655
+ }
656
+ function createAssetAction(asset, agentAssetsDir, targetDir, host, placement, managedTargets) {
657
+ if (asset.kind === "skill" && asset.defaultScope === "user") {
658
+ throw new Error(
659
+ `User-scoped asset ${asset.id} must be linked at the user level, not installed into a project target.`
660
+ );
661
+ }
662
+ const sourcePath = join7(agentAssetsDir, asset.sourcePath);
663
+ const targetPath = resolveHostTargetPath(asset, host, placement);
664
+ const targetAbsolutePath = join7(targetDir, targetPath);
665
+ const targetExists = pathExistsEvenIfDanglingSymlink2(targetAbsolutePath);
666
+ if (targetExists) {
667
+ const stats = lstatSync2(targetAbsolutePath);
668
+ if (stats.isSymbolicLink() && managedTargets.has(targetPath)) {
669
+ return {
670
+ type: "update-symlink",
671
+ assetId: asset.id,
672
+ sourcePath,
673
+ targetPath
674
+ };
675
+ }
676
+ throw new Error(`Refusing to overwrite unmanaged target: ${targetPath}`);
677
+ }
678
+ return {
679
+ type: "symlink",
680
+ assetId: asset.id,
681
+ sourcePath,
682
+ targetPath
683
+ };
684
+ }
685
+ function resolveHostTargetPath(asset, host, placement) {
686
+ if (asset.kind === "skill") {
687
+ const effectivePlacement = resolveSkillPlacement(asset, placement);
688
+ if (host === "claude-code") {
689
+ if (placement === "manual") {
690
+ throw new Error("Manual skill placement is only supported for .agents hosts");
691
+ }
692
+ return `.claude/skills/${basename(asset.sourcePath)}`;
693
+ }
694
+ if (effectivePlacement === "manual") {
695
+ return `.agents/manual-skills/${basename(asset.sourcePath)}`;
696
+ }
697
+ return `.agents/skills/${basename(asset.sourcePath)}`;
698
+ }
699
+ if (asset.kind === "rule") {
700
+ return `.pro-gov/agent-assets/rules/${basename(asset.sourcePath)}`;
701
+ }
702
+ return `.pro-gov/agent-assets/commands/${basename(asset.sourcePath)}`;
703
+ }
704
+ function resolveSkillPlacement(asset, placement) {
705
+ if (placement !== "registry") return placement;
706
+ return asset.defaultPlacement ?? "auto";
707
+ }
708
+ function createDirectoryActions(actions) {
709
+ const directories = /* @__PURE__ */ new Set();
710
+ for (const action of actions) {
711
+ if (action.type === "create-dir") continue;
712
+ const directory = dirname3(action.targetPath);
713
+ if (directory !== ".") directories.add(directory);
714
+ }
715
+ return [...directories].sort().map((targetPath) => ({ type: "create-dir", targetPath }));
716
+ }
717
+ function readManagedEntries(targetDir) {
718
+ const lockfilePath = join7(targetDir, ".pro-gov/assets.lock.json");
719
+ if (!existsSync7(lockfilePath)) return [];
720
+ try {
721
+ const lockfile = JSON.parse(readFileSync4(lockfilePath, "utf8"));
722
+ return (lockfile.assets ?? []).filter(
723
+ (entry) => typeof entry.id === "string" && typeof entry.sourcePath === "string" && typeof entry.targetPath === "string"
724
+ );
725
+ } catch {
726
+ return [];
727
+ }
728
+ }
729
+ function createRemovalActions(targetDir, agentAssetsDir, managedEntries, expectedTargetPaths) {
730
+ const actions = [];
731
+ for (const entry of managedEntries) {
732
+ if (expectedTargetPaths.has(entry.targetPath)) continue;
733
+ if (!isManagedAssetTargetPath(entry.targetPath)) {
734
+ throw new Error(`Refusing to remove managed asset outside supported roots: ${entry.targetPath}`);
735
+ }
736
+ const targetAbsolutePath = join7(targetDir, entry.targetPath);
737
+ if (!pathExistsEvenIfDanglingSymlink2(targetAbsolutePath)) continue;
738
+ const stats = lstatSync2(targetAbsolutePath);
739
+ if (!stats.isSymbolicLink()) {
740
+ throw new Error(`Refusing to remove path that is no longer a managed symlink: ${entry.targetPath}`);
741
+ }
742
+ const expectedSourcePath = join7(agentAssetsDir, entry.sourcePath);
743
+ const actualSourcePath = resolve(dirname3(targetAbsolutePath), readlinkSync(targetAbsolutePath));
744
+ if (actualSourcePath !== resolve(expectedSourcePath)) {
745
+ throw new Error(`Refusing to remove managed symlink with changed target: ${entry.targetPath}`);
746
+ }
747
+ actions.push({
748
+ type: "remove-symlink",
749
+ assetId: entry.id,
750
+ expectedSourcePath,
751
+ targetPath: entry.targetPath
752
+ });
753
+ }
754
+ return actions.sort((a, b) => a.targetPath.localeCompare(b.targetPath));
755
+ }
756
+ function isManagedAssetTargetPath(path) {
757
+ return [
758
+ ".agents/skills/",
759
+ ".agents/manual-skills/",
760
+ ".claude/skills/",
761
+ ".pro-gov/agent-assets/rules/",
762
+ ".pro-gov/agent-assets/commands/"
763
+ ].some((prefix) => path.startsWith(prefix));
764
+ }
765
+ function pathExistsEvenIfDanglingSymlink2(path) {
766
+ try {
767
+ lstatSync2(path);
768
+ return true;
769
+ } catch {
770
+ return false;
771
+ }
772
+ }
773
+
774
+ // src/asset-targets/apply.ts
541
775
  function applyAssetInstallPlan(plan) {
542
776
  const appliedActions = [];
543
777
  for (const action of plan.actions) {
@@ -547,52 +781,71 @@ function applyAssetInstallPlan(plan) {
547
781
  return { appliedActions };
548
782
  }
549
783
  function applyAction(targetDir, action) {
550
- const targetAbsolutePath = join7(targetDir, action.targetPath);
784
+ const targetAbsolutePath = join8(targetDir, action.targetPath);
785
+ if (action.type === "remove-symlink") {
786
+ removeManagedSymlink(targetAbsolutePath, action);
787
+ return;
788
+ }
551
789
  if (action.type === "create-dir") {
552
790
  mkdirSync2(targetAbsolutePath, { recursive: true });
553
791
  return;
554
792
  }
555
793
  if (action.type === "write-file") {
556
- mkdirSync2(dirname3(targetAbsolutePath), { recursive: true });
794
+ mkdirSync2(dirname4(targetAbsolutePath), { recursive: true });
557
795
  writeFileSync(targetAbsolutePath, action.content);
558
796
  return;
559
797
  }
560
- mkdirSync2(dirname3(targetAbsolutePath), { recursive: true });
561
- const sourceAbsolutePath = resolve(action.sourcePath);
562
- const symlinkTarget = relative4(realpathSync(dirname3(targetAbsolutePath)), realpathSync(sourceAbsolutePath)) || ".";
798
+ mkdirSync2(dirname4(targetAbsolutePath), { recursive: true });
799
+ const sourceAbsolutePath = resolve2(action.sourcePath);
800
+ const symlinkTarget = relative4(realpathSync(dirname4(targetAbsolutePath)), realpathSync(sourceAbsolutePath)) || ".";
563
801
  if (action.type === "symlink") {
564
- if (pathExistsEvenIfDanglingSymlink2(targetAbsolutePath)) {
802
+ if (pathExistsEvenIfDanglingSymlink3(targetAbsolutePath)) {
565
803
  throw new Error(`Refusing to overwrite unmanaged target: ${action.targetPath}`);
566
804
  }
567
805
  symlinkSync(symlinkTarget, targetAbsolutePath);
568
806
  return;
569
807
  }
570
- if (!pathExistsEvenIfDanglingSymlink2(targetAbsolutePath)) {
808
+ if (!pathExistsEvenIfDanglingSymlink3(targetAbsolutePath)) {
571
809
  symlinkSync(symlinkTarget, targetAbsolutePath);
572
810
  return;
573
811
  }
574
- const stats = lstatSync2(targetAbsolutePath);
812
+ const stats = lstatSync3(targetAbsolutePath);
575
813
  if (!stats.isSymbolicLink()) {
576
814
  throw new Error(`Refusing to overwrite unmanaged target: ${action.targetPath}`);
577
815
  }
578
816
  unlinkSync(targetAbsolutePath);
579
817
  symlinkSync(symlinkTarget, targetAbsolutePath);
580
818
  }
581
- function pathExistsEvenIfDanglingSymlink2(path) {
819
+ function removeManagedSymlink(targetAbsolutePath, action) {
820
+ if (!isManagedAssetTargetPath(action.targetPath)) {
821
+ throw new Error(`Refusing to remove managed asset outside supported roots: ${action.targetPath}`);
822
+ }
823
+ if (!pathExistsEvenIfDanglingSymlink3(targetAbsolutePath)) return;
824
+ const stats = lstatSync3(targetAbsolutePath);
825
+ if (!stats.isSymbolicLink()) {
826
+ throw new Error(`Refusing to remove path that is no longer a managed symlink: ${action.targetPath}`);
827
+ }
828
+ const actualSourcePath = resolve2(dirname4(targetAbsolutePath), readlinkSync2(targetAbsolutePath));
829
+ if (actualSourcePath !== resolve2(action.expectedSourcePath)) {
830
+ throw new Error(`Refusing to remove managed symlink with changed target: ${action.targetPath}`);
831
+ }
832
+ unlinkSync(targetAbsolutePath);
833
+ }
834
+ function pathExistsEvenIfDanglingSymlink3(path) {
582
835
  try {
583
- lstatSync2(path);
836
+ lstatSync3(path);
584
837
  return true;
585
838
  } catch {
586
- return existsSync7(path);
839
+ return existsSync8(path);
587
840
  }
588
841
  }
589
842
 
590
843
  // src/asset-targets/check.ts
591
- import { existsSync as existsSync8, lstatSync as lstatSync3, readFileSync as readFileSync4 } from "node:fs";
592
- import { basename, join as join8 } from "node:path";
844
+ import { existsSync as existsSync9, lstatSync as lstatSync4, readFileSync as readFileSync5 } from "node:fs";
845
+ import { basename as basename2, join as join9 } from "node:path";
593
846
  function checkInstalledAssets(options) {
594
- const lockfilePath = join8(options.targetDir, ".pro-gov/assets.lock.json");
595
- if (!existsSync8(lockfilePath)) {
847
+ const lockfilePath = join9(options.targetDir, ".pro-gov/assets.lock.json");
848
+ if (!existsSync9(lockfilePath)) {
596
849
  return {
597
850
  targetDir: options.targetDir,
598
851
  issues: [
@@ -604,12 +857,12 @@ function checkInstalledAssets(options) {
604
857
  };
605
858
  }
606
859
  const registryById = new Map(options.registry.assets.map((asset) => [asset.id, asset]));
607
- const lockfile = JSON.parse(readFileSync4(lockfilePath, "utf8"));
860
+ const lockfile = JSON.parse(readFileSync5(lockfilePath, "utf8"));
608
861
  const issues = [];
609
862
  const strictRegistry = options.strictRegistry ?? false;
610
863
  for (const entry of lockfile.assets ?? []) {
611
864
  const asset = registryById.get(entry.id);
612
- const targetAbsolutePath = join8(options.targetDir, entry.targetPath);
865
+ const targetAbsolutePath = join9(options.targetDir, entry.targetPath);
613
866
  if (!asset && strictRegistry) {
614
867
  issues.push({
615
868
  type: "unknown-asset",
@@ -636,7 +889,7 @@ function checkInstalledAssets(options) {
636
889
  issues.push(placementDriftIssue);
637
890
  }
638
891
  }
639
- if (!pathExistsEvenIfDanglingSymlink3(targetAbsolutePath)) {
892
+ if (!pathExistsEvenIfDanglingSymlink4(targetAbsolutePath)) {
640
893
  issues.push({
641
894
  type: "missing-target",
642
895
  id: entry.id,
@@ -645,7 +898,7 @@ function checkInstalledAssets(options) {
645
898
  });
646
899
  continue;
647
900
  }
648
- const targetStats = lstatSync3(targetAbsolutePath);
901
+ const targetStats = lstatSync4(targetAbsolutePath);
649
902
  if (!targetStats.isSymbolicLink()) {
650
903
  issues.push({
651
904
  type: "unmanaged-conflict",
@@ -655,7 +908,7 @@ function checkInstalledAssets(options) {
655
908
  });
656
909
  continue;
657
910
  }
658
- if (!existsSync8(targetAbsolutePath)) {
911
+ if (!existsSync9(targetAbsolutePath)) {
659
912
  issues.push({
660
913
  type: "dangling-symlink",
661
914
  id: entry.id,
@@ -675,8 +928,8 @@ function checkInstalledAssets(options) {
675
928
  });
676
929
  }
677
930
  if (!asset || !strictRegistry) continue;
678
- const sourceAbsolutePath = join8(options.agentAssetsDir, asset.sourcePath);
679
- if (!existsSync8(sourceAbsolutePath)) {
931
+ const sourceAbsolutePath = join9(options.agentAssetsDir, asset.sourcePath);
932
+ if (!existsSync9(sourceAbsolutePath)) {
680
933
  issues.push({
681
934
  type: "missing-source",
682
935
  id: entry.id,
@@ -714,10 +967,10 @@ function checkDuplicateSkillPlacements(targetDir, registry) {
714
967
  const issues = [];
715
968
  for (const asset of registry.assets) {
716
969
  if (asset.kind !== "skill") continue;
717
- const skillName = basename(asset.sourcePath);
970
+ const skillName = basename2(asset.sourcePath);
718
971
  const autoPath = `.agents/skills/${skillName}`;
719
972
  const manualPath = `.agents/manual-skills/${skillName}`;
720
- if (pathExistsEvenIfDanglingSymlink3(join8(targetDir, autoPath)) && pathExistsEvenIfDanglingSymlink3(join8(targetDir, manualPath))) {
973
+ if (pathExistsEvenIfDanglingSymlink4(join9(targetDir, autoPath)) && pathExistsEvenIfDanglingSymlink4(join9(targetDir, manualPath))) {
721
974
  issues.push({
722
975
  type: "duplicate-skill-placement",
723
976
  id: asset.id,
@@ -736,192 +989,35 @@ function checkHostFolder(host, kind, targetPath, id) {
736
989
  type: "unsupported-host-folder",
737
990
  id,
738
991
  targetPath,
739
- message: `Lockfile host is unsupported for managed skill target: ${host ?? "missing"}`
740
- };
741
- }
742
- if (!expectedPrefixes.some((prefix) => targetPath.startsWith(prefix))) {
743
- return {
744
- type: "unsupported-host-folder",
745
- id,
746
- targetPath,
747
- message: `Managed skill target ${targetPath} does not match host ${host}; expected ${expectedPrefixes.join(" or ")}`
748
- };
749
- }
750
- return void 0;
751
- }
752
- function expectedSkillTargetPrefixes(host) {
753
- if (host === "claude-code") return [".claude/skills/"];
754
- if (host === "codex" || host === "gemini-cli" || host === "antigravity") {
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}`;
766
- }
767
- return void 0;
768
- }
769
- function pathExistsEvenIfDanglingSymlink3(path) {
770
- try {
771
- lstatSync3(path);
772
- return true;
773
- } catch {
774
- return false;
775
- }
776
- }
777
-
778
- // src/asset-targets/install-plan.ts
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";
781
- function createAssetInstallPlan(options) {
782
- const placement = options.placement ?? "registry";
783
- const assetsById = new Map(options.registry.assets.map((asset) => [asset.id, asset]));
784
- const bundlesById = new Map(options.bundles.map((bundle) => [bundle.id, bundle]));
785
- const assetIds = resolveBundleAssetIds(options.bundleIds, bundlesById);
786
- const assets = assetIds.map((assetId) => {
787
- const asset = assetsById.get(assetId);
788
- if (!asset) throw new Error(`Unknown asset id in bundle: ${assetId}`);
789
- if (asset.kind === "skill" && !asset.hosts.includes(options.host)) {
790
- throw new Error(`Asset ${assetId} does not support host ${options.host}`);
791
- }
792
- return asset;
793
- });
794
- const lockEntries = createAgentAssetLockEntries(options.registry, options.agentAssetsDir, assetIds);
795
- const managedTargets = readManagedTargets(options.targetDir);
796
- const assetActions = assets.map(
797
- (asset) => createAssetAction(asset, options.agentAssetsDir, options.targetDir, options.host, placement, managedTargets)
798
- );
799
- const manifest = {
800
- schemaVersion: 1,
801
- host: options.host,
802
- placement,
803
- bundleIds: [...options.bundleIds],
804
- assetIds
805
- };
806
- const lockfile = {
807
- schemaVersion: 1,
808
- host: options.host,
809
- placement,
810
- bundleIds: [...options.bundleIds],
811
- assets: lockEntries.map((entry) => {
812
- const action = assetActions.find((candidate) => "assetId" in candidate && candidate.assetId === entry.id);
813
- return {
814
- ...entry,
815
- targetPath: action && "targetPath" in action ? action.targetPath : ""
816
- };
817
- })
818
- };
819
- const writeActions = [
820
- {
821
- type: "write-file",
822
- targetPath: ".pro-gov/assets.json",
823
- content: `${JSON.stringify(manifest, null, 2)}
824
- `
825
- },
826
- {
827
- type: "write-file",
828
- targetPath: ".pro-gov/assets.lock.json",
829
- content: `${JSON.stringify(lockfile, null, 2)}
830
- `
831
- }
832
- ];
833
- return {
834
- schemaVersion: 1,
835
- dryRun: true,
836
- targetDir: options.targetDir,
837
- host: options.host,
838
- placement,
839
- bundleIds: [...options.bundleIds],
840
- assetIds,
841
- actions: [...createDirectoryActions([...assetActions, ...writeActions]), ...assetActions, ...writeActions]
842
- };
843
- }
844
- function resolveBundleAssetIds(bundleIds, bundlesById) {
845
- const ids = /* @__PURE__ */ new Set();
846
- for (const bundleId of bundleIds) {
847
- const bundle = bundlesById.get(bundleId);
848
- if (!bundle) throw new Error(`Unknown bundle id: ${bundleId}`);
849
- for (const assetId of bundle.assets) {
850
- ids.add(assetId);
851
- }
852
- }
853
- return [...ids].sort();
854
- }
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);
864
- const targetExists = pathExistsEvenIfDanglingSymlink4(targetAbsolutePath);
865
- if (targetExists) {
866
- const stats = lstatSync4(targetAbsolutePath);
867
- if (stats.isSymbolicLink() && managedTargets.has(targetPath)) {
868
- return {
869
- type: "update-symlink",
870
- assetId: asset.id,
871
- sourcePath,
872
- targetPath
873
- };
874
- }
875
- throw new Error(`Refusing to overwrite unmanaged target: ${targetPath}`);
876
- }
877
- return {
878
- type: "symlink",
879
- assetId: asset.id,
880
- sourcePath,
881
- targetPath
882
- };
883
- }
884
- function resolveHostTargetPath(asset, host, placement) {
885
- if (asset.kind === "skill") {
886
- const effectivePlacement = resolveSkillPlacement(asset, placement);
887
- if (host === "claude-code") {
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)}`;
895
- }
896
- return `.agents/skills/${basename2(asset.sourcePath)}`;
992
+ message: `Lockfile host is unsupported for managed skill target: ${host ?? "missing"}`
993
+ };
897
994
  }
898
- if (asset.kind === "rule") {
899
- return `.pro-gov/agent-assets/rules/${basename2(asset.sourcePath)}`;
995
+ if (!expectedPrefixes.some((prefix) => targetPath.startsWith(prefix))) {
996
+ return {
997
+ type: "unsupported-host-folder",
998
+ id,
999
+ targetPath,
1000
+ message: `Managed skill target ${targetPath} does not match host ${host}; expected ${expectedPrefixes.join(" or ")}`
1001
+ };
900
1002
  }
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";
1003
+ return void 0;
906
1004
  }
907
- function createDirectoryActions(actions) {
908
- const directories = /* @__PURE__ */ new Set();
909
- for (const action of actions) {
910
- if (action.type === "create-dir") continue;
911
- const directory = dirname4(action.targetPath);
912
- if (directory !== ".") directories.add(directory);
1005
+ function expectedSkillTargetPrefixes(host) {
1006
+ if (host === "claude-code") return [".claude/skills/"];
1007
+ if (host === "codex" || host === "gemini-cli" || host === "antigravity") {
1008
+ return [".agents/skills/", ".agents/manual-skills/"];
913
1009
  }
914
- return [...directories].sort().map((targetPath) => ({ type: "create-dir", targetPath }));
1010
+ return void 0;
915
1011
  }
916
- function readManagedTargets(targetDir) {
917
- const lockfilePath = join9(targetDir, ".pro-gov/assets.lock.json");
918
- if (!existsSync9(lockfilePath)) return /* @__PURE__ */ new Set();
919
- try {
920
- const lockfile = JSON.parse(readFileSync5(lockfilePath, "utf8"));
921
- return new Set((lockfile.assets ?? []).map((asset) => asset.targetPath).filter(Boolean));
922
- } catch {
923
- return /* @__PURE__ */ new Set();
1012
+ function expectedRegistrySkillTargetPath(host, sourcePath, placement) {
1013
+ const skillName = basename2(sourcePath);
1014
+ if (host === "claude-code") {
1015
+ return placement === "manual" ? void 0 : `.claude/skills/${skillName}`;
1016
+ }
1017
+ if (host === "codex" || host === "gemini-cli" || host === "antigravity") {
1018
+ return placement === "manual" ? `.agents/manual-skills/${skillName}` : `.agents/skills/${skillName}`;
924
1019
  }
1020
+ return void 0;
925
1021
  }
926
1022
  function pathExistsEvenIfDanglingSymlink4(path) {
927
1023
  try {
@@ -1628,6 +1724,9 @@ function checkStrictHostHooks(root) {
1628
1724
  if (!content.includes("pro-gov host-hook") || !content.includes(`--host ${entry.host}`)) {
1629
1725
  issues.push(`host-hooks: ${entry.path} does not call pro-gov host-hook for ${entry.host}`);
1630
1726
  }
1727
+ if (entry.host === "antigravity" && !content.includes("PGS_HOST_HOOK_DEBUG=1")) {
1728
+ issues.push(`host-hooks: ${entry.path} does not enable PGS_HOST_HOOK_DEBUG=1 for Antigravity diagnostics`);
1729
+ }
1631
1730
  }
1632
1731
  return issues;
1633
1732
  }
@@ -1636,7 +1735,10 @@ function isEngineeringRuntimeProject(root) {
1636
1735
  }
1637
1736
 
1638
1737
  // src/commands/host-hook.ts
1639
- import { readFileSync as readFileSync10 } from "node:fs";
1738
+ import { spawnSync as spawnSync3 } from "node:child_process";
1739
+ import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
1740
+ import { tmpdir as tmpdir2 } from "node:os";
1741
+ import { basename as basename3, join as join13, resolve as resolve3 } from "node:path";
1640
1742
 
1641
1743
  // src/host-hooks/host-hook-runner.ts
1642
1744
  import { closeSync, openSync, readFileSync as readFileSync9, readSync, statSync as statSync3 } from "node:fs";
@@ -1645,7 +1747,11 @@ var completionSignalPatterns = [
1645
1747
  /\b(done|completed|implemented|fixed|verified|validated|shipped|pushed|committed)\b/i,
1646
1748
  /\b(tests?|typecheck|build|lint|doctor|pack)\b.*\b(pass|passed|green|succeed|succeeded|ok)\b/i,
1647
1749
  /\b(changed|updated|modified|created|deleted|refactored)\b.*\b(files?|docs?|tests?|hooks?|configs?)\b/i,
1648
- /完成|已完成|修好了|实现了|验证通过|测试通过|已经提交|已经推送|提交并推送/
1750
+ /已完成|完成了|修好了|实现了|验证通过|测试通过|已经提交|已经推送|提交并推送|已提交|已推送/
1751
+ ];
1752
+ var negativeCompletionSignalPatterns = [
1753
+ /\b(not done|not completed|not implemented|not fixed|did not complete|didn't complete|have not completed|haven't completed)\b/i,
1754
+ /没有完成|未完成|还没完成|没有改动|未改动|没有修改|未修改/
1649
1755
  ];
1650
1756
  var compoundGateInstruction = [
1651
1757
  "Before final reporting, pass the PGS Compound Gate.",
@@ -1730,6 +1836,9 @@ function normalizeStopInput(input) {
1730
1836
  };
1731
1837
  }
1732
1838
  function looksLikeCompletedEngineeringWork(message) {
1839
+ if (negativeCompletionSignalPatterns.some((pattern) => pattern.test(message))) {
1840
+ return false;
1841
+ }
1733
1842
  return completionSignalPatterns.some((pattern) => pattern.test(message));
1734
1843
  }
1735
1844
  function findString(input, keys) {
@@ -1843,7 +1952,9 @@ function isHostHookEvent(value) {
1843
1952
  }
1844
1953
 
1845
1954
  // src/commands/host-hook.ts
1846
- function runHostHook(args) {
1955
+ var defaultStdinTimeoutMs = 750;
1956
+ var maxDebugRawInputBytes = 256 * 1024;
1957
+ async function runHostHook(args) {
1847
1958
  const host = readOption(args, "--host");
1848
1959
  const event = readOption(args, "--event");
1849
1960
  if (!isHostHookHost(host)) {
@@ -1854,9 +1965,18 @@ function runHostHook(args) {
1854
1965
  console.error("Expected --event <Stop|SubagentStop|PreToolUse|PostToolUse|UserPromptSubmit>");
1855
1966
  return 1;
1856
1967
  }
1857
- const input = readStdinJson();
1968
+ const rawInput = await readStdinText(defaultStdinTimeoutMs);
1969
+ const input = parseStdinJson(rawInput);
1858
1970
  const decision = evaluateHostHook({ host, event, input });
1859
1971
  const output = formatHostHookOutput(host, event, decision);
1972
+ writeDebugLogIfRequested(args, {
1973
+ decision,
1974
+ event,
1975
+ host,
1976
+ input,
1977
+ output,
1978
+ rawInput
1979
+ });
1860
1980
  console.log(`${JSON.stringify(output)}
1861
1981
  `);
1862
1982
  return 0;
@@ -1866,8 +1986,7 @@ function readOption(args, name) {
1866
1986
  if (index < 0) return void 0;
1867
1987
  return args[index + 1];
1868
1988
  }
1869
- function readStdinJson() {
1870
- const raw = readFileSync10(0, "utf8").trim();
1989
+ function parseStdinJson(raw) {
1871
1990
  if (!raw) return {};
1872
1991
  try {
1873
1992
  return JSON.parse(raw);
@@ -1875,10 +1994,106 @@ function readStdinJson() {
1875
1994
  return {};
1876
1995
  }
1877
1996
  }
1997
+ function readStdinText(timeoutMs) {
1998
+ if (process.stdin.isTTY) {
1999
+ return Promise.resolve("");
2000
+ }
2001
+ process.stdin.setEncoding("utf8");
2002
+ return new Promise((resolveText) => {
2003
+ let settled = false;
2004
+ let content = "";
2005
+ const settle = () => {
2006
+ if (settled) return;
2007
+ settled = true;
2008
+ clearTimeout(timer);
2009
+ process.stdin.off("data", onData);
2010
+ process.stdin.off("end", settle);
2011
+ process.stdin.off("error", settle);
2012
+ process.stdin.pause();
2013
+ resolveText(content.trim());
2014
+ };
2015
+ const onData = (chunk) => {
2016
+ content += chunk.toString();
2017
+ };
2018
+ const timer = setTimeout(settle, timeoutMs);
2019
+ timer.unref();
2020
+ process.stdin.on("data", onData);
2021
+ process.stdin.on("end", settle);
2022
+ process.stdin.on("error", settle);
2023
+ process.stdin.resume();
2024
+ });
2025
+ }
2026
+ function writeDebugLogIfRequested(args, record) {
2027
+ const explicitPath = readOption(args, "--debug-log");
2028
+ const enabled = explicitPath !== void 0 || process.env.PGS_HOST_HOOK_DEBUG === "1";
2029
+ if (!enabled) return;
2030
+ const debugDir = explicitPath && explicitPath.trim().length > 0 ? explicitPath : defaultDebugDir();
2031
+ try {
2032
+ mkdirSync4(debugDir, { recursive: true });
2033
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2034
+ const fileName = `${timestamp}-${process.pid}-${record.host}-${record.event}.json`;
2035
+ writeFileSync3(
2036
+ join13(debugDir, fileName),
2037
+ `${JSON.stringify(
2038
+ {
2039
+ schemaVersion: 1,
2040
+ cwd: process.cwd(),
2041
+ event: record.event,
2042
+ host: record.host,
2043
+ nodeVersion: process.version,
2044
+ packageVersion: readPackageVersion(),
2045
+ rawInput: truncateDebugRawInput(record.rawInput),
2046
+ input: record.input,
2047
+ decision: record.decision,
2048
+ output: record.output
2049
+ },
2050
+ null,
2051
+ 2
2052
+ )}
2053
+ `,
2054
+ "utf8"
2055
+ );
2056
+ } catch {
2057
+ }
2058
+ }
2059
+ function defaultDebugDir() {
2060
+ const gitPath = spawnSync3("git", ["rev-parse", "--git-path", "pro-gov-hook-debug"], {
2061
+ encoding: "utf8",
2062
+ stdio: ["ignore", "pipe", "ignore"]
2063
+ });
2064
+ const value = gitPath.status === 0 ? gitPath.stdout.trim() : "";
2065
+ if (value) {
2066
+ return resolve3(process.cwd(), value);
2067
+ }
2068
+ return join13(tmpdir2(), "pro-gov-hook-debug", basename3(process.cwd()));
2069
+ }
2070
+ function readPackageVersion() {
2071
+ const packageJsonPath = resolvePackageJsonPath();
2072
+ if (!packageJsonPath) return void 0;
2073
+ try {
2074
+ const parsed = JSON.parse(readFileSync10(packageJsonPath, "utf8"));
2075
+ return typeof parsed.version === "string" ? parsed.version : void 0;
2076
+ } catch {
2077
+ return void 0;
2078
+ }
2079
+ }
2080
+ function resolvePackageJsonPath() {
2081
+ const candidates = [
2082
+ resolve3(process.cwd(), "packages/pro-gov/package.json"),
2083
+ resolve3(process.cwd(), "node_modules/@pieai/pro-gov/package.json")
2084
+ ];
2085
+ return candidates.find((candidate) => existsSync13(candidate));
2086
+ }
2087
+ function truncateDebugRawInput(value) {
2088
+ const buffer = Buffer.from(value);
2089
+ if (buffer.byteLength <= maxDebugRawInputBytes) return value;
2090
+ return `${buffer.subarray(0, maxDebugRawInputBytes).toString("utf8")}
2091
+ [truncated]`;
2092
+ }
1878
2093
 
1879
2094
  // src/commands/init.ts
1880
- import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync11, writeFileSync as writeFileSync3 } from "node:fs";
1881
- import { basename as basename3, dirname as dirname7, join as join13 } from "node:path";
2095
+ import { existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "node:fs";
2096
+ import { basename as basename4, dirname as dirname7, join as join14 } from "node:path";
1882
2097
 
1883
2098
  // src/commands/shared.ts
1884
2099
  function planStarterFiles(profile) {
@@ -1950,7 +2165,7 @@ function runInit(args) {
1950
2165
  }
1951
2166
  function applyStarterFiles(files, profile) {
1952
2167
  const root = process.cwd();
1953
- const conflicts = files.filter((file) => existsSync13(join13(root, file.targetPath)));
2168
+ const conflicts = files.filter((file) => existsSync14(join14(root, file.targetPath)));
1954
2169
  if (conflicts.length > 0) {
1955
2170
  console.error("pro-gov init is refusing to overwrite existing project files:");
1956
2171
  for (const file of conflicts) console.error(` ${file.targetPath}`);
@@ -1958,11 +2173,11 @@ function applyStarterFiles(files, profile) {
1958
2173
  return 1;
1959
2174
  }
1960
2175
  for (const file of files) {
1961
- const targetPath = join13(root, file.targetPath);
1962
- mkdirSync4(dirname7(targetPath), { recursive: true });
2176
+ const targetPath = join14(root, file.targetPath);
2177
+ mkdirSync5(dirname7(targetPath), { recursive: true });
1963
2178
  const source = readFileSync11(file.absoluteSourcePath);
1964
- const content = file.targetPath === "AGENTS.md" ? renderAgentsTemplate(source.toString("utf8"), basename3(root), profile) : source;
1965
- writeFileSync3(targetPath, content);
2179
+ const content = file.targetPath === "AGENTS.md" ? renderAgentsTemplate(source.toString("utf8"), basename4(root), profile) : source;
2180
+ writeFileSync4(targetPath, content);
1966
2181
  }
1967
2182
  console.log("pro-gov init APPLIED");
1968
2183
  console.log(`profile: ${profile}`);
@@ -1990,12 +2205,12 @@ function readFlag(args, flag) {
1990
2205
  }
1991
2206
 
1992
2207
  // src/commands/lens.ts
1993
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
2208
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "node:fs";
1994
2209
  import { dirname as dirname9 } from "node:path";
1995
2210
 
1996
2211
  // src/lens/audit.ts
1997
- import { existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "node:fs";
1998
- import { basename as basename4, dirname as dirname8, join as join14 } from "node:path";
2212
+ import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "node:fs";
2213
+ import { basename as basename5, dirname as dirname8, join as join15 } from "node:path";
1999
2214
  var REQUIRED_ARTIFACTS = [
2000
2215
  "manifest.md",
2001
2216
  "raw/project-lens/architecture-lens.md",
@@ -2015,20 +2230,20 @@ function createProjectLensAuditPackage(targetDir, auditDir) {
2015
2230
  version: 1,
2016
2231
  target: {
2017
2232
  path: targetDir,
2018
- name: basename4(targetDir) || "target"
2233
+ name: basename5(targetDir) || "target"
2019
2234
  },
2020
2235
  requiredArtifacts: [...REQUIRED_ARTIFACTS]
2021
2236
  };
2022
- mkdirSync5(auditDir, { recursive: true });
2023
- writeJson(join14(auditDir, "audit.contract.json"), contract);
2237
+ mkdirSync6(auditDir, { recursive: true });
2238
+ writeJson(join15(auditDir, "audit.contract.json"), contract);
2024
2239
  for (const artifactPath of REQUIRED_ARTIFACTS) {
2025
- writeTemplate(join14(auditDir, artifactPath), renderArtifactTemplate(artifactPath, contract));
2240
+ writeTemplate(join15(auditDir, artifactPath), renderArtifactTemplate(artifactPath, contract));
2026
2241
  }
2027
2242
  return contract;
2028
2243
  }
2029
2244
  function checkProjectLensAuditPackage(auditDir, options = {}) {
2030
- const contractPath = join14(auditDir, "audit.contract.json");
2031
- if (!existsSync14(contractPath)) {
2245
+ const contractPath = join15(auditDir, "audit.contract.json");
2246
+ if (!existsSync15(contractPath)) {
2032
2247
  return {
2033
2248
  ok: false,
2034
2249
  auditDir,
@@ -2075,8 +2290,8 @@ function checkProjectLensAuditPackage(auditDir, options = {}) {
2075
2290
  }
2076
2291
  }
2077
2292
  for (const artifactPath of REQUIRED_ARTIFACTS) {
2078
- const absolutePath = join14(auditDir, artifactPath);
2079
- if (!existsSync14(absolutePath)) {
2293
+ const absolutePath = join15(auditDir, artifactPath);
2294
+ if (!existsSync15(absolutePath)) {
2080
2295
  issues.push({ type: "missing-required-artifact", path: artifactPath });
2081
2296
  continue;
2082
2297
  }
@@ -2096,13 +2311,13 @@ function checkProjectLensAuditPackage(auditDir, options = {}) {
2096
2311
  };
2097
2312
  }
2098
2313
  function writeJson(path, value) {
2099
- mkdirSync5(dirname8(path), { recursive: true });
2100
- writeFileSync4(path, `${JSON.stringify(value, null, 2)}
2314
+ mkdirSync6(dirname8(path), { recursive: true });
2315
+ writeFileSync5(path, `${JSON.stringify(value, null, 2)}
2101
2316
  `);
2102
2317
  }
2103
2318
  function writeTemplate(path, content) {
2104
- mkdirSync5(dirname8(path), { recursive: true });
2105
- writeFileSync4(path, content);
2319
+ mkdirSync6(dirname8(path), { recursive: true });
2320
+ writeFileSync5(path, content);
2106
2321
  }
2107
2322
  function renderArtifactTemplate(artifactPath, contract) {
2108
2323
  const title = artifactPath.replace(/\.md$/, "").split("/").map((part) => part.replaceAll("-", " ")).join(" / ");
@@ -2339,9 +2554,9 @@ function bulletList(values) {
2339
2554
  }
2340
2555
 
2341
2556
  // src/lens/scan.ts
2342
- import { spawnSync as spawnSync3 } from "node:child_process";
2343
- import { existsSync as existsSync15, readdirSync as readdirSync6, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
2344
- import { join as join15, relative as relative5 } from "node:path";
2557
+ import { spawnSync as spawnSync4 } from "node:child_process";
2558
+ import { existsSync as existsSync16, readdirSync as readdirSync6, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
2559
+ import { join as join16, relative as relative5 } from "node:path";
2345
2560
  var ignoredDirectories = /* @__PURE__ */ new Set([
2346
2561
  ".git",
2347
2562
  ".next",
@@ -2358,22 +2573,22 @@ function scanProjectLensTarget(targetDir, options = {}) {
2358
2573
  return {
2359
2574
  targetDir,
2360
2575
  aiEntryFiles: ["AGENTS.md", "CLAUDE.md"].filter(
2361
- (file) => existsSync15(join15(targetDir, file))
2576
+ (file) => existsSync16(join16(targetDir, file))
2362
2577
  ),
2363
2578
  aiConfigFiles: [],
2364
2579
  packageJson,
2365
2580
  docs: {
2366
- hasDocsDirectory: existsSync15(join15(targetDir, "docs")),
2581
+ hasDocsDirectory: existsSync16(join16(targetDir, "docs")),
2367
2582
  markdownFileCount: markdownFiles.length,
2368
2583
  governanceFiles: markdownFiles.filter((file) => file.startsWith("docs/governance/") || file.startsWith("docs/policy/")).sort()
2369
2584
  },
2370
2585
  git: readGitState(targetDir),
2371
- largeFiles: files.map((file) => ({ path: file, bytes: statSync4(join15(targetDir, file)).size })).filter((file) => file.bytes >= largeFileBytes).sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path)).slice(0, 25)
2586
+ largeFiles: files.map((file) => ({ path: file, bytes: statSync4(join16(targetDir, file)).size })).filter((file) => file.bytes >= largeFileBytes).sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path)).slice(0, 25)
2372
2587
  };
2373
2588
  }
2374
2589
  function readPackageJson(targetDir) {
2375
- const packageJsonPath = join15(targetDir, "package.json");
2376
- if (!existsSync15(packageJsonPath)) return void 0;
2590
+ const packageJsonPath = join16(targetDir, "package.json");
2591
+ if (!existsSync16(packageJsonPath)) return void 0;
2377
2592
  try {
2378
2593
  const packageJson = JSON.parse(readFileSync13(packageJsonPath, "utf8"));
2379
2594
  return {
@@ -2398,7 +2613,7 @@ function readGitState(targetDir) {
2398
2613
  };
2399
2614
  }
2400
2615
  function runGit(targetDir, args) {
2401
- const result = spawnSync3("git", ["-C", targetDir, ...args], {
2616
+ const result = spawnSync4("git", ["-C", targetDir, ...args], {
2402
2617
  encoding: "utf8"
2403
2618
  });
2404
2619
  if (result.status !== 0) return { ok: false };
@@ -2410,13 +2625,13 @@ function listProjectFiles(targetDir) {
2410
2625
  return files.sort();
2411
2626
  }
2412
2627
  function collectFiles2(rootDir, currentDir, files) {
2413
- if (!existsSync15(currentDir)) return;
2628
+ if (!existsSync16(currentDir)) return;
2414
2629
  for (const entry of readdirSync6(currentDir, { withFileTypes: true })) {
2415
2630
  if (entry.isDirectory()) {
2416
2631
  if (ignoredDirectories.has(entry.name)) continue;
2417
- collectFiles2(rootDir, join15(currentDir, entry.name), files);
2632
+ collectFiles2(rootDir, join16(currentDir, entry.name), files);
2418
2633
  } else if (entry.isFile()) {
2419
- files.push(toUnixPath4(relative5(rootDir, join15(currentDir, entry.name))));
2634
+ files.push(toUnixPath4(relative5(rootDir, join16(currentDir, entry.name))));
2420
2635
  }
2421
2636
  }
2422
2637
  }
@@ -2468,8 +2683,8 @@ function runLensReport(args) {
2468
2683
  }
2469
2684
  const report = scanProjectLensTarget(options.value.targetDir);
2470
2685
  const markdown = renderProjectLensMarkdownReport(report);
2471
- mkdirSync6(dirname9(options.value.outPath), { recursive: true });
2472
- writeFileSync5(options.value.outPath, markdown);
2686
+ mkdirSync7(dirname9(options.value.outPath), { recursive: true });
2687
+ writeFileSync6(options.value.outPath, markdown);
2473
2688
  console.log(`report: ${options.value.outPath}`);
2474
2689
  return 0;
2475
2690
  }
@@ -2572,12 +2787,12 @@ function printUsage2() {
2572
2787
  }
2573
2788
 
2574
2789
  // src/commands/portfolio.ts
2575
- import { existsSync as existsSync17 } from "node:fs";
2576
- import { join as join16 } from "node:path";
2790
+ import { existsSync as existsSync20 } from "node:fs";
2791
+ import { join as join19 } from "node:path";
2577
2792
 
2578
2793
  // src/portfolio/manifest.ts
2579
- import { existsSync as existsSync16, readFileSync as readFileSync14 } from "node:fs";
2580
- import { dirname as dirname10, isAbsolute as isAbsolute3, resolve as resolve2 } from "node:path";
2794
+ import { existsSync as existsSync17, readFileSync as readFileSync14 } from "node:fs";
2795
+ import { dirname as dirname10, isAbsolute as isAbsolute3, resolve as resolve4 } from "node:path";
2581
2796
  function loadPortfolioManifest(configPath) {
2582
2797
  let parsed;
2583
2798
  try {
@@ -2593,7 +2808,7 @@ function loadPortfolioManifest(configPath) {
2593
2808
  ]
2594
2809
  };
2595
2810
  }
2596
- const normalized = resolveManifestPaths(parsed, dirname10(resolve2(configPath)));
2811
+ const normalized = resolveManifestPaths(parsed, dirname10(resolve4(configPath)));
2597
2812
  const issues = validatePortfolioManifest(normalized);
2598
2813
  return {
2599
2814
  configPath,
@@ -2607,7 +2822,7 @@ function resolveManifestPaths(value, configDir) {
2607
2822
  if (!isRecord2(endpoint) || typeof endpoint.path !== "string" || isAbsolute3(endpoint.path)) {
2608
2823
  return endpoint;
2609
2824
  }
2610
- return { ...endpoint, path: resolve2(configDir, endpoint.path) };
2825
+ return { ...endpoint, path: resolve4(configDir, endpoint.path) };
2611
2826
  };
2612
2827
  return {
2613
2828
  ...value,
@@ -2641,9 +2856,15 @@ function validatePortfolioManifest(value) {
2641
2856
  message: "Portfolio manifest portfolioId must be a non-empty string."
2642
2857
  });
2643
2858
  }
2644
- validateAllowedFields(value, "root", ["schemaVersion", "portfolioId", "controlPlane", "executionEngine", "targets"], issues);
2859
+ validateAllowedFields(
2860
+ value,
2861
+ "root",
2862
+ ["schemaVersion", "portfolioId", "controlPlane", "executionEngine", "hostTooling", "targets"],
2863
+ issues
2864
+ );
2645
2865
  validateEndpoint(value.controlPlane, "controlPlane", issues);
2646
2866
  validateEndpoint(value.executionEngine, "executionEngine", issues);
2867
+ validateHostTooling(value.hostTooling, issues);
2647
2868
  if (!Array.isArray(value.targets)) {
2648
2869
  issues.push({
2649
2870
  type: "invalid-field",
@@ -2735,7 +2956,7 @@ function validateEndpoint(value, field, issues) {
2735
2956
  });
2736
2957
  return;
2737
2958
  }
2738
- if (!existsSync16(value.path)) {
2959
+ if (!existsSync17(value.path)) {
2739
2960
  issues.push({
2740
2961
  type: "missing-path",
2741
2962
  id: typeof value.id === "string" ? value.id : void 0,
@@ -2755,19 +2976,462 @@ function validateOptionalStringArray(value, id, field, issues) {
2755
2976
  });
2756
2977
  }
2757
2978
  }
2979
+ function validateHostTooling(value, issues) {
2980
+ if (value === void 0) return;
2981
+ if (!Array.isArray(value)) {
2982
+ issues.push({
2983
+ type: "invalid-field",
2984
+ field: "hostTooling",
2985
+ message: "Portfolio hostTooling must be an array."
2986
+ });
2987
+ return;
2988
+ }
2989
+ const seenHosts = /* @__PURE__ */ new Set();
2990
+ for (const entry of value) {
2991
+ if (!isRecord2(entry)) {
2992
+ issues.push({
2993
+ type: "invalid-field",
2994
+ field: "hostTooling",
2995
+ message: "Portfolio hostTooling entry must be an object."
2996
+ });
2997
+ continue;
2998
+ }
2999
+ validateAllowedFields(entry, "hostTooling", ["host", "plugins"], issues);
3000
+ if (entry.host !== "codex" && entry.host !== "claude-code") {
3001
+ issues.push({
3002
+ type: "invalid-field",
3003
+ field: "hostTooling.host",
3004
+ message: "Portfolio hostTooling host must be codex or claude-code."
3005
+ });
3006
+ } else if (seenHosts.has(entry.host)) {
3007
+ issues.push({
3008
+ type: "invalid-field",
3009
+ field: "hostTooling",
3010
+ message: `Duplicate portfolio hostTooling entry: ${entry.host}`
3011
+ });
3012
+ } else {
3013
+ seenHosts.add(entry.host);
3014
+ }
3015
+ if (!Array.isArray(entry.plugins) || !entry.plugins.every((plugin) => typeof plugin === "string" && plugin.length > 0)) {
3016
+ issues.push({
3017
+ type: "invalid-field",
3018
+ field: "hostTooling.plugins",
3019
+ message: "Portfolio hostTooling plugins must be an array of non-empty strings."
3020
+ });
3021
+ }
3022
+ }
3023
+ }
2758
3024
  function isRecord2(value) {
2759
3025
  return typeof value === "object" && value !== null && !Array.isArray(value);
2760
3026
  }
2761
3027
 
3028
+ // src/portfolio/doctor.ts
3029
+ import { spawnSync as spawnSync6 } from "node:child_process";
3030
+ import { existsSync as existsSync19, readFileSync as readFileSync16 } from "node:fs";
3031
+ import { createRequire as createRequire2 } from "node:module";
3032
+ import { dirname as dirname11, join as join18 } from "node:path";
3033
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
3034
+
3035
+ // src/host-tooling/inventory.ts
3036
+ import { spawnSync as spawnSync5 } from "node:child_process";
3037
+ function inspectHostTooling(requirements, runner = defaultRunner2) {
3038
+ const hosts = [];
3039
+ const issues = [];
3040
+ for (const requirement of requirements) {
3041
+ const command2 = requirement.host === "codex" ? ["codex", "plugin", "list", "--json"] : ["claude", "plugin", "list", "--json"];
3042
+ const result = runner({ host: requirement.host, command: command2 });
3043
+ if (result.status !== 0) {
3044
+ issues.push({
3045
+ type: "host-tooling-command-failed",
3046
+ host: requirement.host,
3047
+ message: `Unable to inspect ${requirement.host} plugins: ${result.stderr || `exit ${result.status}`}`
3048
+ });
3049
+ hosts.push({ host: requirement.host, plugins: [] });
3050
+ continue;
3051
+ }
3052
+ let plugins;
3053
+ try {
3054
+ plugins = parseHostPlugins(requirement.host, JSON.parse(result.stdout));
3055
+ } catch (error) {
3056
+ issues.push({
3057
+ type: "host-tooling-command-failed",
3058
+ host: requirement.host,
3059
+ message: `Unable to parse ${requirement.host} plugin inventory: ${error instanceof Error ? error.message : String(error)}`
3060
+ });
3061
+ hosts.push({ host: requirement.host, plugins: [] });
3062
+ continue;
3063
+ }
3064
+ const byId = new Map(plugins.map((plugin) => [plugin.id, plugin]));
3065
+ hosts.push({
3066
+ host: requirement.host,
3067
+ plugins: requirement.plugins.flatMap((pluginId) => {
3068
+ const plugin = byId.get(pluginId);
3069
+ return plugin ? [plugin] : [];
3070
+ })
3071
+ });
3072
+ for (const pluginId of requirement.plugins) {
3073
+ const plugin = byId.get(pluginId);
3074
+ if (!plugin) {
3075
+ issues.push({
3076
+ type: "host-tooling-missing",
3077
+ host: requirement.host,
3078
+ pluginId,
3079
+ message: `Required ${requirement.host} plugin is missing: ${pluginId}`
3080
+ });
3081
+ } else if (!plugin.enabled) {
3082
+ issues.push({
3083
+ type: "host-tooling-disabled",
3084
+ host: requirement.host,
3085
+ pluginId,
3086
+ message: `Required ${requirement.host} plugin is disabled: ${pluginId}`
3087
+ });
3088
+ }
3089
+ }
3090
+ }
3091
+ return { hosts, issues };
3092
+ }
3093
+ function parseHostPlugins(host, value) {
3094
+ const entries = host === "codex" ? isRecord3(value) && Array.isArray(value.installed) ? value.installed : void 0 : Array.isArray(value) ? value : void 0;
3095
+ if (!entries) throw new Error("expected a plugin array");
3096
+ return entries.flatMap((entry) => {
3097
+ if (!isRecord3(entry)) return [];
3098
+ const id = host === "codex" ? entry.pluginId : entry.id;
3099
+ if (typeof id !== "string") return [];
3100
+ return [{
3101
+ id,
3102
+ version: typeof entry.version === "string" ? entry.version : void 0,
3103
+ enabled: entry.enabled === true
3104
+ }];
3105
+ });
3106
+ }
3107
+ function defaultRunner2({ command: command2 }) {
3108
+ const result = spawnSync5(command2[0] ?? "", command2.slice(1), {
3109
+ encoding: "utf8",
3110
+ timeout: 1e4
3111
+ });
3112
+ return {
3113
+ status: result.status,
3114
+ stdout: result.stdout,
3115
+ stderr: result.stderr || result.error?.message || ""
3116
+ };
3117
+ }
3118
+ function isRecord3(value) {
3119
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3120
+ }
3121
+
3122
+ // src/portfolio/asset-state.ts
3123
+ import { existsSync as existsSync18, lstatSync as lstatSync5, readFileSync as readFileSync15 } from "node:fs";
3124
+ import { join as join17 } from "node:path";
3125
+ function comparePortfolioAssetState(options) {
3126
+ const expectedManifest = readPlanDocument(options.expectedPlan, ".pro-gov/assets.json");
3127
+ const expectedLock = readPlanDocument(options.expectedPlan, ".pro-gov/assets.lock.json");
3128
+ const currentManifest = readJsonFile(join17(options.targetDir, ".pro-gov/assets.json"));
3129
+ const currentLock = readJsonFile(join17(options.targetDir, ".pro-gov/assets.lock.json"));
3130
+ const issues = [];
3131
+ if (!sameStrings(currentManifest?.bundleIds, expectedManifest?.bundleIds)) {
3132
+ issues.push({
3133
+ type: "bundle-drift",
3134
+ message: "Target asset bundles do not match the portfolio manifest."
3135
+ });
3136
+ }
3137
+ if (!sameStrings(currentManifest?.assetIds, expectedManifest?.assetIds)) {
3138
+ issues.push({
3139
+ type: "asset-set-drift",
3140
+ message: "Target asset ids do not match the current bundle definitions."
3141
+ });
3142
+ }
3143
+ if (!sameLock(currentLock, expectedLock)) {
3144
+ issues.push({
3145
+ type: "asset-lock-drift",
3146
+ message: "Target asset lock does not match the current registry, placement, and content hashes."
3147
+ });
3148
+ }
3149
+ const expectedTargets = new Set((expectedLock?.assets ?? []).map((entry) => entry.targetPath));
3150
+ for (const entry of currentLock?.assets ?? []) {
3151
+ if (expectedTargets.has(entry.targetPath)) continue;
3152
+ const targetAbsolutePath = join17(options.targetDir, entry.targetPath);
3153
+ if (!pathIsSymlink(targetAbsolutePath)) continue;
3154
+ issues.push({
3155
+ type: "orphaned-managed-symlink",
3156
+ targetPath: entry.targetPath,
3157
+ message: `Previously managed symlink is absent from the expected bundle state: ${entry.targetPath}`
3158
+ });
3159
+ }
3160
+ return { issues };
3161
+ }
3162
+ function readPlanDocument(plan, targetPath) {
3163
+ const action = plan.actions.find(
3164
+ (candidate) => candidate.type === "write-file" && candidate.targetPath === targetPath
3165
+ );
3166
+ if (!action || action.type !== "write-file") return void 0;
3167
+ try {
3168
+ return JSON.parse(action.content);
3169
+ } catch {
3170
+ return void 0;
3171
+ }
3172
+ }
3173
+ function readJsonFile(path) {
3174
+ if (!existsSync18(path)) return void 0;
3175
+ try {
3176
+ return JSON.parse(readFileSync15(path, "utf8"));
3177
+ } catch {
3178
+ return void 0;
3179
+ }
3180
+ }
3181
+ function sameStrings(left, right) {
3182
+ return JSON.stringify([...left ?? []].sort()) === JSON.stringify([...right ?? []].sort());
3183
+ }
3184
+ function sameLock(left, right) {
3185
+ return JSON.stringify(normalizeLock(left)) === JSON.stringify(normalizeLock(right));
3186
+ }
3187
+ function normalizeLock(lock) {
3188
+ return {
3189
+ host: lock?.host,
3190
+ placement: lock?.placement,
3191
+ bundleIds: [...lock?.bundleIds ?? []].sort(),
3192
+ assets: [...lock?.assets ?? []].sort((a, b) => a.id.localeCompare(b.id))
3193
+ };
3194
+ }
3195
+ function pathIsSymlink(path) {
3196
+ try {
3197
+ return lstatSync5(path).isSymbolicLink();
3198
+ } catch {
3199
+ return false;
3200
+ }
3201
+ }
3202
+
3203
+ // src/portfolio/doctor.ts
3204
+ function inspectPortfolio(options) {
3205
+ const expectedPackageVersions = getExpectedPackageVersions();
3206
+ const hostTooling = inspectHostTooling(options.manifest.hostTooling ?? []);
3207
+ const targets = options.targets.map((target) => inspectTarget({
3208
+ target,
3209
+ agentAssetsDir: options.agentAssetsDir,
3210
+ registry: options.registry,
3211
+ bundles: options.bundles,
3212
+ expectedPackageVersions
3213
+ }));
3214
+ return {
3215
+ ok: hostTooling.issues.length === 0 && targets.every((target) => target.issues.length === 0),
3216
+ portfolioId: options.manifest.portfolioId,
3217
+ expectedPackageVersions,
3218
+ hostTooling,
3219
+ targets
3220
+ };
3221
+ }
3222
+ function inspectTarget(options) {
3223
+ const { target } = options;
3224
+ const issues = [];
3225
+ const packageJson = readJson2(join18(target.path, "package.json"));
3226
+ const packages = {};
3227
+ for (const packageName of ["@pieai/pro-gov", "@pieai/doc-gov"]) {
3228
+ const declared = packageJson?.devDependencies?.[packageName] ?? packageJson?.dependencies?.[packageName];
3229
+ const installedPackage = readJson2(join18(target.path, "node_modules", packageName, "package.json"));
3230
+ const installed = installedPackage?.version;
3231
+ const expected = options.expectedPackageVersions[packageName];
3232
+ packages[packageName] = { declared, installed, expected };
3233
+ if (!declared) {
3234
+ issues.push({
3235
+ type: "package-declaration-missing",
3236
+ packageName,
3237
+ message: `Target does not declare ${packageName}.`
3238
+ });
3239
+ }
3240
+ if (!installed || expected && installed !== expected) {
3241
+ issues.push({
3242
+ type: "package-version-drift",
3243
+ packageName,
3244
+ message: `Target ${packageName} installed version is ${installed ?? "missing"}; expected ${expected ?? "unknown"}.`
3245
+ });
3246
+ }
3247
+ }
3248
+ const checks = runTargetChecks(target);
3249
+ for (const check of checks) {
3250
+ if (check.status === 0) continue;
3251
+ issues.push({
3252
+ type: "target-check-failed",
3253
+ check: check.name,
3254
+ message: `Target check failed: ${check.name} (${check.status ?? "unavailable"}).`
3255
+ });
3256
+ }
3257
+ const assetCheck = checkInstalledAssets({
3258
+ targetDir: target.path,
3259
+ agentAssetsDir: options.agentAssetsDir,
3260
+ registry: options.registry,
3261
+ strictRegistry: true
3262
+ });
3263
+ issues.push(...assetCheck.issues.map((issue) => ({
3264
+ type: issue.type,
3265
+ targetPath: issue.targetPath,
3266
+ message: issue.message
3267
+ })));
3268
+ try {
3269
+ const expectedPlan = createAssetInstallPlan({
3270
+ targetDir: target.path,
3271
+ agentAssetsDir: options.agentAssetsDir,
3272
+ registry: options.registry,
3273
+ bundles: options.bundles,
3274
+ bundleIds: target.assetBundles ?? [],
3275
+ host: "codex"
3276
+ });
3277
+ issues.push(...comparePortfolioAssetState({ targetDir: target.path, expectedPlan }).issues);
3278
+ } catch (error) {
3279
+ issues.push({
3280
+ type: "asset-lock-drift",
3281
+ message: error instanceof Error ? error.message : String(error)
3282
+ });
3283
+ if (!existsSync19(join18(target.path, ".pro-gov/assets.json"))) {
3284
+ issues.push({ type: "bundle-drift", message: "Target asset manifest is missing." });
3285
+ }
3286
+ }
3287
+ return {
3288
+ id: target.id,
3289
+ path: target.path,
3290
+ profile: target.profile,
3291
+ packages,
3292
+ git: inspectGit(target.path),
3293
+ checks,
3294
+ issues: deduplicateIssues(issues)
3295
+ };
3296
+ }
3297
+ function runTargetChecks(target) {
3298
+ const proGovCli = join18(target.path, "node_modules/@pieai/pro-gov/dist/cli.js");
3299
+ const docGovCli = join18(target.path, "node_modules/@pieai/doc-gov/dist/cli.js");
3300
+ const commands = [
3301
+ {
3302
+ name: "pro-gov doctor",
3303
+ cli: proGovCli,
3304
+ args: target.profile === "engineering-runtime" ? ["doctor", "--strict-hooks"] : ["doctor"]
3305
+ },
3306
+ { name: "doc-gov router-check", cli: docGovCli, args: ["router-check"] },
3307
+ { name: "doc-gov scan --check", cli: docGovCli, args: ["scan", "--check"] }
3308
+ ];
3309
+ return commands.map((command2) => {
3310
+ if (!existsSync19(command2.cli)) return { name: command2.name, status: null };
3311
+ const result = spawnSync6(process.execPath, [command2.cli, ...command2.args], {
3312
+ cwd: target.path,
3313
+ encoding: "utf8",
3314
+ timeout: 3e4
3315
+ });
3316
+ return { name: command2.name, status: result.status };
3317
+ });
3318
+ }
3319
+ function inspectGit(path) {
3320
+ const inside = spawnSync6("git", ["rev-parse", "--is-inside-work-tree"], {
3321
+ cwd: path,
3322
+ encoding: "utf8"
3323
+ });
3324
+ if (inside.status !== 0) return { isRepository: false, dirty: false };
3325
+ const status = spawnSync6("git", ["status", "--porcelain"], { cwd: path, encoding: "utf8" });
3326
+ const branch = spawnSync6("git", ["branch", "--show-current"], { cwd: path, encoding: "utf8" });
3327
+ return {
3328
+ isRepository: true,
3329
+ dirty: status.stdout.trim().length > 0,
3330
+ branch: branch.stdout.trim() || void 0
3331
+ };
3332
+ }
3333
+ function getExpectedPackageVersions() {
3334
+ const proGovPackage = readJson2(findOwnPackageJson());
3335
+ let docGovVersion;
3336
+ try {
3337
+ const require2 = createRequire2(import.meta.url);
3338
+ const docGovPackage = readJson2(require2.resolve("@pieai/doc-gov/package.json"));
3339
+ docGovVersion = docGovPackage?.version;
3340
+ } catch {
3341
+ docGovVersion = void 0;
3342
+ }
3343
+ return {
3344
+ "@pieai/pro-gov": proGovPackage?.version,
3345
+ "@pieai/doc-gov": docGovVersion
3346
+ };
3347
+ }
3348
+ function findOwnPackageJson() {
3349
+ let current = dirname11(fileURLToPath3(import.meta.url));
3350
+ for (let depth = 0; depth < 5; depth += 1) {
3351
+ const candidate = join18(current, "package.json");
3352
+ if (existsSync19(candidate)) return candidate;
3353
+ current = dirname11(current);
3354
+ }
3355
+ return "";
3356
+ }
3357
+ function readJson2(path) {
3358
+ if (!path || !existsSync19(path)) return void 0;
3359
+ try {
3360
+ return JSON.parse(readFileSync16(path, "utf8"));
3361
+ } catch {
3362
+ return void 0;
3363
+ }
3364
+ }
3365
+ function deduplicateIssues(issues) {
3366
+ const seen = /* @__PURE__ */ new Set();
3367
+ return issues.filter((issue) => {
3368
+ const key = `${issue.type}\0${issue.packageName ?? ""}\0${issue.targetPath ?? ""}\0${issue.check ?? ""}\0${issue.message}`;
3369
+ if (seen.has(key)) return false;
3370
+ seen.add(key);
3371
+ return true;
3372
+ });
3373
+ }
3374
+
2762
3375
  // src/commands/portfolio.ts
2763
3376
  function runPortfolio(args) {
2764
3377
  const [subcommand2, ...rest] = args;
2765
3378
  if (subcommand2 === "check") return runPortfolioCheck(rest);
2766
3379
  if (subcommand2 === "plan") return runPortfolioPlan(rest);
2767
3380
  if (subcommand2 === "assets-check") return runPortfolioAssetsCheck(rest);
3381
+ if (subcommand2 === "doctor") return runPortfolioDoctor(rest);
2768
3382
  printUsage3();
2769
3383
  return 1;
2770
3384
  }
3385
+ function runPortfolioDoctor(args) {
3386
+ const options = parsePortfolioOptions(args);
3387
+ if (!options.ok) {
3388
+ console.error(options.error);
3389
+ printUsage3();
3390
+ return 1;
3391
+ }
3392
+ const loaded = loadPortfolioManifest(options.value.configPath);
3393
+ if (loaded.issues.length > 0 || !loaded.manifest) {
3394
+ if (options.value.json) {
3395
+ console.log(JSON.stringify({ ok: false, configPath: loaded.configPath, issues: loaded.issues, targets: [] }, null, 2));
3396
+ } else {
3397
+ for (const issue of loaded.issues) console.error(`${issue.type}: ${issue.message}`);
3398
+ }
3399
+ return 1;
3400
+ }
3401
+ const targets = getDefaultPortfolioTargets(loaded.manifest).filter(
3402
+ (target) => !options.value.targetId || options.value.targetId === "all" || target.id === options.value.targetId
3403
+ );
3404
+ if (targets.length === 0) {
3405
+ console.error(`Unknown portfolio target: ${options.value.targetId}`);
3406
+ return 1;
3407
+ }
3408
+ const loadedAssets = loadAgentAssetRegistry({
3409
+ agentAssetsDir: findPortfolioAgentAssetsDir(loaded.manifest)
3410
+ });
3411
+ if (loadedAssets.issues.length > 0) {
3412
+ for (const issue of loadedAssets.issues) console.error(`${issue.type}: ${issue.message}`);
3413
+ return 1;
3414
+ }
3415
+ const result = inspectPortfolio({
3416
+ manifest: loaded.manifest,
3417
+ targets,
3418
+ agentAssetsDir: loadedAssets.agentAssetsDir,
3419
+ registry: loadedAssets.registry,
3420
+ bundles: loadAgentAssetBundles(loadedAssets.agentAssetsDir)
3421
+ });
3422
+ const output = { configPath: loaded.configPath, ...result };
3423
+ if (options.value.json) {
3424
+ console.log(JSON.stringify(output, null, 2));
3425
+ } else if (result.ok) {
3426
+ console.log(`portfolio doctor passed (${targets.length} targets)`);
3427
+ } else {
3428
+ for (const issue of result.hostTooling.issues) console.log(`${issue.host} ${issue.type}: ${issue.message}`);
3429
+ for (const target of result.targets) {
3430
+ for (const issue of target.issues) console.log(`${target.id} ${issue.type}: ${issue.message}`);
3431
+ }
3432
+ }
3433
+ return result.ok ? 0 : 1;
3434
+ }
2771
3435
  function runPortfolioCheck(args) {
2772
3436
  const options = parsePortfolioOptions(args);
2773
3437
  if (!options.ok) {
@@ -3010,19 +3674,20 @@ function isHost2(value) {
3010
3674
  return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
3011
3675
  }
3012
3676
  function findPortfolioAgentAssetsDir(manifest) {
3013
- const agentAssetsDir = manifest?.executionEngine?.path ? join16(manifest.executionEngine.path, "agent-assets") : void 0;
3014
- return agentAssetsDir && existsSync17(join16(agentAssetsDir, "registry.json")) ? agentAssetsDir : void 0;
3677
+ const agentAssetsDir = manifest?.executionEngine?.path ? join19(manifest.executionEngine.path, "agent-assets") : void 0;
3678
+ return agentAssetsDir && existsSync20(join19(agentAssetsDir, "registry.json")) ? agentAssetsDir : void 0;
3015
3679
  }
3016
3680
  function printUsage3() {
3017
3681
  console.error("Usage:");
3018
3682
  console.error(" pro-gov portfolio check --config <path> [--json]");
3019
3683
  console.error(" pro-gov portfolio plan --config <path> [--target <id|all>] [--host codex|claude-code|gemini-cli|antigravity] [--json]");
3020
3684
  console.error(" pro-gov portfolio assets-check --config <path> [--target <id|all>] [--json]");
3685
+ console.error(" pro-gov portfolio doctor --config <path> [--target <id|all>] [--json]");
3021
3686
  }
3022
3687
 
3023
3688
  // src/commands/sync.ts
3024
- import { existsSync as existsSync18, readFileSync as readFileSync15 } from "node:fs";
3025
- import { join as join17 } from "node:path";
3689
+ import { existsSync as existsSync21, readFileSync as readFileSync17 } from "node:fs";
3690
+ import { join as join20 } from "node:path";
3026
3691
  function runSync(args) {
3027
3692
  if (!args.includes("--check")) {
3028
3693
  console.error("pro-gov sync is read-only and requires --check.");
@@ -3049,16 +3714,16 @@ function runSync(args) {
3049
3714
  console.log("pro-gov sync check");
3050
3715
  console.log(`profile: ${profile}`);
3051
3716
  for (const file of planStarterFiles(profile)) {
3052
- const targetPath = join17(process.cwd(), file.targetPath);
3053
- if (!existsSync18(targetPath)) {
3717
+ const targetPath = join20(process.cwd(), file.targetPath);
3718
+ if (!existsSync21(targetPath)) {
3054
3719
  if (file.ownership === "optional-guardrail") continue;
3055
3720
  console.log(`missing: ${file.targetPath}`);
3056
3721
  differences += 1;
3057
3722
  continue;
3058
3723
  }
3059
3724
  if (file.ownership === "project-local-seed") continue;
3060
- const source = readFileSync15(file.absoluteSourcePath, "utf8");
3061
- const target = readFileSync15(targetPath, "utf8");
3725
+ const source = readFileSync17(file.absoluteSourcePath, "utf8");
3726
+ const target = readFileSync17(targetPath, "utf8");
3062
3727
  if (source !== target) {
3063
3728
  console.log(`different: ${file.targetPath}`);
3064
3729
  differences += 1;
@@ -3073,7 +3738,7 @@ function runSync(args) {
3073
3738
  }
3074
3739
  function inferInstalledProfile(root) {
3075
3740
  const installed = ["engineering-runtime", "doc-only"].filter(
3076
- (profile) => existsSync18(join17(root, `docs/governance/agents-routing/${profile}-v0.9.md`))
3741
+ (profile) => existsSync21(join20(root, `docs/governance/agents-routing/${profile}-v0.9.md`))
3077
3742
  );
3078
3743
  return installed.length === 1 ? installed[0] : void 0;
3079
3744
  }
@@ -3096,6 +3761,7 @@ var COMMANDS = [
3096
3761
  "portfolio check --config <path> [--json]",
3097
3762
  "portfolio plan --config <path> [--target <id|all>] [--json]",
3098
3763
  "portfolio assets-check --config <path> [--target <id|all>] [--json]",
3764
+ "portfolio doctor --config <path> [--target <id|all>] [--json]",
3099
3765
  "lens scan [--target <path>] [--json]",
3100
3766
  "lens inspect [--target <path>] [--format text|json]",
3101
3767
  "lens report --target <path> --out <path>",
@@ -3107,27 +3773,22 @@ var COMMANDS = [
3107
3773
  "doctor"
3108
3774
  ];
3109
3775
  var [command, subcommand] = process.argv.slice(2);
3110
- if (!command || command === "--help" || command === "-h") {
3111
- printHelp();
3112
- process.exitCode = command ? 0 : 1;
3113
- } else if (command === "assets") {
3114
- process.exitCode = runAssets(process.argv.slice(3));
3115
- } else if (command === "lens") {
3116
- process.exitCode = runLens(process.argv.slice(3));
3117
- } else if (command === "portfolio") {
3118
- process.exitCode = runPortfolio(process.argv.slice(3));
3119
- } else if (command === "host-hook") {
3120
- process.exitCode = runHostHook(process.argv.slice(3));
3121
- } else if (command === "init") {
3122
- process.exitCode = runInit(process.argv.slice(3));
3123
- } else if (command === "sync") {
3124
- process.exitCode = runSync(process.argv.slice(3));
3125
- } else if (command === "doctor") {
3126
- process.exitCode = runDoctor(process.argv.slice(3));
3127
- } else {
3776
+ process.exitCode = await main();
3777
+ async function main() {
3778
+ if (!command || command === "--help" || command === "-h") {
3779
+ printHelp();
3780
+ return command ? 0 : 1;
3781
+ }
3782
+ if (command === "assets") return runAssets(process.argv.slice(3));
3783
+ if (command === "lens") return runLens(process.argv.slice(3));
3784
+ if (command === "portfolio") return runPortfolio(process.argv.slice(3));
3785
+ if (command === "host-hook") return runHostHook(process.argv.slice(3));
3786
+ if (command === "init") return runInit(process.argv.slice(3));
3787
+ if (command === "sync") return runSync(process.argv.slice(3));
3788
+ if (command === "doctor") return runDoctor(process.argv.slice(3));
3128
3789
  console.error(`Unknown command: ${[command, subcommand].filter(Boolean).join(" ")}`);
3129
3790
  printHelp();
3130
- process.exitCode = 1;
3791
+ return 1;
3131
3792
  }
3132
3793
  function printHelp() {
3133
3794
  console.log("pro-gov \u2014 project-level distribution kit for Project Governance System");