@pieai/pro-gov 0.3.11 → 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/README.md CHANGED
@@ -45,6 +45,8 @@ pro-gov assets discover --target .
45
45
  pro-gov assets recommend --target .
46
46
  pro-gov portfolio check --config /path/to/portfolio.json
47
47
  pro-gov portfolio plan --config /path/to/portfolio.json --target web-app --json
48
+ pro-gov portfolio assets-check --config /path/to/portfolio.json --json
49
+ pro-gov portfolio doctor --config /path/to/portfolio.json --json
48
50
  pro-gov lens inspect --target .
49
51
  pro-gov lens report --target . --out .pro-gov/lens-report.md
50
52
  pro-gov init --profile engineering-runtime --dry-run
@@ -63,6 +65,8 @@ What these commands do:
63
65
  | `assets recommend` | Suggests relevant asset bundles with reasons. | No |
64
66
  | `portfolio check` | Validates an external portfolio manifest owned by the user or organization. | No |
65
67
  | `portfolio plan` | Builds dry-run asset plans for manifest targets, using packaged public assets unless a full checkout is supplied. | No |
68
+ | `portfolio assets-check` | Checks the integrity of each target's current managed asset lock and symlinks. | No |
69
+ | `portfolio doctor` | Runs the offline fleet gate: package, target CLI, bundle, asset, symlink, Git-state evidence, and optional host-tooling checks. | No |
66
70
  | `lens inspect` | Produces ProjectLens-style local evidence. | No |
67
71
  | `lens report` | Writes the requested report file. | Only the explicit output |
68
72
  | `init --dry-run` | Shows starter/profile files that would be needed. | No |
@@ -98,7 +102,10 @@ User-scoped skills, such as a personal loop library, are not installed into
98
102
  project targets; link them once under the user's skill roots instead.
99
103
 
100
104
  The plan is the safety gate. `apply` may update managed targets described by the
101
- plan; it must not overwrite an unrelated unmanaged file.
105
+ plan; it must not overwrite an unrelated unmanaged file. When an asset leaves a
106
+ bundle, a plan may remove its old symlink only when the previous lock proves
107
+ ownership and both plan-time and apply-time checks confirm that the path is
108
+ still the same managed symlink.
102
109
 
103
110
  These checkout-only workflows depend on a maintainer-local `agent-assets/`
104
111
  registry when local-only assets are being used. The public repository and npm
@@ -111,7 +118,7 @@ the private-source and public-copy hashes recorded during promotion.
111
118
  - `pro-gov` distributes starter, profile, integration, and adoption assets.
112
119
  - `pro-gov assets discover|recommend` provides read-only project evidence and
113
120
  deterministic recommendations.
114
- - `pro-gov portfolio check|plan` reads an external portfolio manifest. Real
121
+ - `pro-gov portfolio check|plan|assets-check|doctor` reads an external portfolio manifest. Real
115
122
  downstream project lists belong in the user's control repository, not in this
116
123
  public package. The manifest does not require a private headquarters repo:
117
124
  npm users can omit `controlPlane` and `executionEngine`, and PGS will use the
@@ -127,6 +134,8 @@ the private-source and public-copy hashes recorded during promotion.
127
134
  use `--dry-run` and a deliberate migration so local truth is never overwritten.
128
135
  - Superpowers, Compound Engineering, and Ponytail are external tools, not
129
136
  bundled runtime dependencies.
137
+ - Optional `hostTooling` entries let `portfolio doctor` verify required Codex or
138
+ Claude Code plugins. PGS does not install or upgrade those plugins.
130
139
 
131
140
  ## Recommended Companion Tools
132
141
 
@@ -153,6 +153,10 @@ Managed symlinks are relative by default. A target project's normal
153
153
  public package to know a maintainer's private registry. Maintainers can add
154
154
  `--strict-registry`, or run `pro-gov portfolio assets-check --config
155
155
  /path/to/portfolio.json`, when they need central private-registry validation.
156
+ Use `pro-gov portfolio doctor --config /path/to/portfolio.json` as the final
157
+ offline fleet gate when package versions, target-local routers and hooks,
158
+ declared bundles, locks, managed symlinks, and optional host plugins all need to
159
+ be checked together. The command reports dirty Git state but does not edit it.
156
160
 
157
161
  When a maintainer promotes a private asset into `public-agent-assets/`, the
158
162
  public registry must record the private-source hash and the public-copy hash.
@@ -125,4 +125,7 @@ install PGS packages
125
125
 
126
126
  PGS does not automatically install, enable, update, or remove these external
127
127
  plugins in another person's AI host. It documents the recommended boundary and
128
- lets each project or user adopt tools deliberately.
128
+ lets each project or user adopt tools deliberately. A user-owned portfolio may
129
+ declare required Codex or Claude Code plugin ids in `hostTooling`; `portfolio
130
+ doctor` then verifies installed/enabled state and reports observed versions.
131
+ The host's native plugin manager remains the installation and update authority.
package/cli-guide.md CHANGED
@@ -18,6 +18,8 @@ pro-gov assets discover --target .
18
18
  pro-gov assets recommend --target .
19
19
  pro-gov portfolio check --config /path/to/portfolio.json
20
20
  pro-gov portfolio plan --config /path/to/portfolio.json --target web-app --json
21
+ pro-gov portfolio assets-check --config /path/to/portfolio.json --json
22
+ pro-gov portfolio doctor --config /path/to/portfolio.json --json
21
23
  pro-gov lens inspect --target .
22
24
  pro-gov lens report --target . --out .pro-gov/lens-report.md
23
25
  pro-gov lens audit init --target /path/to/project --out audits/project/2026-07-01
@@ -46,6 +48,8 @@ Full upstream-checkout commands:
46
48
  pro-gov assets list --json
47
49
  pro-gov portfolio check --config /path/to/control-repo/.pro-gov/portfolio.json --json
48
50
  pro-gov portfolio plan --config /path/to/control-repo/.pro-gov/portfolio.json --target web-app --json
51
+ pro-gov portfolio assets-check --config /path/to/control-repo/.pro-gov/portfolio.json --json
52
+ pro-gov portfolio doctor --config /path/to/control-repo/.pro-gov/portfolio.json --json
49
53
  pro-gov assets plan --bundle base-governance --target . --out .pro-gov/asset-plan.json
50
54
  pro-gov assets plan --bundle project-lens --target /path/to/project --host codex --placement manual --out /tmp/project-lens-plan.json
51
55
  pro-gov assets apply --plan .pro-gov/asset-plan.json
@@ -72,6 +76,27 @@ public assets packaged with `@pieai/pro-gov`. Add `executionEngine.path` only
72
76
  when a full local checkout should provide a private asset registry for strict
73
77
  maintainer checks.
74
78
 
79
+ `portfolio assets-check` verifies that the currently recorded lock and managed
80
+ links are healthy. `portfolio doctor` additionally compares each target with the
81
+ current portfolio `assetBundles`, registry hashes, package versions, target-local
82
+ router/hook checks, and optional `hostTooling` requirements. The doctor is
83
+ offline and read-only by default. A dirty product worktree is reported as
84
+ evidence but is not itself a governance failure.
85
+
86
+ Third-party update discovery is deliberately separate and low frequency:
87
+
88
+ ```bash
89
+ pro-gov assets npx update --plan
90
+ codex plugin marketplace upgrade
91
+ codex plugin list --json
92
+ claude plugin update <plugin>
93
+ claude plugin list --json
94
+ ```
95
+
96
+ The native tools own fetching and installation. PGS reviews resulting asset
97
+ changes, regenerates plans, and verifies the fleet; it does not implement a
98
+ second plugin marketplace or silently accept upstream changes.
99
+
75
100
  `lens audit init` creates a raw-first audit package for Project Lens plus
76
101
  Ponytail reviews. For a "read-only project audit", the target repository remains
77
102
  read-only, but the audit package is still the required output record. `lens
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,
@@ -757,7 +1010,7 @@ function expectedSkillTargetPrefixes(host) {
757
1010
  return void 0;
758
1011
  }
759
1012
  function expectedRegistrySkillTargetPath(host, sourcePath, placement) {
760
- const skillName = basename(sourcePath);
1013
+ const skillName = basename2(sourcePath);
761
1014
  if (host === "claude-code") {
762
1015
  return placement === "manual" ? void 0 : `.claude/skills/${skillName}`;
763
1016
  }
@@ -766,163 +1019,6 @@ function expectedRegistrySkillTargetPath(host, sourcePath, placement) {
766
1019
  }
767
1020
  return void 0;
768
1021
  }
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)}`;
897
- }
898
- if (asset.kind === "rule") {
899
- return `.pro-gov/agent-assets/rules/${basename2(asset.sourcePath)}`;
900
- }
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";
906
- }
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);
913
- }
914
- return [...directories].sort().map((targetPath) => ({ type: "create-dir", targetPath }));
915
- }
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();
924
- }
925
- }
926
1022
  function pathExistsEvenIfDanglingSymlink4(path) {
927
1023
  try {
928
1024
  lstatSync4(path);
@@ -1642,7 +1738,7 @@ function isEngineeringRuntimeProject(root) {
1642
1738
  import { spawnSync as spawnSync3 } from "node:child_process";
1643
1739
  import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
1644
1740
  import { tmpdir as tmpdir2 } from "node:os";
1645
- import { basename as basename3, join as join13, resolve as resolve2 } from "node:path";
1741
+ import { basename as basename3, join as join13, resolve as resolve3 } from "node:path";
1646
1742
 
1647
1743
  // src/host-hooks/host-hook-runner.ts
1648
1744
  import { closeSync, openSync, readFileSync as readFileSync9, readSync, statSync as statSync3 } from "node:fs";
@@ -1967,7 +2063,7 @@ function defaultDebugDir() {
1967
2063
  });
1968
2064
  const value = gitPath.status === 0 ? gitPath.stdout.trim() : "";
1969
2065
  if (value) {
1970
- return resolve2(process.cwd(), value);
2066
+ return resolve3(process.cwd(), value);
1971
2067
  }
1972
2068
  return join13(tmpdir2(), "pro-gov-hook-debug", basename3(process.cwd()));
1973
2069
  }
@@ -1983,8 +2079,8 @@ function readPackageVersion() {
1983
2079
  }
1984
2080
  function resolvePackageJsonPath() {
1985
2081
  const candidates = [
1986
- resolve2(process.cwd(), "packages/pro-gov/package.json"),
1987
- resolve2(process.cwd(), "node_modules/@pieai/pro-gov/package.json")
2082
+ resolve3(process.cwd(), "packages/pro-gov/package.json"),
2083
+ resolve3(process.cwd(), "node_modules/@pieai/pro-gov/package.json")
1988
2084
  ];
1989
2085
  return candidates.find((candidate) => existsSync13(candidate));
1990
2086
  }
@@ -2691,12 +2787,12 @@ function printUsage2() {
2691
2787
  }
2692
2788
 
2693
2789
  // src/commands/portfolio.ts
2694
- import { existsSync as existsSync18 } from "node:fs";
2695
- import { join as join17 } from "node:path";
2790
+ import { existsSync as existsSync20 } from "node:fs";
2791
+ import { join as join19 } from "node:path";
2696
2792
 
2697
2793
  // src/portfolio/manifest.ts
2698
2794
  import { existsSync as existsSync17, readFileSync as readFileSync14 } from "node:fs";
2699
- import { dirname as dirname10, isAbsolute as isAbsolute3, resolve as resolve3 } from "node:path";
2795
+ import { dirname as dirname10, isAbsolute as isAbsolute3, resolve as resolve4 } from "node:path";
2700
2796
  function loadPortfolioManifest(configPath) {
2701
2797
  let parsed;
2702
2798
  try {
@@ -2712,7 +2808,7 @@ function loadPortfolioManifest(configPath) {
2712
2808
  ]
2713
2809
  };
2714
2810
  }
2715
- const normalized = resolveManifestPaths(parsed, dirname10(resolve3(configPath)));
2811
+ const normalized = resolveManifestPaths(parsed, dirname10(resolve4(configPath)));
2716
2812
  const issues = validatePortfolioManifest(normalized);
2717
2813
  return {
2718
2814
  configPath,
@@ -2726,7 +2822,7 @@ function resolveManifestPaths(value, configDir) {
2726
2822
  if (!isRecord2(endpoint) || typeof endpoint.path !== "string" || isAbsolute3(endpoint.path)) {
2727
2823
  return endpoint;
2728
2824
  }
2729
- return { ...endpoint, path: resolve3(configDir, endpoint.path) };
2825
+ return { ...endpoint, path: resolve4(configDir, endpoint.path) };
2730
2826
  };
2731
2827
  return {
2732
2828
  ...value,
@@ -2760,9 +2856,15 @@ function validatePortfolioManifest(value) {
2760
2856
  message: "Portfolio manifest portfolioId must be a non-empty string."
2761
2857
  });
2762
2858
  }
2763
- 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
+ );
2764
2865
  validateEndpoint(value.controlPlane, "controlPlane", issues);
2765
2866
  validateEndpoint(value.executionEngine, "executionEngine", issues);
2867
+ validateHostTooling(value.hostTooling, issues);
2766
2868
  if (!Array.isArray(value.targets)) {
2767
2869
  issues.push({
2768
2870
  type: "invalid-field",
@@ -2874,19 +2976,462 @@ function validateOptionalStringArray(value, id, field, issues) {
2874
2976
  });
2875
2977
  }
2876
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
+ }
2877
3024
  function isRecord2(value) {
2878
3025
  return typeof value === "object" && value !== null && !Array.isArray(value);
2879
3026
  }
2880
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
+
2881
3375
  // src/commands/portfolio.ts
2882
3376
  function runPortfolio(args) {
2883
3377
  const [subcommand2, ...rest] = args;
2884
3378
  if (subcommand2 === "check") return runPortfolioCheck(rest);
2885
3379
  if (subcommand2 === "plan") return runPortfolioPlan(rest);
2886
3380
  if (subcommand2 === "assets-check") return runPortfolioAssetsCheck(rest);
3381
+ if (subcommand2 === "doctor") return runPortfolioDoctor(rest);
2887
3382
  printUsage3();
2888
3383
  return 1;
2889
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
+ }
2890
3435
  function runPortfolioCheck(args) {
2891
3436
  const options = parsePortfolioOptions(args);
2892
3437
  if (!options.ok) {
@@ -3129,19 +3674,20 @@ function isHost2(value) {
3129
3674
  return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
3130
3675
  }
3131
3676
  function findPortfolioAgentAssetsDir(manifest) {
3132
- const agentAssetsDir = manifest?.executionEngine?.path ? join17(manifest.executionEngine.path, "agent-assets") : void 0;
3133
- return agentAssetsDir && existsSync18(join17(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;
3134
3679
  }
3135
3680
  function printUsage3() {
3136
3681
  console.error("Usage:");
3137
3682
  console.error(" pro-gov portfolio check --config <path> [--json]");
3138
3683
  console.error(" pro-gov portfolio plan --config <path> [--target <id|all>] [--host codex|claude-code|gemini-cli|antigravity] [--json]");
3139
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]");
3140
3686
  }
3141
3687
 
3142
3688
  // src/commands/sync.ts
3143
- import { existsSync as existsSync19, readFileSync as readFileSync15 } from "node:fs";
3144
- import { join as join18 } from "node:path";
3689
+ import { existsSync as existsSync21, readFileSync as readFileSync17 } from "node:fs";
3690
+ import { join as join20 } from "node:path";
3145
3691
  function runSync(args) {
3146
3692
  if (!args.includes("--check")) {
3147
3693
  console.error("pro-gov sync is read-only and requires --check.");
@@ -3168,16 +3714,16 @@ function runSync(args) {
3168
3714
  console.log("pro-gov sync check");
3169
3715
  console.log(`profile: ${profile}`);
3170
3716
  for (const file of planStarterFiles(profile)) {
3171
- const targetPath = join18(process.cwd(), file.targetPath);
3172
- if (!existsSync19(targetPath)) {
3717
+ const targetPath = join20(process.cwd(), file.targetPath);
3718
+ if (!existsSync21(targetPath)) {
3173
3719
  if (file.ownership === "optional-guardrail") continue;
3174
3720
  console.log(`missing: ${file.targetPath}`);
3175
3721
  differences += 1;
3176
3722
  continue;
3177
3723
  }
3178
3724
  if (file.ownership === "project-local-seed") continue;
3179
- const source = readFileSync15(file.absoluteSourcePath, "utf8");
3180
- const target = readFileSync15(targetPath, "utf8");
3725
+ const source = readFileSync17(file.absoluteSourcePath, "utf8");
3726
+ const target = readFileSync17(targetPath, "utf8");
3181
3727
  if (source !== target) {
3182
3728
  console.log(`different: ${file.targetPath}`);
3183
3729
  differences += 1;
@@ -3192,7 +3738,7 @@ function runSync(args) {
3192
3738
  }
3193
3739
  function inferInstalledProfile(root) {
3194
3740
  const installed = ["engineering-runtime", "doc-only"].filter(
3195
- (profile) => existsSync19(join18(root, `docs/governance/agents-routing/${profile}-v0.9.md`))
3741
+ (profile) => existsSync21(join20(root, `docs/governance/agents-routing/${profile}-v0.9.md`))
3196
3742
  );
3197
3743
  return installed.length === 1 ? installed[0] : void 0;
3198
3744
  }
@@ -3215,6 +3761,7 @@ var COMMANDS = [
3215
3761
  "portfolio check --config <path> [--json]",
3216
3762
  "portfolio plan --config <path> [--target <id|all>] [--json]",
3217
3763
  "portfolio assets-check --config <path> [--target <id|all>] [--json]",
3764
+ "portfolio doctor --config <path> [--target <id|all>] [--json]",
3218
3765
  "lens scan [--target <path>] [--json]",
3219
3766
  "lens inspect [--target <path>] [--format text|json]",
3220
3767
  "lens report --target <path> --out <path>",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pieai/pro-gov",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "description": "Project-level distribution kit for Project Governance System.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -35,7 +35,7 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "@pieai/doc-gov": "^0.3.11"
38
+ "@pieai/doc-gov": "^0.3.12"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "24.13.2",