@pieai/pro-gov 0.5.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -565,7 +565,7 @@ import {
565
565
  lstatSync as lstatSync3,
566
566
  mkdirSync as mkdirSync2,
567
567
  readlinkSync as readlinkSync2,
568
- realpathSync,
568
+ realpathSync as realpathSync2,
569
569
  symlinkSync,
570
570
  unlinkSync,
571
571
  writeFileSync
@@ -573,7 +573,7 @@ import {
573
573
  import { dirname as dirname4, join as join8, relative as relative4, resolve as resolve2 } from "node:path";
574
574
 
575
575
  // src/asset-targets/install-plan.ts
576
- import { existsSync as existsSync7, lstatSync as lstatSync2, readFileSync as readFileSync4, readlinkSync } from "node:fs";
576
+ import { existsSync as existsSync7, lstatSync as lstatSync2, readFileSync as readFileSync4, readlinkSync, realpathSync } from "node:fs";
577
577
  import { basename, dirname as dirname3, join as join7, resolve } from "node:path";
578
578
  function createAssetInstallPlan(options) {
579
579
  const placement = options.placement ?? "registry";
@@ -589,10 +589,19 @@ function createAssetInstallPlan(options) {
589
589
  return asset;
590
590
  });
591
591
  const lockEntries = createAgentAssetLockEntries(options.registry, options.agentAssetsDir, assetIds);
592
- const managedEntries = readManagedEntries(options.targetDir);
592
+ const managedLock = readManagedLock(options.targetDir);
593
+ const managedEntries = managedLock.entries;
593
594
  const managedTargets = new Set(managedEntries.map((entry) => entry.targetPath));
595
+ const legacyAdoptions = createLegacyClaudeAdoptions({
596
+ targetDir: options.targetDir,
597
+ agentAssetsDir: options.agentAssetsDir,
598
+ managedLock,
599
+ assets,
600
+ host: options.host,
601
+ placement
602
+ });
594
603
  const assetActions = assets.map(
595
- (asset) => createAssetAction(asset, options.agentAssetsDir, options.targetDir, options.host, placement, managedTargets)
604
+ (asset) => legacyAdoptions.byAssetId.get(asset.id) ?? createAssetAction(asset, options.agentAssetsDir, options.targetDir, options.host, placement, managedTargets)
596
605
  );
597
606
  const manifest = {
598
607
  schemaVersion: 1,
@@ -635,7 +644,8 @@ function createAssetInstallPlan(options) {
635
644
  options.targetDir,
636
645
  options.agentAssetsDir,
637
646
  managedEntries,
638
- expectedTargetPaths
647
+ expectedTargetPaths,
648
+ legacyAdoptions.consumedTargetPaths
639
649
  );
640
650
  return {
641
651
  schemaVersion: 1,
@@ -693,15 +703,9 @@ function createAssetAction(asset, agentAssetsDir, targetDir, host, placement, ma
693
703
  targetPath
694
704
  };
695
705
  }
696
- function resolveHostTargetPath(asset, host, placement) {
706
+ function resolveHostTargetPath(asset, _host, placement) {
697
707
  if (asset.kind === "skill") {
698
708
  const effectivePlacement = resolveSkillPlacement(asset, placement);
699
- if (host === "claude-code") {
700
- if (placement === "manual") {
701
- throw new Error("Manual skill placement is only supported for .agents hosts");
702
- }
703
- return `.claude/skills/${basename(asset.sourcePath)}`;
704
- }
705
709
  if (effectivePlacement === "manual") {
706
710
  return `.agents/manual-skills/${basename(asset.sourcePath)}`;
707
711
  }
@@ -725,22 +729,84 @@ function createDirectoryActions(actions) {
725
729
  }
726
730
  return [...directories].sort().map((targetPath) => ({ type: "create-dir", targetPath }));
727
731
  }
728
- function readManagedEntries(targetDir) {
732
+ function readManagedLock(targetDir) {
729
733
  const lockfilePath = join7(targetDir, ".pro-gov/assets.lock.json");
730
- if (!existsSync7(lockfilePath)) return [];
734
+ if (!existsSync7(lockfilePath)) return { entries: [] };
731
735
  try {
732
736
  const lockfile = JSON.parse(readFileSync4(lockfilePath, "utf8"));
733
- return (lockfile.assets ?? []).filter(
734
- (entry) => typeof entry.id === "string" && typeof entry.sourcePath === "string" && typeof entry.targetPath === "string"
735
- );
737
+ return {
738
+ host: typeof lockfile.host === "string" ? lockfile.host : void 0,
739
+ entries: (lockfile.assets ?? []).filter(
740
+ (entry) => typeof entry.id === "string" && typeof entry.sourcePath === "string" && typeof entry.targetPath === "string"
741
+ )
742
+ };
736
743
  } catch {
737
- return [];
744
+ return { entries: [] };
745
+ }
746
+ }
747
+ function createLegacyClaudeAdoptions(options) {
748
+ const byAssetId = /* @__PURE__ */ new Map();
749
+ const consumedTargetPaths = /* @__PURE__ */ new Set();
750
+ const legacyEntries = options.managedLock.entries.filter((entry) => isLegacyClaudeSkillTargetPath(entry.targetPath));
751
+ if (legacyEntries.length === 0) return { byAssetId, consumedTargetPaths };
752
+ if (options.managedLock.host !== "claude-code" || options.host !== "claude-code") {
753
+ throw new Error("Legacy Claude skill targets require a claude-code lock and migration plan.");
754
+ }
755
+ const assetsById = new Map(options.assets.map((asset) => [asset.id, asset]));
756
+ const seenAssetIds = /* @__PURE__ */ new Set();
757
+ for (const entry of legacyEntries) {
758
+ if (seenAssetIds.has(entry.id)) {
759
+ throw new Error(`Duplicate legacy Claude lock entry: ${entry.id}`);
760
+ }
761
+ seenAssetIds.add(entry.id);
762
+ const asset = assetsById.get(entry.id);
763
+ if (!asset) {
764
+ throw new Error(`Legacy Claude asset ${entry.id} must be adopted before it can be removed.`);
765
+ }
766
+ const match = entry.targetPath.match(/^\.claude\/skills\/([^/]+)$/);
767
+ const skillName = match?.[1];
768
+ if (!skillName || asset.kind !== "skill" || entry.sourcePath !== asset.sourcePath || skillName !== basename(entry.sourcePath) || resolveHostTargetPath(asset, options.host, options.placement) !== `.agents/skills/${skillName}`) {
769
+ throw new Error(`Legacy Claude lock entry cannot be safely normalized: ${entry.targetPath}`);
770
+ }
771
+ const targetPath = `.agents/skills/${skillName}`;
772
+ const targetAbsolutePath = join7(options.targetDir, targetPath);
773
+ const compatibilityRootPath = join7(options.targetDir, ".claude/skills");
774
+ const canonicalRootPath = join7(options.targetDir, ".agents/skills");
775
+ if (!lstatSync2(canonicalRootPath).isDirectory() || !lstatSync2(compatibilityRootPath).isSymbolicLink() || readlinkSync(compatibilityRootPath) !== "../.agents/skills" || realpathSync(compatibilityRootPath) !== realpathSync(canonicalRootPath)) {
776
+ throw new Error(`Legacy Claude compatibility root is not the exact canonical alias for ${entry.id}.`);
777
+ }
778
+ if (!pathExistsEvenIfDanglingSymlink2(targetAbsolutePath)) {
779
+ consumedTargetPaths.add(entry.targetPath);
780
+ continue;
781
+ }
782
+ const targetStat = lstatSync2(targetAbsolutePath);
783
+ const legacyAbsolutePath = join7(options.targetDir, entry.targetPath);
784
+ const legacyStat = lstatSync2(legacyAbsolutePath);
785
+ const expectedSourcePath = join7(options.agentAssetsDir, entry.sourcePath);
786
+ if (!targetStat.isSymbolicLink() || !legacyStat.isSymbolicLink() || targetStat.dev !== legacyStat.dev || targetStat.ino !== legacyStat.ino || realpathSync(targetAbsolutePath) !== realpathSync(expectedSourcePath)) {
787
+ throw new Error(`Legacy Claude skill target cannot be safely adopted: ${entry.targetPath}`);
788
+ }
789
+ const action = {
790
+ type: "adopt-symlink",
791
+ assetId: entry.id,
792
+ sourcePath: expectedSourcePath,
793
+ targetPath,
794
+ legacyTargetPath: entry.targetPath,
795
+ compatibilityRootPath: ".claude/skills",
796
+ expectedCompatibilityRawTarget: "../.agents/skills",
797
+ expectedDevice: targetStat.dev,
798
+ expectedInode: targetStat.ino
799
+ };
800
+ byAssetId.set(entry.id, action);
801
+ consumedTargetPaths.add(entry.targetPath);
738
802
  }
803
+ return { byAssetId, consumedTargetPaths };
739
804
  }
740
- function createRemovalActions(targetDir, agentAssetsDir, managedEntries, expectedTargetPaths) {
805
+ function createRemovalActions(targetDir, agentAssetsDir, managedEntries, expectedTargetPaths, consumedTargetPaths) {
741
806
  const actions = [];
742
807
  for (const entry of managedEntries) {
743
808
  if (expectedTargetPaths.has(entry.targetPath)) continue;
809
+ if (consumedTargetPaths.has(entry.targetPath)) continue;
744
810
  if (!isManagedAssetTargetPath(entry.targetPath)) {
745
811
  throw new Error(`Refusing to remove managed asset outside supported roots: ${entry.targetPath}`);
746
812
  }
@@ -765,13 +831,10 @@ function createRemovalActions(targetDir, agentAssetsDir, managedEntries, expecte
765
831
  return actions.sort((a, b) => a.targetPath.localeCompare(b.targetPath));
766
832
  }
767
833
  function isManagedAssetTargetPath(path) {
768
- return [
769
- ".agents/skills/",
770
- ".agents/manual-skills/",
771
- ".claude/skills/",
772
- ".pro-gov/agent-assets/rules/",
773
- ".pro-gov/agent-assets/commands/"
774
- ].some((prefix) => path.startsWith(prefix));
834
+ return /^(?:\.agents\/(?:skills|manual-skills)|\.pro-gov\/agent-assets\/(?:rules|commands))\/[^/.][^/]*$/.test(path);
835
+ }
836
+ function isLegacyClaudeSkillTargetPath(path) {
837
+ return /^\.claude\/skills\/[^/.][^/]*$/.test(path);
775
838
  }
776
839
  function pathExistsEvenIfDanglingSymlink2(path) {
777
840
  try {
@@ -785,6 +848,9 @@ function pathExistsEvenIfDanglingSymlink2(path) {
785
848
  // src/asset-targets/apply.ts
786
849
  function applyAssetInstallPlan(plan) {
787
850
  const appliedActions = [];
851
+ for (const action of plan.actions) {
852
+ if (action.type === "adopt-symlink") validateAdoptedSymlink(plan.targetDir, action);
853
+ }
788
854
  for (const action of plan.actions) {
789
855
  applyAction(plan.targetDir, action);
790
856
  appliedActions.push(action.type);
@@ -797,6 +863,7 @@ function applyAction(targetDir, action) {
797
863
  removeManagedSymlink(targetAbsolutePath, action);
798
864
  return;
799
865
  }
866
+ if (action.type === "adopt-symlink") return;
800
867
  if (action.type === "create-dir") {
801
868
  mkdirSync2(targetAbsolutePath, { recursive: true });
802
869
  return;
@@ -808,7 +875,7 @@ function applyAction(targetDir, action) {
808
875
  }
809
876
  mkdirSync2(dirname4(targetAbsolutePath), { recursive: true });
810
877
  const sourceAbsolutePath = resolve2(action.sourcePath);
811
- const symlinkTarget = relative4(realpathSync(dirname4(targetAbsolutePath)), realpathSync(sourceAbsolutePath)) || ".";
878
+ const symlinkTarget = relative4(realpathSync2(dirname4(targetAbsolutePath)), realpathSync2(sourceAbsolutePath)) || ".";
812
879
  if (action.type === "symlink") {
813
880
  if (pathExistsEvenIfDanglingSymlink3(targetAbsolutePath)) {
814
881
  throw new Error(`Refusing to overwrite unmanaged target: ${action.targetPath}`);
@@ -827,6 +894,23 @@ function applyAction(targetDir, action) {
827
894
  unlinkSync(targetAbsolutePath);
828
895
  symlinkSync(symlinkTarget, targetAbsolutePath);
829
896
  }
897
+ function validateAdoptedSymlink(targetDir, action) {
898
+ if (!isManagedAssetTargetPath(action.targetPath) || !isLegacyClaudeSkillTargetPath(action.legacyTargetPath) || action.compatibilityRootPath !== ".claude/skills" || action.expectedCompatibilityRawTarget !== "../.agents/skills") {
899
+ throw new Error(`Refusing unsafe legacy Claude adoption: ${action.legacyTargetPath}`);
900
+ }
901
+ const canonicalRootPath = join8(targetDir, ".agents/skills");
902
+ const compatibilityRootPath = join8(targetDir, action.compatibilityRootPath);
903
+ const targetAbsolutePath = join8(targetDir, action.targetPath);
904
+ const legacyAbsolutePath = join8(targetDir, action.legacyTargetPath);
905
+ if (!lstatSync3(canonicalRootPath).isDirectory() || !lstatSync3(compatibilityRootPath).isSymbolicLink() || readlinkSync2(compatibilityRootPath) !== action.expectedCompatibilityRawTarget || realpathSync2(compatibilityRootPath) !== realpathSync2(canonicalRootPath)) {
906
+ throw new Error(`Legacy Claude compatibility alias changed before apply: ${action.compatibilityRootPath}`);
907
+ }
908
+ const targetStat = lstatSync3(targetAbsolutePath);
909
+ const legacyStat = lstatSync3(legacyAbsolutePath);
910
+ if (!targetStat.isSymbolicLink() || !legacyStat.isSymbolicLink() || targetStat.dev !== legacyStat.dev || targetStat.ino !== legacyStat.ino || targetStat.dev !== action.expectedDevice || targetStat.ino !== action.expectedInode || realpathSync2(targetAbsolutePath) !== realpathSync2(action.sourcePath)) {
911
+ throw new Error(`Legacy Claude skill target changed before apply: ${action.targetPath}`);
912
+ }
913
+ }
830
914
  function removeManagedSymlink(targetAbsolutePath, action) {
831
915
  if (!isManagedAssetTargetPath(action.targetPath)) {
832
916
  throw new Error(`Refusing to remove managed asset outside supported roots: ${action.targetPath}`);
@@ -1014,18 +1098,14 @@ function checkHostFolder(host, kind, targetPath, id) {
1014
1098
  return void 0;
1015
1099
  }
1016
1100
  function expectedSkillTargetPrefixes(host) {
1017
- if (host === "claude-code") return [".claude/skills/"];
1018
- if (host === "codex" || host === "gemini-cli" || host === "antigravity") {
1101
+ if (host === "codex" || host === "claude-code" || host === "gemini-cli" || host === "antigravity") {
1019
1102
  return [".agents/skills/", ".agents/manual-skills/"];
1020
1103
  }
1021
1104
  return void 0;
1022
1105
  }
1023
1106
  function expectedRegistrySkillTargetPath(host, sourcePath, placement) {
1024
1107
  const skillName = basename2(sourcePath);
1025
- if (host === "claude-code") {
1026
- return placement === "manual" ? void 0 : `.claude/skills/${skillName}`;
1027
- }
1028
- if (host === "codex" || host === "gemini-cli" || host === "antigravity") {
1108
+ if (host === "codex" || host === "claude-code" || host === "gemini-cli" || host === "antigravity") {
1029
1109
  return placement === "manual" ? `.agents/manual-skills/${skillName}` : `.agents/skills/${skillName}`;
1030
1110
  }
1031
1111
  return void 0;
@@ -1653,12 +1733,14 @@ import { existsSync as existsSync12 } from "node:fs";
1653
1733
  import { createRequire } from "node:module";
1654
1734
  import { dirname as dirname6, join as join12 } from "node:path";
1655
1735
  var REQUIRED_ASSETS = [
1736
+ "starter/.agents/skills/.gitkeep",
1656
1737
  "starter/AGENTS.template.md",
1657
- "starter/docs/governance/ssot-v1.0.md",
1658
- "starter/docs/governance/agents-routing/engineering-runtime-v1.0.md",
1659
- "starter/docs/governance/agents-routing/doc-only-v1.0.md",
1738
+ "starter/docs/governance/ssot-v1.1.md",
1739
+ "starter/docs/governance/agents-routing/engineering-runtime-v1.1.md",
1740
+ "starter/docs/governance/agents-routing/doc-only-v1.1.md",
1660
1741
  "profiles/engineering-runtime/profile.md",
1661
- "profiles/doc-only/profile.md"
1742
+ "profiles/doc-only/profile.md",
1743
+ "docs/reference/adoption/migration-v1.1.md"
1662
1744
  ];
1663
1745
  function runDoctor(_args) {
1664
1746
  const assets = listAssets();
@@ -1709,40 +1791,46 @@ function resolveDocGovDependencyCli() {
1709
1791
  }
1710
1792
 
1711
1793
  // src/commands/init.ts
1712
- import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "node:fs";
1794
+ import { lstatSync as lstatSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync8, symlinkSync as symlinkSync2, writeFileSync as writeFileSync3 } from "node:fs";
1713
1795
  import { basename as basename3, dirname as dirname7, join as join13 } from "node:path";
1714
1796
 
1715
1797
  // src/commands/shared.ts
1716
1798
  function planStarterFiles(profile) {
1717
- return listAssets().flatMap((asset) => {
1799
+ const files = listAssets().flatMap((asset) => {
1718
1800
  const targetPath = starterTargetPath(asset.path);
1719
1801
  if (!targetPath) return [];
1720
1802
  if (profile && isOtherProfileRouting(targetPath, profile)) return [];
1721
1803
  return [
1722
1804
  {
1805
+ kind: "file",
1723
1806
  sourcePath: asset.path,
1724
1807
  targetPath,
1725
1808
  absoluteSourcePath: asset.absolutePath,
1726
1809
  ownership: classifyOwnership(targetPath)
1727
1810
  }
1728
1811
  ];
1729
- }).sort((a, b) => a.targetPath.localeCompare(b.targetPath));
1812
+ });
1813
+ const compatibilityEntries = [
1814
+ { kind: "directory", targetPath: ".agents/skills", ownership: "shared" },
1815
+ { kind: "symlink", targetPath: "CLAUDE.md", linkTarget: "AGENTS.md", ownership: "shared" },
1816
+ { kind: "symlink", targetPath: ".claude/skills", linkTarget: "../.agents/skills", ownership: "shared" }
1817
+ ];
1818
+ return [...files, ...compatibilityEntries].sort((a, b) => a.targetPath.localeCompare(b.targetPath));
1730
1819
  }
1731
1820
  function classifyOwnership(targetPath) {
1732
1821
  if (targetPath === "lefthook.yml" || targetPath === ".github/workflows/docs-check.yml") {
1733
1822
  return "optional-guardrail";
1734
1823
  }
1735
- if (targetPath === "AGENTS.md" || targetPath === "CLAUDE.md" || targetPath === "docs/policy/best-practice-for-this-project.md" || targetPath === "docs/reference/documentation-map.md" || targetPath === "docs/reference/execution/current-work.md") {
1824
+ if (targetPath === "AGENTS.md" || targetPath === "docs/policy/best-practice-for-this-project.md" || targetPath === "docs/reference/documentation-map.md" || targetPath === "docs/reference/execution/current-work.md") {
1736
1825
  return "project-local-seed";
1737
1826
  }
1738
1827
  return "shared";
1739
1828
  }
1740
1829
  function isOtherProfileRouting(targetPath, profile) {
1741
- return targetPath.startsWith("docs/governance/agents-routing/") && targetPath !== `docs/governance/agents-routing/${profile}-v1.0.md`;
1830
+ return targetPath.startsWith("docs/governance/agents-routing/") && targetPath !== `docs/governance/agents-routing/${profile}-v1.1.md`;
1742
1831
  }
1743
1832
  function starterTargetPath(sourcePath) {
1744
1833
  if (sourcePath === "starter/AGENTS.template.md") return "AGENTS.md";
1745
- if (sourcePath === "starter/CLAUDE.template.md") return "CLAUDE.md";
1746
1834
  if (sourcePath === "starter/lefthook.template.yml") return "lefthook.yml";
1747
1835
  if (!sourcePath.startsWith("starter/")) return null;
1748
1836
  return sourcePath.slice("starter/".length);
@@ -1772,13 +1860,20 @@ function runInit(args) {
1772
1860
  console.log("");
1773
1861
  console.log("Planned starter files:");
1774
1862
  for (const file of files) {
1775
- console.log(` ${file.targetPath} <- ${file.sourcePath}`);
1863
+ if (file.kind === "file") console.log(` ${file.targetPath} <- ${file.sourcePath}`);
1864
+ if (file.kind === "directory") console.log(` ${file.targetPath} <- directory`);
1865
+ if (file.kind === "symlink") console.log(` ${file.targetPath} <- symlink:${file.linkTarget}`);
1776
1866
  }
1777
1867
  return 0;
1778
1868
  }
1779
1869
  function applyStarterFiles(files, profile) {
1780
1870
  const root = process.cwd();
1781
- const conflicts = files.filter((file) => existsSync13(join13(root, file.targetPath)));
1871
+ const conflicts = files.filter((file) => {
1872
+ const targetPath = join13(root, file.targetPath);
1873
+ const stat = safeLstat(targetPath);
1874
+ if (!stat) return false;
1875
+ return file.kind !== "directory" || !stat.isDirectory();
1876
+ });
1782
1877
  if (conflicts.length > 0) {
1783
1878
  console.error("pro-gov init is refusing to overwrite existing project files:");
1784
1879
  for (const file of conflicts) console.error(` ${file.targetPath}`);
@@ -1788,6 +1883,14 @@ function applyStarterFiles(files, profile) {
1788
1883
  for (const file of files) {
1789
1884
  const targetPath = join13(root, file.targetPath);
1790
1885
  mkdirSync4(dirname7(targetPath), { recursive: true });
1886
+ if (file.kind === "directory") {
1887
+ mkdirSync4(targetPath, { recursive: true });
1888
+ continue;
1889
+ }
1890
+ if (file.kind === "symlink") {
1891
+ symlinkSync2(file.linkTarget, targetPath);
1892
+ continue;
1893
+ }
1791
1894
  const source = readFileSync8(file.absoluteSourcePath);
1792
1895
  const content = file.targetPath === "AGENTS.md" ? renderAgentsTemplate(source.toString("utf8"), basename3(root), profile) : source;
1793
1896
  writeFileSync3(targetPath, content);
@@ -1800,9 +1903,16 @@ function applyStarterFiles(files, profile) {
1800
1903
  return 0;
1801
1904
  }
1802
1905
  function renderAgentsTemplate(template, projectName, profile) {
1803
- const selectedRoute = `docs/governance/agents-routing/${profile}-v1.0.md`;
1906
+ const selectedRoute = `docs/governance/agents-routing/${profile}-v1.1.md`;
1804
1907
  return template.replace("# PROJECT_NAME AI Router", `# ${projectName} AI Router`).replace("`PROFILE_NAME`", `\`${profile}\``).replace("`PROFILE_ROUTE`", `\`${selectedRoute}\``).replace(/\n{3,}/g, "\n\n");
1805
1908
  }
1909
+ function safeLstat(path) {
1910
+ try {
1911
+ return lstatSync5(path);
1912
+ } catch {
1913
+ return void 0;
1914
+ }
1915
+ }
1806
1916
  function readFlag(args, flag) {
1807
1917
  const index = args.indexOf(flag);
1808
1918
  if (index === -1) return null;
@@ -1812,7 +1922,7 @@ function readFlag(args, flag) {
1812
1922
  }
1813
1923
 
1814
1924
  // src/learning/recall.ts
1815
- import { existsSync as existsSync14, readdirSync as readdirSync6, readFileSync as readFileSync9 } from "node:fs";
1925
+ import { existsSync as existsSync13, readdirSync as readdirSync6, readFileSync as readFileSync9 } from "node:fs";
1816
1926
  import { basename as basename4, join as join14, relative as relative5 } from "node:path";
1817
1927
  function recallLearnings(root, options) {
1818
1928
  const query = options.query.trim();
@@ -1837,7 +1947,7 @@ function loadLearningRecords(root) {
1837
1947
  const recordsByTitle = /* @__PURE__ */ new Map();
1838
1948
  for (const relativeDir of ["docs/reference/learnings", "docs/solutions"]) {
1839
1949
  const learningDir = join14(root, relativeDir);
1840
- if (!existsSync14(learningDir)) continue;
1950
+ if (!existsSync13(learningDir)) continue;
1841
1951
  for (const path of listMarkdownFiles(learningDir)) {
1842
1952
  const record = readLearningRecord(root, path);
1843
1953
  const key = record.title.trim().toLowerCase();
@@ -1845,7 +1955,7 @@ function loadLearningRecords(root) {
1845
1955
  }
1846
1956
  }
1847
1957
  const conceptsPath = join14(root, "CONCEPTS.md");
1848
- if (existsSync14(conceptsPath)) {
1958
+ if (existsSync13(conceptsPath)) {
1849
1959
  const record = readLearningRecord(root, conceptsPath);
1850
1960
  recordsByTitle.set(`concepts:${record.title.toLowerCase()}`, record);
1851
1961
  }
@@ -1947,7 +2057,7 @@ function cleanMarkdownLine(input) {
1947
2057
  }
1948
2058
 
1949
2059
  // src/learning/capture.ts
1950
- import { existsSync as existsSync15, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
2060
+ import { existsSync as existsSync14, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
1951
2061
  import { basename as basename5, join as join15, relative as relative6 } from "node:path";
1952
2062
  function captureLearning(root, options) {
1953
2063
  const title = options.title.trim();
@@ -2005,7 +2115,7 @@ function renderLearning(options) {
2005
2115
  function uniquePath(dir, slug) {
2006
2116
  let index = 1;
2007
2117
  let candidate = join15(dir, `${slug}.md`);
2008
- while (existsSync15(candidate)) {
2118
+ while (existsSync14(candidate)) {
2009
2119
  index += 1;
2010
2120
  candidate = join15(dir, `${slug}-${index}.md`);
2011
2121
  }
@@ -2188,10 +2298,10 @@ function printUsage2() {
2188
2298
 
2189
2299
  // src/commands/lens.ts
2190
2300
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "node:fs";
2191
- import { dirname as dirname9 } from "node:path";
2301
+ import { dirname as dirname10 } from "node:path";
2192
2302
 
2193
2303
  // src/lens/audit.ts
2194
- import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
2304
+ import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
2195
2305
  import { basename as basename6, dirname as dirname8, join as join16 } from "node:path";
2196
2306
  var REQUIRED_ARTIFACTS = [
2197
2307
  "manifest.md",
@@ -2223,7 +2333,7 @@ function createProjectLensAuditPackage(targetDir, auditDir) {
2223
2333
  }
2224
2334
  function checkProjectLensAuditPackage(auditDir, options = {}) {
2225
2335
  const contractPath = join16(auditDir, "audit.contract.json");
2226
- if (!existsSync16(contractPath)) {
2336
+ if (!existsSync15(contractPath)) {
2227
2337
  return {
2228
2338
  ok: false,
2229
2339
  auditDir,
@@ -2271,7 +2381,7 @@ function checkProjectLensAuditPackage(auditDir, options = {}) {
2271
2381
  }
2272
2382
  for (const artifactPath of REQUIRED_ARTIFACTS) {
2273
2383
  const absolutePath = join16(auditDir, artifactPath);
2274
- if (!existsSync16(absolutePath)) {
2384
+ if (!existsSync15(absolutePath)) {
2275
2385
  issues.push({ type: "missing-required-artifact", path: artifactPath });
2276
2386
  continue;
2277
2387
  }
@@ -2471,6 +2581,12 @@ function formatProjectLensInspection(report) {
2471
2581
  `excluded-files: ${report.scanScope.excludedFileCount}`,
2472
2582
  `ai-entry-files: ${formatList(report.aiEntryFiles)}`,
2473
2583
  `ai-config-files: ${formatList(report.aiConfigFiles)}`,
2584
+ `ai-host-ssot: ${report.hostSsot.compliant ? "compliant" : "non-compliant"}`,
2585
+ `claude-entry-link: ${formatLink(report.hostSsot.claudeEntry)}`,
2586
+ `claude-skills-link: ${formatLink(report.hostSsot.claudeSkills)}`,
2587
+ `user-host-ssot: ${report.userHostSsot.compliant ? "compliant" : "non-compliant"}`,
2588
+ `verification: ${report.verification.status} (missing: ${formatList(report.verification.missing)})`,
2589
+ `redundancy: ${report.redundancy.status}`,
2474
2590
  `package-scripts: ${formatList(report.packageJson?.scripts ?? [])}`,
2475
2591
  `dependencies: ${formatList(report.packageJson?.dependencies ?? [])}`,
2476
2592
  `dev-dependencies: ${formatList(report.packageJson?.devDependencies ?? [])}`,
@@ -2503,6 +2619,29 @@ function renderProjectLensMarkdownReport(report) {
2503
2619
  "",
2504
2620
  bulletList(report.aiConfigFiles),
2505
2621
  "",
2622
+ "## AI Host SSOT",
2623
+ "",
2624
+ `- Status: ${report.hostSsot.compliant ? "compliant" : "non-compliant"}`,
2625
+ `- Canonical entry: \`${report.hostSsot.agentsEntry.path}\` (${report.hostSsot.agentsEntry.status})`,
2626
+ `- Claude entry: ${formatLink(report.hostSsot.claudeEntry)}`,
2627
+ `- Canonical skills: \`${report.hostSsot.canonicalSkills.path}\` (${report.hostSsot.canonicalSkills.status})`,
2628
+ `- Claude skills: ${formatLink(report.hostSsot.claudeSkills)}`,
2629
+ `- Issues: ${formatList(report.hostSsot.issues)}`,
2630
+ `- User skills SSOT: ${report.userHostSsot.compliant ? "compliant" : "non-compliant"}`,
2631
+ `- User SSOT issues: ${formatList(report.userHostSsot.issues)}`,
2632
+ "",
2633
+ "## Verification Gates",
2634
+ "",
2635
+ `- Status: ${report.verification.status}`,
2636
+ `- Required scripts: ${formatList(report.verification.requiredScripts)}`,
2637
+ `- Missing scripts: ${formatList(report.verification.missing)}`,
2638
+ "",
2639
+ "## Redundancy Evidence",
2640
+ "",
2641
+ `- Status: ${report.redundancy.status}`,
2642
+ `- Legacy directories: ${formatList(report.redundancy.legacyDirectories.map((entry) => entry.path))}`,
2643
+ `- Playwright caches: ${formatList(report.redundancy.playwrightCaches.filter((entry) => entry.exists).map((entry) => `${entry.path} (${entry.bytes} bytes)`))}`,
2644
+ "",
2506
2645
  "## Package",
2507
2646
  "",
2508
2647
  `- Scripts: ${formatList(report.packageJson?.scripts ?? [])}`,
@@ -2539,11 +2678,253 @@ function bulletList(values) {
2539
2678
  if (values.length === 0) return "- none";
2540
2679
  return values.map((value) => `- \`${value}\``).join("\n");
2541
2680
  }
2681
+ function formatLink(link) {
2682
+ const rawTarget = link.rawTarget ? ` -> \`${link.rawTarget}\`` : "";
2683
+ const resolvedTarget = link.resolvedTarget ? `; resolved: \`${link.resolvedTarget}\`` : "";
2684
+ return `\`${link.path}\`${rawTarget} (${link.status}${resolvedTarget})`;
2685
+ }
2542
2686
 
2543
2687
  // src/lens/scan.ts
2544
2688
  import { spawnSync as spawnSync3 } from "node:child_process";
2545
- import { existsSync as existsSync17, readdirSync as readdirSync7, readFileSync as readFileSync11, statSync as statSync3 } from "node:fs";
2546
- import { join as join17, relative as relative7 } from "node:path";
2689
+ import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync12, statSync as statSync4 } from "node:fs";
2690
+ import { homedir as homedir2 } from "node:os";
2691
+ import { join as join20, relative as relative7 } from "node:path";
2692
+
2693
+ // src/host-ssot.ts
2694
+ import {
2695
+ lstatSync as lstatSync6,
2696
+ readlinkSync as readlinkSync3,
2697
+ realpathSync as realpathSync3
2698
+ } from "node:fs";
2699
+ import { dirname as dirname9, isAbsolute as isAbsolute3, join as join17, resolve as resolve3 } from "node:path";
2700
+ function inspectProjectHostSsot(root) {
2701
+ const agentsEntry = inspectCanonicalPath(root, "AGENTS.md");
2702
+ const canonicalSkills = inspectCanonicalPath(root, ".agents/skills");
2703
+ const claudeEntry = inspectCompatibilityLink(root, "CLAUDE.md", "AGENTS.md");
2704
+ const claudeSkills = inspectCompatibilityLink(
2705
+ root,
2706
+ ".claude/skills",
2707
+ "../.agents/skills"
2708
+ );
2709
+ const issues = [
2710
+ ...agentsEntry.status === "file" ? [] : [`AGENTS.md must be the canonical project entry file; found ${agentsEntry.status}.`],
2711
+ ...canonicalSkills.status === "directory" ? [] : [`.agents/skills must be the canonical project skill directory; found ${canonicalSkills.status}.`],
2712
+ ...claudeEntry.compliant ? [] : [`CLAUDE.md must be the relative symlink AGENTS.md; found ${claudeEntry.status}.`],
2713
+ ...claudeSkills.compliant ? [] : [`.claude/skills must be the relative symlink ../.agents/skills; found ${claudeSkills.status}.`]
2714
+ ];
2715
+ return {
2716
+ agentsEntry,
2717
+ canonicalSkills,
2718
+ claudeEntry,
2719
+ claudeSkills,
2720
+ compliant: issues.length === 0,
2721
+ issues
2722
+ };
2723
+ }
2724
+ function inspectUserSkillsSsot(homeDir) {
2725
+ const canonicalSkills = inspectCanonicalPath(homeDir, ".agents/skills");
2726
+ const claudeSkills = inspectCompatibilityLink(
2727
+ homeDir,
2728
+ ".claude/skills",
2729
+ "../.agents/skills"
2730
+ );
2731
+ const issues = [
2732
+ ...canonicalSkills.status === "directory" ? [] : [`~/.agents/skills must be the canonical user skill directory; found ${canonicalSkills.status}.`],
2733
+ ...claudeSkills.compliant ? [] : [`~/.claude/skills must be the relative symlink ../.agents/skills; found ${claudeSkills.status}.`]
2734
+ ];
2735
+ return {
2736
+ canonicalSkills,
2737
+ claudeSkills,
2738
+ compliant: issues.length === 0,
2739
+ issues
2740
+ };
2741
+ }
2742
+ function inspectCanonicalPath(root, path) {
2743
+ const absolutePath = join17(root, path);
2744
+ const stat = safeLstat2(absolutePath);
2745
+ if (!stat) return { path, status: "missing" };
2746
+ if (stat.isSymbolicLink()) {
2747
+ try {
2748
+ realpathSync3(absolutePath);
2749
+ return { path, status: "symlink" };
2750
+ } catch {
2751
+ return { path, status: "dangling-symlink" };
2752
+ }
2753
+ }
2754
+ if (stat.isFile()) return { path, status: "file" };
2755
+ if (stat.isDirectory()) return { path, status: "directory" };
2756
+ return { path, status: "other" };
2757
+ }
2758
+ function inspectCompatibilityLink(root, path, expectedRawTarget) {
2759
+ const absolutePath = join17(root, path);
2760
+ const stat = safeLstat2(absolutePath);
2761
+ const base = { path, expectedRawTarget, compliant: false };
2762
+ if (!stat) return { ...base, status: "missing" };
2763
+ if (!stat.isSymbolicLink()) {
2764
+ if (stat.isFile()) return { ...base, status: "file" };
2765
+ if (stat.isDirectory()) return { ...base, status: "directory" };
2766
+ return { ...base, status: "other" };
2767
+ }
2768
+ const rawTarget = readlinkSync3(absolutePath);
2769
+ const resolvedTarget = resolve3(dirname9(absolutePath), rawTarget);
2770
+ const expectedPath = resolve3(dirname9(absolutePath), expectedRawTarget);
2771
+ const targetStat = safeLstat2(resolvedTarget);
2772
+ if (!targetStat) {
2773
+ return {
2774
+ ...base,
2775
+ rawTarget,
2776
+ resolvedTarget,
2777
+ status: rawTarget === expectedRawTarget ? "target-missing" : "dangling-symlink"
2778
+ };
2779
+ }
2780
+ let targetMatches = false;
2781
+ try {
2782
+ targetMatches = realpathSync3(absolutePath) === realpathSync3(expectedPath);
2783
+ } catch {
2784
+ return { ...base, rawTarget, resolvedTarget, status: "dangling-symlink" };
2785
+ }
2786
+ if (!targetMatches) {
2787
+ return { ...base, rawTarget, resolvedTarget, status: "wrong-target" };
2788
+ }
2789
+ if (isAbsolute3(rawTarget)) {
2790
+ return { ...base, rawTarget, resolvedTarget, status: "absolute-symlink" };
2791
+ }
2792
+ if (rawTarget !== expectedRawTarget) {
2793
+ return {
2794
+ ...base,
2795
+ rawTarget,
2796
+ resolvedTarget,
2797
+ status: "noncanonical-relative-symlink"
2798
+ };
2799
+ }
2800
+ return {
2801
+ ...base,
2802
+ rawTarget,
2803
+ resolvedTarget,
2804
+ status: "compliant-relative-symlink",
2805
+ compliant: true
2806
+ };
2807
+ }
2808
+ function safeLstat2(path) {
2809
+ try {
2810
+ return lstatSync6(path);
2811
+ } catch {
2812
+ return void 0;
2813
+ }
2814
+ }
2815
+
2816
+ // src/portfolio/redundancy.ts
2817
+ import { existsSync as existsSync16, readdirSync as readdirSync7, statSync as statSync3 } from "node:fs";
2818
+ import { homedir } from "node:os";
2819
+ import { join as join18 } from "node:path";
2820
+ var DEFAULT_CACHE_THRESHOLD_BYTES = 1e9;
2821
+ var MAX_CACHE_ENTRIES = 2e4;
2822
+ function inspectProjectRedundancy(root, options = {}) {
2823
+ const legacyDirectories = inspectLegacyDirectories(root);
2824
+ const homeDir = options.homeDir ?? homedir();
2825
+ const cachePaths = getPlaywrightCachePaths(homeDir, options.playwrightBrowsersPath ?? process.env.PLAYWRIGHT_BROWSERS_PATH);
2826
+ const playwrightCaches = cachePaths.map((path) => inspectPlaywrightCache(path));
2827
+ const cacheThresholdBytes = options.cacheThresholdBytes ?? DEFAULT_CACHE_THRESHOLD_BYTES;
2828
+ const status = legacyDirectories.length > 0 || playwrightCaches.some((cache) => cache.exists && (cache.bytes >= cacheThresholdBytes || cache.truncated)) ? "attention" : "clean";
2829
+ return { status, legacyDirectories, playwrightCaches };
2830
+ }
2831
+ function inspectLegacyDirectories(root) {
2832
+ const relativePath = ".agent";
2833
+ const path = join18(root, relativePath);
2834
+ if (!existsSync16(path)) return [];
2835
+ const stats = collectDirectoryStats(path);
2836
+ return [{
2837
+ path: relativePath,
2838
+ kind: "legacy-ai-directory",
2839
+ fileCount: stats.fileCount,
2840
+ bytes: stats.bytes,
2841
+ reason: "\u9879\u76EE\u7EA7\u65E7 AI \u5BBF\u4E3B\u76EE\u5F55\uFF1B\u5E94\u4E0E .agents/skills \u7684 SSOT \u9010\u9879\u6BD4\u5BF9\u540E\u518D\u51B3\u5B9A\u662F\u5426\u8FC1\u79FB\uFF0C\u626B\u63CF\u5668\u4E0D\u81EA\u52A8\u5220\u9664\u3002"
2842
+ }];
2843
+ }
2844
+ function getPlaywrightCachePaths(homeDir, configuredPath) {
2845
+ const candidates = [
2846
+ configuredPath && configuredPath !== "0" ? configuredPath : void 0,
2847
+ join18(homeDir, "Library/Caches/ms-playwright"),
2848
+ join18(homeDir, ".cache/ms-playwright"),
2849
+ join18(homeDir, "AppData/Local/ms-playwright")
2850
+ ].filter((path) => Boolean(path));
2851
+ return [...new Set(candidates)];
2852
+ }
2853
+ function inspectPlaywrightCache(path) {
2854
+ if (!existsSync16(path)) {
2855
+ return { path, exists: false, fileCount: 0, bytes: 0, revisionCount: 0, truncated: false };
2856
+ }
2857
+ const stats = collectDirectoryStats(path);
2858
+ let revisionCount = 0;
2859
+ try {
2860
+ revisionCount = readdirSync7(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).length;
2861
+ } catch {
2862
+ revisionCount = 0;
2863
+ }
2864
+ return { path, exists: true, fileCount: stats.fileCount, bytes: stats.bytes, revisionCount, truncated: stats.truncated };
2865
+ }
2866
+ function collectDirectoryStats(root) {
2867
+ let fileCount = 0;
2868
+ let bytes = 0;
2869
+ let truncated = false;
2870
+ const pending = [root];
2871
+ while (pending.length > 0) {
2872
+ const current = pending.pop();
2873
+ if (!current) continue;
2874
+ let entries;
2875
+ try {
2876
+ entries = readdirSync7(current, { withFileTypes: true });
2877
+ } catch {
2878
+ continue;
2879
+ }
2880
+ for (const entry of entries) {
2881
+ if (fileCount >= MAX_CACHE_ENTRIES) {
2882
+ truncated = true;
2883
+ break;
2884
+ }
2885
+ const path = join18(current, entry.name);
2886
+ if (entry.isDirectory()) {
2887
+ pending.push(path);
2888
+ } else if (entry.isFile()) {
2889
+ fileCount += 1;
2890
+ try {
2891
+ bytes += statSync3(path).size;
2892
+ } catch {
2893
+ }
2894
+ }
2895
+ }
2896
+ if (truncated) break;
2897
+ }
2898
+ return { fileCount, bytes, truncated };
2899
+ }
2900
+
2901
+ // src/portfolio/verification.ts
2902
+ import { existsSync as existsSync17, readFileSync as readFileSync11 } from "node:fs";
2903
+ import { join as join19 } from "node:path";
2904
+ var REQUIRED_PROJECT_SCRIPTS = ["typecheck", "lint", "format:check", "verify"];
2905
+ function inspectProjectVerification(root) {
2906
+ const packageJson = readPackageJson(join19(root, "package.json"));
2907
+ const scripts = Object.fromEntries(
2908
+ REQUIRED_PROJECT_SCRIPTS.map((name) => [name, typeof packageJson?.scripts?.[name] === "string" ? packageJson.scripts[name] : void 0])
2909
+ );
2910
+ const missing = REQUIRED_PROJECT_SCRIPTS.filter((name) => typeof scripts[name] !== "string" || scripts[name]?.trim().length === 0);
2911
+ return {
2912
+ status: packageJson ? missing.length === 0 ? "compliant" : "attention" : "unknown",
2913
+ requiredScripts: REQUIRED_PROJECT_SCRIPTS,
2914
+ scripts,
2915
+ missing
2916
+ };
2917
+ }
2918
+ function readPackageJson(path) {
2919
+ if (!existsSync17(path)) return void 0;
2920
+ try {
2921
+ return JSON.parse(readFileSync11(path, "utf8"));
2922
+ } catch {
2923
+ return void 0;
2924
+ }
2925
+ }
2926
+
2927
+ // src/lens/scan.ts
2547
2928
  var ignoredDirectories = /* @__PURE__ */ new Set([
2548
2929
  ".git",
2549
2930
  ".next",
@@ -2559,7 +2940,7 @@ function scanProjectLensTarget(targetDir, options = {}) {
2559
2940
  const markdownFiles = files.filter(
2560
2941
  (file) => file.startsWith("docs/") && file.endsWith(".md")
2561
2942
  );
2562
- const packageJson = readPackageJson(targetDir);
2943
+ const packageJson = readPackageJson2(targetDir);
2563
2944
  return {
2564
2945
  targetDir,
2565
2946
  scanScope: {
@@ -2569,24 +2950,31 @@ function scanProjectLensTarget(targetDir, options = {}) {
2569
2950
  excludedFileCount: candidateFiles.length - files.length
2570
2951
  },
2571
2952
  aiEntryFiles: ["AGENTS.md", "CLAUDE.md"].filter(
2572
- (file) => existsSync17(join17(targetDir, file))
2953
+ (file) => existsSync18(join20(targetDir, file))
2573
2954
  ),
2574
2955
  aiConfigFiles: [],
2956
+ hostSsot: inspectProjectHostSsot(targetDir),
2957
+ userHostSsot: inspectUserSkillsSsot(options.homeDir ?? process.env.HOME ?? homedir2()),
2958
+ verification: inspectProjectVerification(targetDir),
2959
+ redundancy: inspectProjectRedundancy(targetDir, {
2960
+ homeDir: options.homeDir,
2961
+ playwrightBrowsersPath: options.playwrightBrowsersPath
2962
+ }),
2575
2963
  packageJson,
2576
2964
  docs: {
2577
- hasDocsDirectory: existsSync17(join17(targetDir, "docs")),
2965
+ hasDocsDirectory: existsSync18(join20(targetDir, "docs")),
2578
2966
  markdownFileCount: markdownFiles.length,
2579
2967
  governanceFiles: markdownFiles.filter((file) => file.startsWith("docs/governance/") || file.startsWith("docs/policy/")).sort()
2580
2968
  },
2581
2969
  git: readGitState(targetDir),
2582
- largeFiles: files.map((file) => ({ path: file, bytes: statSync3(join17(targetDir, file)).size })).filter((file) => file.bytes >= largeFileBytes).sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path)).slice(0, 25)
2970
+ largeFiles: files.map((file) => ({ path: file, bytes: statSync4(join20(targetDir, file)).size })).filter((file) => file.bytes >= largeFileBytes).sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path)).slice(0, 25)
2583
2971
  };
2584
2972
  }
2585
- function readPackageJson(targetDir) {
2586
- const packageJsonPath = join17(targetDir, "package.json");
2587
- if (!existsSync17(packageJsonPath)) return void 0;
2973
+ function readPackageJson2(targetDir) {
2974
+ const packageJsonPath = join20(targetDir, "package.json");
2975
+ if (!existsSync18(packageJsonPath)) return void 0;
2588
2976
  try {
2589
- const packageJson = JSON.parse(readFileSync11(packageJsonPath, "utf8"));
2977
+ const packageJson = JSON.parse(readFileSync12(packageJsonPath, "utf8"));
2590
2978
  return {
2591
2979
  scripts: Object.keys(packageJson.scripts ?? {}).sort(),
2592
2980
  dependencies: Object.keys(packageJson.dependencies ?? {}).sort(),
@@ -2624,7 +3012,7 @@ function listProjectFiles(targetDir) {
2624
3012
  "-z"
2625
3013
  ]);
2626
3014
  if (gitFiles.ok) {
2627
- return gitFiles.stdout.split("\0").filter(Boolean).map(toUnixPath4).filter((file) => existsSync17(join17(targetDir, file))).sort();
3015
+ return gitFiles.stdout.split("\0").filter(Boolean).map(toUnixPath4).filter((file) => existsSync18(join20(targetDir, file))).sort();
2628
3016
  }
2629
3017
  const files = [];
2630
3018
  collectFiles2(targetDir, targetDir, files);
@@ -2642,13 +3030,13 @@ function isFirstPartyEvidenceFile(file) {
2642
3030
  return !excludedEvidencePrefixes.some((prefix) => file.startsWith(prefix));
2643
3031
  }
2644
3032
  function collectFiles2(rootDir, currentDir, files) {
2645
- if (!existsSync17(currentDir)) return;
2646
- for (const entry of readdirSync7(currentDir, { withFileTypes: true })) {
3033
+ if (!existsSync18(currentDir)) return;
3034
+ for (const entry of readdirSync8(currentDir, { withFileTypes: true })) {
2647
3035
  if (entry.isDirectory()) {
2648
3036
  if (ignoredDirectories.has(entry.name)) continue;
2649
- collectFiles2(rootDir, join17(currentDir, entry.name), files);
3037
+ collectFiles2(rootDir, join20(currentDir, entry.name), files);
2650
3038
  } else if (entry.isFile()) {
2651
- files.push(toUnixPath4(relative7(rootDir, join17(currentDir, entry.name))));
3039
+ files.push(toUnixPath4(relative7(rootDir, join20(currentDir, entry.name))));
2652
3040
  }
2653
3041
  }
2654
3042
  }
@@ -2700,7 +3088,7 @@ function runLensReport(args) {
2700
3088
  }
2701
3089
  const report = scanProjectLensTarget(options.value.targetDir);
2702
3090
  const markdown = renderProjectLensMarkdownReport(report);
2703
- mkdirSync7(dirname9(options.value.outPath), { recursive: true });
3091
+ mkdirSync7(dirname10(options.value.outPath), { recursive: true });
2704
3092
  writeFileSync6(options.value.outPath, markdown);
2705
3093
  console.log(`report: ${options.value.outPath}`);
2706
3094
  return 0;
@@ -2804,16 +3192,16 @@ function printUsage3() {
2804
3192
  }
2805
3193
 
2806
3194
  // src/commands/portfolio.ts
2807
- import { existsSync as existsSync22 } from "node:fs";
2808
- import { join as join21 } from "node:path";
3195
+ import { existsSync as existsSync24, readFileSync as readFileSync18 } from "node:fs";
3196
+ import { join as join25 } from "node:path";
2809
3197
 
2810
3198
  // src/portfolio/manifest.ts
2811
- import { existsSync as existsSync18, readFileSync as readFileSync12 } from "node:fs";
2812
- import { dirname as dirname10, isAbsolute as isAbsolute3, resolve as resolve3 } from "node:path";
3199
+ import { existsSync as existsSync19, readFileSync as readFileSync13 } from "node:fs";
3200
+ import { dirname as dirname11, isAbsolute as isAbsolute4, resolve as resolve4 } from "node:path";
2813
3201
  function loadPortfolioManifest(configPath) {
2814
3202
  let parsed;
2815
3203
  try {
2816
- parsed = JSON.parse(readFileSync12(configPath, "utf8"));
3204
+ parsed = JSON.parse(readFileSync13(configPath, "utf8"));
2817
3205
  } catch (error) {
2818
3206
  return {
2819
3207
  configPath,
@@ -2825,7 +3213,7 @@ function loadPortfolioManifest(configPath) {
2825
3213
  ]
2826
3214
  };
2827
3215
  }
2828
- const normalized = resolveManifestPaths(parsed, dirname10(resolve3(configPath)));
3216
+ const normalized = resolveManifestPaths(parsed, dirname11(resolve4(configPath)));
2829
3217
  const issues = validatePortfolioManifest(normalized);
2830
3218
  return {
2831
3219
  configPath,
@@ -2836,14 +3224,14 @@ function loadPortfolioManifest(configPath) {
2836
3224
  function resolveManifestPaths(value, configDir) {
2837
3225
  if (!isRecord(value)) return value;
2838
3226
  const resolveEndpoint = (endpoint) => {
2839
- if (!isRecord(endpoint) || typeof endpoint.path !== "string" || isAbsolute3(endpoint.path)) {
3227
+ if (!isRecord(endpoint) || typeof endpoint.path !== "string" || isAbsolute4(endpoint.path)) {
2840
3228
  return endpoint;
2841
3229
  }
2842
- return { ...endpoint, path: resolve3(configDir, endpoint.path) };
3230
+ return { ...endpoint, path: resolve4(configDir, endpoint.path) };
2843
3231
  };
2844
3232
  return {
2845
3233
  ...value,
2846
- technologyGovernance: isRecord(value.technologyGovernance) && typeof value.technologyGovernance.strategySource === "string" && !isAbsolute3(value.technologyGovernance.strategySource) ? { ...value.technologyGovernance, strategySource: resolve3(configDir, value.technologyGovernance.strategySource) } : value.technologyGovernance,
3234
+ technologyGovernance: isRecord(value.technologyGovernance) && typeof value.technologyGovernance.strategySource === "string" && !isAbsolute4(value.technologyGovernance.strategySource) ? { ...value.technologyGovernance, strategySource: resolve4(configDir, value.technologyGovernance.strategySource) } : value.technologyGovernance,
2847
3235
  controlPlane: resolveEndpoint(value.controlPlane),
2848
3236
  executionEngine: resolveEndpoint(value.executionEngine),
2849
3237
  targets: Array.isArray(value.targets) ? value.targets.map(resolveEndpoint) : value.targets
@@ -2877,13 +3265,14 @@ function validatePortfolioManifest(value) {
2877
3265
  validateAllowedFields(
2878
3266
  value,
2879
3267
  "root",
2880
- ["schemaVersion", "portfolioId", "technologyGovernance", "controlPlane", "executionEngine", "hostTooling", "targets"],
3268
+ ["schemaVersion", "portfolioId", "technologyGovernance", "controlPlane", "executionEngine", "hostTooling", "specialistChecks", "targets"],
2881
3269
  issues
2882
3270
  );
2883
3271
  const technologyCatalog = validateTechnologyGovernance(value.technologyGovernance, issues);
2884
3272
  validateEndpoint(value.controlPlane, "controlPlane", issues, technologyCatalog);
2885
3273
  validateEndpoint(value.executionEngine, "executionEngine", issues, technologyCatalog);
2886
3274
  validateHostTooling(value.hostTooling, issues);
3275
+ validateSpecialistChecks(value.specialistChecks, issues);
2887
3276
  if (!Array.isArray(value.targets)) {
2888
3277
  issues.push({
2889
3278
  type: "invalid-field",
@@ -2981,7 +3370,7 @@ function validateEndpoint(value, field, issues, technologyCatalog) {
2981
3370
  });
2982
3371
  return;
2983
3372
  }
2984
- if (!existsSync18(value.path)) {
3373
+ if (!existsSync19(value.path)) {
2985
3374
  issues.push({
2986
3375
  type: "missing-path",
2987
3376
  id: typeof value.id === "string" ? value.id : void 0,
@@ -3010,7 +3399,7 @@ function validateTechnologyGovernance(value, issues) {
3010
3399
  issues.push({ type: "invalid-field", field: "technologyGovernance", message: "Portfolio technologyGovernance must be an object." });
3011
3400
  return { technologies, projectTypes };
3012
3401
  }
3013
- validateAllowedFields(value, "technologyGovernance", ["strategySource", "technologies", "projectTypes"], issues);
3402
+ validateAllowedFields(value, "technologyGovernance", ["strategySource", "versionPolicy", "technologies", "projectTypes"], issues);
3014
3403
  if (value.strategySource !== void 0 && (typeof value.strategySource !== "string" || value.strategySource.length === 0)) {
3015
3404
  issues.push({ type: "invalid-field", field: "technologyGovernance.strategySource", message: "Technology strategySource must be a non-empty string." });
3016
3405
  }
@@ -3073,16 +3462,76 @@ function validateTechnologyGovernance(value, issues) {
3073
3462
  if (!technologies.has(technology)) issues.push({ type: "invalid-field", field: "technologyGovernance.projectTypes", message: `Project type ${projectType.id} references unknown technology: ${technology}` });
3074
3463
  }
3075
3464
  }
3465
+ validateVersionPolicy(value.versionPolicy, projectTypes, issues);
3076
3466
  return { technologies, projectTypes };
3077
3467
  }
3468
+ function validateVersionPolicy(value, projectTypes, issues) {
3469
+ if (value === void 0) return;
3470
+ if (!isRecord(value)) {
3471
+ issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy", message: "Technology versionPolicy must be an object." });
3472
+ return;
3473
+ }
3474
+ validateAllowedFields(value, "versionPolicy", ["schemaVersion", "packageManager", "runtime", "packages"], issues);
3475
+ if (value.schemaVersion !== 1) {
3476
+ issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.schemaVersion", message: "Technology versionPolicy schemaVersion must be 1." });
3477
+ }
3478
+ validateVersionRequirement(value.packageManager, "technologyGovernance.versionPolicy.packageManager", issues);
3479
+ validateVersionRequirement(value.runtime, "technologyGovernance.versionPolicy.runtime", issues);
3480
+ if (!Array.isArray(value.packages)) {
3481
+ issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.packages", message: "Technology versionPolicy packages must be an array." });
3482
+ return;
3483
+ }
3484
+ const seen = /* @__PURE__ */ new Set();
3485
+ for (const entry of value.packages) {
3486
+ if (!isRecord(entry)) {
3487
+ issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.packages", message: "Version policy package entries must be objects." });
3488
+ continue;
3489
+ }
3490
+ validateAllowedFields(entry, "versionPolicy.package", ["name", "version", "appliesTo"], issues);
3491
+ const name = typeof entry.name === "string" ? entry.name : "";
3492
+ if (!name || seen.has(name)) {
3493
+ issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.packages.name", message: `Version policy package name must be non-empty and unique: ${String(entry.name)}` });
3494
+ } else seen.add(name);
3495
+ if (typeof entry.version !== "string" || !isExactVersion(entry.version)) {
3496
+ issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.packages.version", message: `Version policy package version must be exact semver: ${String(entry.version)}` });
3497
+ }
3498
+ if (entry.appliesTo !== void 0) {
3499
+ validateOptionalStringArray(entry.appliesTo, name, "technologyGovernance.versionPolicy.packages.appliesTo", issues);
3500
+ if (Array.isArray(entry.appliesTo)) {
3501
+ for (const projectType of entry.appliesTo) {
3502
+ if (typeof projectType === "string" && !projectTypes.has(projectType)) {
3503
+ issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.packages.appliesTo", message: `Version policy references unknown project type: ${projectType}` });
3504
+ }
3505
+ }
3506
+ }
3507
+ }
3508
+ }
3509
+ }
3510
+ function validateVersionRequirement(value, field, issues) {
3511
+ if (value === void 0) return;
3512
+ if (!isRecord(value)) {
3513
+ issues.push({ type: "invalid-field", field, message: "Version requirement must be an object." });
3514
+ return;
3515
+ }
3516
+ validateAllowedFields(value, field, ["name", "version"], issues);
3517
+ if (typeof value.name !== "string" || value.name.length === 0) {
3518
+ issues.push({ type: "invalid-field", field: `${field}.name`, message: "Version requirement name must be non-empty." });
3519
+ }
3520
+ if (typeof value.version !== "string" || !isExactVersion(value.version)) {
3521
+ issues.push({ type: "invalid-field", field: `${field}.version`, message: `Version requirement version must be exact semver: ${String(value.version)}` });
3522
+ }
3523
+ }
3524
+ function isExactVersion(value) {
3525
+ return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value);
3526
+ }
3078
3527
  function isRepositoryRelativePath(value) {
3079
- if (value.length === 0 || isAbsolute3(value)) return false;
3528
+ if (value.length === 0 || isAbsolute4(value)) return false;
3080
3529
  const segments = value.replaceAll("\\", "/").split("/");
3081
3530
  return !segments.includes("..");
3082
3531
  }
3083
3532
  function isExactRepositoryRelativePath(value) {
3084
3533
  if (value.length === 0) return false;
3085
- if (isAbsolute3(value)) return false;
3534
+ if (isAbsolute4(value)) return false;
3086
3535
  if (/^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\") || value.startsWith("//")) return false;
3087
3536
  if (value.includes("\\")) return false;
3088
3537
  const segments = value.split("/");
@@ -3214,15 +3663,45 @@ function validateHostTooling(value, issues) {
3214
3663
  }
3215
3664
  }
3216
3665
  }
3666
+ function validateSpecialistChecks(value, issues) {
3667
+ if (value === void 0) return;
3668
+ if (!isRecord(value)) {
3669
+ issues.push({
3670
+ type: "invalid-field",
3671
+ field: "specialistChecks",
3672
+ message: "Portfolio specialistChecks must be an object."
3673
+ });
3674
+ return;
3675
+ }
3676
+ validateAllowedFields(value, "specialistChecks", ["devspace"], issues);
3677
+ if (value.devspace === void 0) return;
3678
+ if (!isRecord(value.devspace)) {
3679
+ issues.push({
3680
+ type: "invalid-field",
3681
+ field: "specialistChecks.devspace",
3682
+ message: "Portfolio specialistChecks.devspace must be an object."
3683
+ });
3684
+ return;
3685
+ }
3686
+ validateAllowedFields(value.devspace, "specialistChecks.devspace", ["expectedToolMode"], issues);
3687
+ if (value.devspace.expectedToolMode !== void 0 && !["minimal", "full", "codex"].includes(String(value.devspace.expectedToolMode))) {
3688
+ issues.push({
3689
+ type: "invalid-field",
3690
+ field: "specialistChecks.devspace.expectedToolMode",
3691
+ message: "DevSpace expectedToolMode must be minimal, full, or codex."
3692
+ });
3693
+ }
3694
+ }
3217
3695
  function isRecord(value) {
3218
3696
  return typeof value === "object" && value !== null && !Array.isArray(value);
3219
3697
  }
3220
3698
 
3221
3699
  // src/portfolio/doctor.ts
3222
3700
  import { spawnSync as spawnSync5 } from "node:child_process";
3223
- import { existsSync as existsSync20, readFileSync as readFileSync14 } from "node:fs";
3701
+ import { existsSync as existsSync22, readFileSync as readFileSync16 } from "node:fs";
3224
3702
  import { createRequire as createRequire2 } from "node:module";
3225
- import { dirname as dirname11, join as join19 } from "node:path";
3703
+ import { homedir as homedir3 } from "node:os";
3704
+ import { dirname as dirname12, join as join23 } from "node:path";
3226
3705
  import { fileURLToPath as fileURLToPath3 } from "node:url";
3227
3706
 
3228
3707
  // src/host-tooling/inventory.ts
@@ -3313,13 +3792,13 @@ function isRecord2(value) {
3313
3792
  }
3314
3793
 
3315
3794
  // src/portfolio/asset-state.ts
3316
- import { existsSync as existsSync19, lstatSync as lstatSync5, readFileSync as readFileSync13 } from "node:fs";
3317
- import { join as join18 } from "node:path";
3795
+ import { existsSync as existsSync20, lstatSync as lstatSync7, readFileSync as readFileSync14 } from "node:fs";
3796
+ import { join as join21 } from "node:path";
3318
3797
  function comparePortfolioAssetState(options) {
3319
3798
  const expectedManifest = readPlanDocument(options.expectedPlan, ".pro-gov/assets.json");
3320
3799
  const expectedLock = readPlanDocument(options.expectedPlan, ".pro-gov/assets.lock.json");
3321
- const currentManifest = readJsonFile(join18(options.targetDir, ".pro-gov/assets.json"));
3322
- const currentLock = readJsonFile(join18(options.targetDir, ".pro-gov/assets.lock.json"));
3800
+ const currentManifest = readJsonFile(join21(options.targetDir, ".pro-gov/assets.json"));
3801
+ const currentLock = readJsonFile(join21(options.targetDir, ".pro-gov/assets.lock.json"));
3323
3802
  const issues = [];
3324
3803
  if (!sameStrings(currentManifest?.bundleIds, expectedManifest?.bundleIds)) {
3325
3804
  issues.push({
@@ -3342,7 +3821,8 @@ function comparePortfolioAssetState(options) {
3342
3821
  const expectedTargets = new Set((expectedLock?.assets ?? []).map((entry) => entry.targetPath));
3343
3822
  for (const entry of currentLock?.assets ?? []) {
3344
3823
  if (expectedTargets.has(entry.targetPath)) continue;
3345
- const targetAbsolutePath = join18(options.targetDir, entry.targetPath);
3824
+ if (options.expectedPlan.actions.some((action) => action.type === "adopt-symlink" && action.assetId === entry.id && action.legacyTargetPath === entry.targetPath)) continue;
3825
+ const targetAbsolutePath = join21(options.targetDir, entry.targetPath);
3346
3826
  if (!pathIsSymlink(targetAbsolutePath)) continue;
3347
3827
  issues.push({
3348
3828
  type: "orphaned-managed-symlink",
@@ -3364,9 +3844,9 @@ function readPlanDocument(plan, targetPath) {
3364
3844
  }
3365
3845
  }
3366
3846
  function readJsonFile(path) {
3367
- if (!existsSync19(path)) return void 0;
3847
+ if (!existsSync20(path)) return void 0;
3368
3848
  try {
3369
- return JSON.parse(readFileSync13(path, "utf8"));
3849
+ return JSON.parse(readFileSync14(path, "utf8"));
3370
3850
  } catch {
3371
3851
  return void 0;
3372
3852
  }
@@ -3387,12 +3867,86 @@ function normalizeLock(lock) {
3387
3867
  }
3388
3868
  function pathIsSymlink(path) {
3389
3869
  try {
3390
- return lstatSync5(path).isSymbolicLink();
3870
+ return lstatSync7(path).isSymbolicLink();
3391
3871
  } catch {
3392
3872
  return false;
3393
3873
  }
3394
3874
  }
3395
3875
 
3876
+ // src/portfolio/version-policy.ts
3877
+ import { existsSync as existsSync21, readFileSync as readFileSync15 } from "node:fs";
3878
+ import { join as join22 } from "node:path";
3879
+ function inspectVersionPolicy(root, policy, projectType) {
3880
+ if (!policy) return { status: "compliant", packages: [], attentionCount: 0 };
3881
+ const packageJson = readJson2(join22(root, "package.json"));
3882
+ const packageManager = policy.packageManager ? inspectPackageManager(packageJson, policy.packageManager.name, policy.packageManager.version) : void 0;
3883
+ const runtime = policy.runtime ? inspectRuntime(policy.runtime.name, policy.runtime.version) : void 0;
3884
+ const packages = policy.packages.map((requirement) => {
3885
+ if (requirement.appliesTo && (!projectType || !requirement.appliesTo.includes(projectType))) {
3886
+ return { name: requirement.name, expected: requirement.version, status: "not-applicable" };
3887
+ }
3888
+ const declared = findDeclaredVersion(packageJson, requirement.name);
3889
+ const installed = readInstalledVersion(root, requirement.name);
3890
+ const status = declared === void 0 ? requirement.appliesTo ? "missing" : "not-applicable" : declared !== requirement.version ? "drift" : installed !== requirement.version ? installed === void 0 ? "missing" : "drift" : "compliant";
3891
+ return {
3892
+ name: requirement.name,
3893
+ expected: requirement.version,
3894
+ declared,
3895
+ installed,
3896
+ actual: installed,
3897
+ status
3898
+ };
3899
+ });
3900
+ const all = [packageManager, runtime, ...packages].filter((item) => item !== void 0);
3901
+ const attentionCount = all.filter((item) => item.status !== "compliant" && item.status !== "not-applicable").length;
3902
+ return {
3903
+ status: attentionCount === 0 ? "compliant" : "attention",
3904
+ packageManager,
3905
+ runtime,
3906
+ packages,
3907
+ attentionCount
3908
+ };
3909
+ }
3910
+ function inspectPackageManager(packageJson, expectedName, expectedVersion) {
3911
+ const declared = typeof packageJson?.packageManager === "string" ? packageJson.packageManager : void 0;
3912
+ const expected = `${expectedName}@${expectedVersion}`;
3913
+ return {
3914
+ name: expectedName,
3915
+ expected: expectedVersion,
3916
+ actual: declared,
3917
+ declared,
3918
+ status: declared === void 0 ? "missing" : declared === expected ? "compliant" : "drift"
3919
+ };
3920
+ }
3921
+ function inspectRuntime(expectedName, expectedVersion) {
3922
+ const actual = expectedName === "node" ? process.versions.node : void 0;
3923
+ return {
3924
+ name: expectedName,
3925
+ expected: expectedVersion,
3926
+ actual,
3927
+ status: actual === void 0 ? "missing" : actual === expectedVersion ? "compliant" : "drift"
3928
+ };
3929
+ }
3930
+ function findDeclaredVersion(packageJson, name) {
3931
+ for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
3932
+ const value = packageJson?.[section]?.[name];
3933
+ if (typeof value === "string") return value;
3934
+ }
3935
+ return void 0;
3936
+ }
3937
+ function readInstalledVersion(root, name) {
3938
+ const packageJson = readJson2(join22(root, "node_modules", name, "package.json"));
3939
+ return typeof packageJson?.version === "string" ? packageJson.version : void 0;
3940
+ }
3941
+ function readJson2(path) {
3942
+ if (!existsSync21(path)) return void 0;
3943
+ try {
3944
+ return JSON.parse(readFileSync15(path, "utf8"));
3945
+ } catch {
3946
+ return void 0;
3947
+ }
3948
+ }
3949
+
3396
3950
  // src/portfolio/doctor.ts
3397
3951
  function inspectPortfolio(options) {
3398
3952
  const expectedPackageVersions = getExpectedPackageVersions();
@@ -3402,24 +3956,27 @@ function inspectPortfolio(options) {
3402
3956
  agentAssetsDir: options.agentAssetsDir,
3403
3957
  registry: options.registry,
3404
3958
  bundles: options.bundles,
3405
- expectedPackageVersions
3959
+ expectedPackageVersions,
3960
+ versionPolicy: options.manifest.technologyGovernance?.versionPolicy
3406
3961
  }));
3407
3962
  return {
3408
3963
  ok: hostTooling.issues.length === 0 && targets.every((target) => target.issues.length === 0),
3409
3964
  portfolioId: options.manifest.portfolioId,
3410
3965
  expectedPackageVersions,
3411
3966
  hostTooling,
3967
+ hostSsot: { userSkills: inspectUserSkillsSsot(options.homeDir ?? homedir3()) },
3412
3968
  targets
3413
3969
  };
3414
3970
  }
3415
3971
  function inspectTarget(options) {
3416
3972
  const { target } = options;
3973
+ const hostSsot = inspectProjectHostSsot(target.path);
3417
3974
  const issues = [];
3418
- const packageJson = readJson2(join19(target.path, "package.json"));
3975
+ const packageJson = readJson3(join23(target.path, "package.json"));
3419
3976
  const packages = {};
3420
3977
  for (const packageName of ["@pieai/pro-gov", "@pieai/doc-gov"]) {
3421
3978
  const declared = packageJson?.devDependencies?.[packageName] ?? packageJson?.dependencies?.[packageName];
3422
- const installedPackage = readJson2(join19(target.path, "node_modules", packageName, "package.json"));
3979
+ const installedPackage = readJson3(join23(target.path, "node_modules", packageName, "package.json"));
3423
3980
  const installed = installedPackage?.version;
3424
3981
  const expected = options.expectedPackageVersions[packageName];
3425
3982
  packages[packageName] = { declared, installed, expected };
@@ -3439,6 +3996,8 @@ function inspectTarget(options) {
3439
3996
  }
3440
3997
  }
3441
3998
  const checks = runTargetChecks(target);
3999
+ const versions = inspectVersionPolicy(target.path, options.versionPolicy, target.projectType);
4000
+ const verification = inspectProjectVerification(target.path);
3442
4001
  for (const check of checks) {
3443
4002
  if (check.status === 0) continue;
3444
4003
  issues.push({
@@ -3473,7 +4032,7 @@ function inspectTarget(options) {
3473
4032
  type: "asset-lock-drift",
3474
4033
  message: error instanceof Error ? error.message : String(error)
3475
4034
  });
3476
- if (!existsSync20(join19(target.path, ".pro-gov/assets.json"))) {
4035
+ if (!existsSync22(join23(target.path, ".pro-gov/assets.json"))) {
3477
4036
  issues.push({ type: "bundle-drift", message: "Target asset manifest is missing." });
3478
4037
  }
3479
4038
  }
@@ -3484,19 +4043,22 @@ function inspectTarget(options) {
3484
4043
  packages,
3485
4044
  git: inspectGit(target.path),
3486
4045
  checks,
4046
+ hostSsot,
4047
+ versions,
4048
+ verification,
3487
4049
  issues: deduplicateIssues(issues)
3488
4050
  };
3489
4051
  }
3490
4052
  function readTargetAssetHost(targetDir) {
3491
- const lockfile = readJson2(join19(targetDir, ".pro-gov/assets.lock.json"));
4053
+ const lockfile = readJson3(join23(targetDir, ".pro-gov/assets.lock.json"));
3492
4054
  return isAssetRegistryHost(lockfile?.host) ? lockfile.host : void 0;
3493
4055
  }
3494
4056
  function isAssetRegistryHost(value) {
3495
4057
  return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
3496
4058
  }
3497
4059
  function runTargetChecks(target) {
3498
- const proGovCli = join19(target.path, "node_modules/@pieai/pro-gov/dist/cli.js");
3499
- const docGovCli = join19(target.path, "node_modules/@pieai/doc-gov/dist/cli.js");
4060
+ const proGovCli = join23(target.path, "node_modules/@pieai/pro-gov/dist/cli.js");
4061
+ const docGovCli = join23(target.path, "node_modules/@pieai/doc-gov/dist/cli.js");
3500
4062
  const commands = [
3501
4063
  {
3502
4064
  name: "pro-gov doctor",
@@ -3507,7 +4069,7 @@ function runTargetChecks(target) {
3507
4069
  { name: "doc-gov scan --check", cli: docGovCli, args: ["scan", "--check"] }
3508
4070
  ];
3509
4071
  return commands.map((command2) => {
3510
- if (!existsSync20(command2.cli)) return { name: command2.name, status: null };
4072
+ if (!existsSync22(command2.cli)) return { name: command2.name, status: null };
3511
4073
  const result = spawnSync5(process.execPath, [command2.cli, ...command2.args], {
3512
4074
  cwd: target.path,
3513
4075
  encoding: "utf8",
@@ -3531,11 +4093,11 @@ function inspectGit(path) {
3531
4093
  };
3532
4094
  }
3533
4095
  function getExpectedPackageVersions() {
3534
- const proGovPackage = readJson2(findOwnPackageJson());
4096
+ const proGovPackage = readJson3(findOwnPackageJson());
3535
4097
  let docGovVersion;
3536
4098
  try {
3537
4099
  const require2 = createRequire2(import.meta.url);
3538
- const docGovPackage = readJson2(require2.resolve("@pieai/doc-gov/package.json"));
4100
+ const docGovPackage = readJson3(require2.resolve("@pieai/doc-gov/package.json"));
3539
4101
  docGovVersion = docGovPackage?.version;
3540
4102
  } catch {
3541
4103
  docGovVersion = void 0;
@@ -3546,18 +4108,18 @@ function getExpectedPackageVersions() {
3546
4108
  };
3547
4109
  }
3548
4110
  function findOwnPackageJson() {
3549
- let current = dirname11(fileURLToPath3(import.meta.url));
4111
+ let current = dirname12(fileURLToPath3(import.meta.url));
3550
4112
  for (let depth = 0; depth < 5; depth += 1) {
3551
- const candidate = join19(current, "package.json");
3552
- if (existsSync20(candidate)) return candidate;
3553
- current = dirname11(current);
4113
+ const candidate = join23(current, "package.json");
4114
+ if (existsSync22(candidate)) return candidate;
4115
+ current = dirname12(current);
3554
4116
  }
3555
4117
  return "";
3556
4118
  }
3557
- function readJson2(path) {
3558
- if (!path || !existsSync20(path)) return void 0;
4119
+ function readJson3(path) {
4120
+ if (!path || !existsSync22(path)) return void 0;
3559
4121
  try {
3560
- return JSON.parse(readFileSync14(path, "utf8"));
4122
+ return JSON.parse(readFileSync16(path, "utf8"));
3561
4123
  } catch {
3562
4124
  return void 0;
3563
4125
  }
@@ -3576,26 +4138,31 @@ function deduplicateIssues(issues) {
3576
4138
  import { execFileSync } from "node:child_process";
3577
4139
  import {
3578
4140
  cpSync as cpSync2,
3579
- existsSync as existsSync21,
3580
- lstatSync as lstatSync6,
4141
+ existsSync as existsSync23,
4142
+ lstatSync as lstatSync8,
3581
4143
  mkdirSync as mkdirSync8,
3582
- readFileSync as readFileSync15,
3583
- readdirSync as readdirSync8,
3584
- realpathSync as realpathSync2,
3585
- statSync as statSync4,
4144
+ readFileSync as readFileSync17,
4145
+ readdirSync as readdirSync9,
4146
+ realpathSync as realpathSync4,
4147
+ statSync as statSync5,
3586
4148
  writeFileSync as writeFileSync7
3587
4149
  } from "node:fs";
3588
- import { homedir } from "node:os";
3589
- import { dirname as dirname12, join as join20, relative as relative8, resolve as resolve4, sep } from "node:path";
4150
+ import { homedir as homedir4 } from "node:os";
4151
+ import { dirname as dirname13, join as join24, relative as relative8, resolve as resolve5, sep } from "node:path";
3590
4152
  import { fileURLToPath as fileURLToPath4 } from "node:url";
4153
+ var CURRENT_ROUTER_VERSION = "1.1";
3591
4154
  function inspectPortfolioAiHealth(options) {
3592
- const endpoints = collectEndpoints(options.manifest);
3593
- const secretsRoot = options.secretsRoot ?? join20(dirname12(options.manifest.controlPlane?.path ?? endpoints[0]?.endpoint.path ?? process.cwd()), ".secrets");
3594
- const homeDir = options.homeDir ?? process.env.HOME ?? homedir();
4155
+ const allEndpoints = collectEndpoints(options.manifest);
4156
+ const endpoints = options.targetId && options.targetId !== "all" ? allEndpoints.filter(({ endpoint }) => endpoint.id === options.targetId) : allEndpoints;
4157
+ if (options.targetId && options.targetId !== "all" && endpoints.length === 0) {
4158
+ throw new Error(`Unknown portfolio target: ${options.targetId}`);
4159
+ }
4160
+ const secretsRoot = options.secretsRoot ?? join24(dirname13(options.manifest.controlPlane?.path ?? allEndpoints[0]?.endpoint.path ?? process.cwd()), ".secrets");
4161
+ const homeDir = options.homeDir ?? process.env.HOME ?? homedir4();
3595
4162
  const grokVersion = commandVersion("grok");
3596
4163
  const executionEngineRoot = options.manifest.executionEngine?.path;
3597
4164
  const skillRegistry = inspectSkillRegistry(executionEngineRoot);
3598
- const expectedPackageVersion = packageVersion(join20(executionEngineRoot ?? "", "packages/pro-gov/package.json"));
4165
+ const expectedPackageVersion = packageVersion(join24(executionEngineRoot ?? "", "packages/pro-gov/package.json"));
3599
4166
  const repositories = endpoints.map(({ endpoint, role }) => inspectRepository(
3600
4167
  endpoint,
3601
4168
  role,
@@ -3608,15 +4175,27 @@ function inspectPortfolioAiHealth(options) {
3608
4175
  const summary = { healthy: 0, attention: 0, unhealthy: 0 };
3609
4176
  for (const repository of repositories) summary[repository.status] += 1;
3610
4177
  return {
3611
- schemaVersion: 2,
4178
+ schemaVersion: 5,
3612
4179
  portfolioId: options.manifest.portfolioId,
3613
4180
  generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3614
- privacy: "Names, paths, counts, scopes, and configuration origins only. Secret values, environment values, MCP commands, arguments, URLs, headers, and MCP environment maps are never retained or rendered.",
4181
+ coverage: {
4182
+ mode: options.targetId && options.targetId !== "all" ? "single" : "all",
4183
+ targetId: options.targetId && options.targetId !== "all" ? options.targetId : void 0,
4184
+ coveredRepositoryIds: repositories.map((repository) => repository.id),
4185
+ totalRepositoryCount: allEndpoints.length
4186
+ },
4187
+ privacy: "Names, paths, counts, scopes, safe health states, and configuration origins only. Secret values, environment values, MCP commands, arguments, URLs, headers, MCP environment maps, and raw process environments are never retained or rendered.",
3615
4188
  secretsRoot: inspectSecretsRoot(secretsRoot),
3616
- hostEnvironment: inspectHostEnvironment(homeDir, grokVersion),
4189
+ hostEnvironment: inspectHostEnvironment(
4190
+ homeDir,
4191
+ grokVersion,
4192
+ allEndpoints.map(({ endpoint }) => endpoint.path),
4193
+ options.manifest.specialistChecks?.devspace
4194
+ ),
3617
4195
  skillRegistry,
3618
4196
  technologyGovernance: {
3619
4197
  strategySource: options.manifest.technologyGovernance?.strategySource,
4198
+ versionPolicy: options.manifest.technologyGovernance?.versionPolicy,
3620
4199
  projectTypes: options.manifest.technologyGovernance?.projectTypes.length ?? 0,
3621
4200
  technologies: options.manifest.technologyGovernance?.technologies.length ?? 0
3622
4201
  },
@@ -3624,19 +4203,44 @@ function inspectPortfolioAiHealth(options) {
3624
4203
  repositories
3625
4204
  };
3626
4205
  }
4206
+ function mergePortfolioAiHealthReport(existing, latest, allRepositoryIds) {
4207
+ const repositoriesById = /* @__PURE__ */ new Map();
4208
+ for (const repository of existing?.repositories ?? []) repositoriesById.set(repository.id, repository);
4209
+ for (const repository of latest.repositories) repositoriesById.set(repository.id, repository);
4210
+ const repositories = allRepositoryIds.map((id) => repositoriesById.get(id)).filter((repository) => repository !== void 0);
4211
+ const coveredIds = /* @__PURE__ */ new Set([
4212
+ ...existing?.coverage?.coveredRepositoryIds ?? [],
4213
+ ...latest.coverage.coveredRepositoryIds
4214
+ ]);
4215
+ const coveredRepositoryIds = allRepositoryIds.filter((id) => coveredIds.has(id));
4216
+ const summary = { healthy: 0, attention: 0, unhealthy: 0 };
4217
+ for (const repository of repositories) summary[repository.status] += 1;
4218
+ const complete = coveredRepositoryIds.length === allRepositoryIds.length;
4219
+ return {
4220
+ ...latest,
4221
+ repositories,
4222
+ summary,
4223
+ coverage: {
4224
+ mode: complete ? "all" : "single",
4225
+ targetId: complete ? void 0 : latest.coverage.targetId,
4226
+ coveredRepositoryIds,
4227
+ totalRepositoryCount: allRepositoryIds.length
4228
+ }
4229
+ };
4230
+ }
3627
4231
  function writePortfolioAiHealthReport(report, outDir) {
3628
4232
  mkdirSync8(outDir, { recursive: true });
3629
4233
  const dashboardAssets = findDashboardAssets();
3630
4234
  for (const file of ["index.html", "app.js", "app.css"]) {
3631
- const source = join20(dashboardAssets, file);
3632
- if (!existsSync21(source)) throw new Error(`Portfolio dashboard asset is missing: ${source}`);
3633
- cpSync2(source, join20(outDir, file));
4235
+ const source = join24(dashboardAssets, file);
4236
+ if (!existsSync23(source)) throw new Error(`Portfolio dashboard asset is missing: ${source}`);
4237
+ cpSync2(source, join24(outDir, file));
3634
4238
  }
3635
- const jsonPath = join20(outDir, "portfolio-ai-health.json");
3636
- const htmlPath = join20(outDir, "index.html");
4239
+ const jsonPath = join24(outDir, "portfolio-ai-health.json");
4240
+ const htmlPath = join24(outDir, "index.html");
3637
4241
  writeFileSync7(jsonPath, `${JSON.stringify(report, null, 2)}
3638
4242
  `);
3639
- writeFileSync7(join20(outDir, "data.js"), `window.__PORTFOLIO_AI_HEALTH__ = ${safeJavaScriptJson(report)};
4243
+ writeFileSync7(join24(outDir, "data.js"), `window.__PORTFOLIO_AI_HEALTH__ = ${safeJavaScriptJson(report)};
3640
4244
  `);
3641
4245
  return { jsonPath, htmlPath };
3642
4246
  }
@@ -3647,7 +4251,7 @@ function collectEndpoints(manifest) {
3647
4251
  for (const target of manifest.targets) result.push({ endpoint: target, role: "target" });
3648
4252
  const seen = /* @__PURE__ */ new Set();
3649
4253
  return result.filter(({ endpoint }) => {
3650
- const key = resolve4(endpoint.path);
4254
+ const key = resolve5(endpoint.path);
3651
4255
  if (seen.has(key)) return false;
3652
4256
  seen.add(key);
3653
4257
  return true;
@@ -3659,18 +4263,22 @@ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackage
3659
4263
  const entries = inspectEntries(root);
3660
4264
  const grokInspection = inspectGrokProject(root, homeDir, grokVersion);
3661
4265
  const skills = inspectSkills(root, grokInspection);
4266
+ const hostSsot = inspectProjectHostSsot(root);
3662
4267
  const hooks = inspectHooks(root);
3663
4268
  const docs = inspectDocs(root, role === "execution-engine" ? void 0 : expectedPackageVersion);
3664
4269
  const mcp = {
3665
- codexProject: tomlMcpNames(join20(root, ".codex/config.toml")),
3666
- claudeCodeProjectShared: jsonObjectKeys(join20(root, ".mcp.json"), "mcpServers"),
4270
+ codexProject: tomlMcpNames(join24(root, ".codex/config.toml")),
4271
+ claudeCodeProjectShared: jsonObjectKeys(join24(root, ".mcp.json"), "mcpServers"),
3667
4272
  claudeCodeProjectLocal: claudeProjectLocalMcpNames(homeDir, root),
3668
- grokProject: tomlMcpNames(join20(root, ".grok/config.toml")),
4273
+ grokProject: tomlMcpNames(join24(root, ".grok/config.toml")),
3669
4274
  grokEffective: grokInspection.effectiveMcp,
3670
4275
  grokInspection: grokInspection.inspection
3671
4276
  };
3672
4277
  const secrets = inspectRepositorySecrets(root, endpoint.id, secretsRoot, git.isRepository, endpoint.environmentPolicy);
3673
4278
  const projectModel = inspectProjectModel(root, endpoint, technologyGovernance);
4279
+ const versions = inspectVersionPolicy(root, technologyGovernance?.versionPolicy, endpoint.projectType);
4280
+ const verification = inspectProjectVerification(root);
4281
+ const redundancy = inspectProjectRedundancy(root, { homeDir });
3674
4282
  const recommendations = [];
3675
4283
  if (!git.isRepository) recommendations.push("\u8BE5\u8DEF\u5F84\u4E0D\u662F Git \u4ED3\u5E93\uFF1B\u786E\u8BA4\u6E05\u5355\u8DEF\u5F84\u662F\u5426\u6B63\u786E\u3002");
3676
4284
  if (git.unmergedBranches.length > 0) recommendations.push(`\u6709 ${git.unmergedBranches.length} \u6761\u5206\u652F\u5C1A\u672A\u5408\u5165\u5F53\u524D HEAD\uFF1A${git.unmergedBranches.join(", ")}\u3002`);
@@ -3689,6 +4297,9 @@ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackage
3689
4297
  if (skills.canonical.some((skill) => skill.kind === "dangling-symlink")) recommendations.push("`.agents/skills` \u4E2D\u5B58\u5728\u65AD\u5F00\u7684\u6280\u80FD\u94FE\u63A5\u3002");
3690
4298
  if (skills.claudeCompatibility === "duplicate-directory") recommendations.push("`.claude/skills` \u662F\u72EC\u7ACB\u526F\u672C\uFF1B\u5EFA\u8BAE\u94FE\u63A5\u5230 `.agents/skills`\uFF0C\u907F\u514D\u53CC\u4EFD\u6280\u80FD\u6F02\u79FB\u3002");
3691
4299
  if (skills.claudeCompatibility === "dangling-symlink") recommendations.push("`.claude/skills` \u662F\u65AD\u5F00\u7684\u94FE\u63A5\u3002");
4300
+ if (!hostSsot.claudeEntry.compliant) recommendations.push(`CLAUDE.md \u4E0D\u662F\u89C4\u8303\u7684\u76F8\u5BF9\u94FE\u63A5 AGENTS.md\uFF08${hostSsot.claudeEntry.status}\uFF09\u3002`);
4301
+ if (!hostSsot.claudeSkills.compliant) recommendations.push(`.claude/skills \u4E0D\u662F\u89C4\u8303\u7684\u76F8\u5BF9\u94FE\u63A5 ../.agents/skills\uFF08${hostSsot.claudeSkills.status}\uFF09\u3002`);
4302
+ if (hostSsot.canonicalSkills.status !== "directory") recommendations.push(`\u7F3A\u5C11\u89C4\u8303\u7684 .agents/skills \u6280\u80FD\u76EE\u5F55\uFF08${hostSsot.canonicalSkills.status}\uFF09\u3002`);
3692
4303
  if (hasWorkflowReminderHooks(hooks)) recommendations.push("\u53D1\u73B0 Stop/SubagentStop hook\uFF1B\u786E\u8BA4\u5B83\u662F\u5426\u4ECD\u6709\u9879\u76EE\u4E13\u5C5E\u7528\u9014\uFF0C\u5E76\u79FB\u9664\u9000\u4F11\u7684 PGS \u5DE5\u4F5C\u6D41\u63D0\u9192\u3002");
3693
4304
  const liveEnv = secrets.repositoryEnvFiles.filter((file) => !file.template && !file.fixture);
3694
4305
  const envNeedsCentralReview = liveEnv.filter((file) => !file.localOnly);
@@ -3697,38 +4308,47 @@ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackage
3697
4308
  else if (envNeedsCentralReview.some((file) => !file.centralized)) recommendations.push("\u53D1\u73B0\u771F\u5B9E\u73AF\u5883\u6587\u4EF6\u5C1A\u672A\u8FDE\u63A5\u5230\u672C\u9879\u76EE\u7684\u4E2D\u592E `.secrets` \u76EE\u5F55\uFF1B\u533A\u5206\u53EF\u4E22\u5F03\u751F\u6210\u7269\u4E0E\u672C\u5730\u4E3B\u6765\u6E90\uFF0C\u9700\u4FDD\u7559\u7684\u6765\u6E90\u5E94\u96C6\u4E2D\u540E\u518D\u63A5\u56DE\u9879\u76EE\u3002");
3698
4309
  if (hasUnsafeCentralSecretPermissions(secrets)) recommendations.push("\u4E2D\u592E\u5BC6\u94A5\u76EE\u5F55\u6216\u6587\u4EF6\u6743\u9650\u8FC7\u5BBD\uFF1B\u76EE\u5F55\u5E94\u4E3A 0700\uFF0C\u914D\u7F6E\u6587\u4EF6\u5E94\u4E3A 0600\u3002");
3699
4310
  if (!docs.packages.aligned) recommendations.push(`PGS \u5305\u7248\u672C\u672A\u4E0E\u6267\u884C\u5F15\u64CE ${docs.packages.expected ?? "\u672A\u77E5\u7248\u672C"} \u5BF9\u9F50\uFF1B\u53D1\u5E03\u4E0A\u6E38\u540E\u518D\u540C\u6B65\u76EE\u6807\u4ED3\u5E93\u3002`);
4311
+ if (!docs.routerAligned) recommendations.push(`PGS Router \u7248\u672C\u4E3A ${docs.routerVersion ? `v${docs.routerVersion}` : "\u672A\u8BC6\u522B"}\uFF0C\u5E94\u540C\u6B65\u5230 v${docs.expectedRouterVersion}\u3002`);
3700
4312
  if (role === "target" && !docs.manifest) recommendations.push("\u7F3A\u5C11 docs/governance/MANIFEST.yml\uFF1B\u6587\u6863\u6E05\u5355\u65E0\u6CD5\u8BC1\u660E\u5DF2\u540C\u6B65\u3002");
3701
4313
  if (technologyGovernance && !projectModel.projectType) recommendations.push("\u672A\u58F0\u660E\u9879\u76EE\u7C7B\u578B\uFF1B\u65E0\u6CD5\u628A\u5B9E\u9645\u6280\u672F\u4E0E\u4EA7\u54C1\u7EBF\u57FA\u7EBF\u8FDB\u884C\u6BD4\u8F83\u3002");
3702
4314
  const missingBaseline = projectModel.baseline.filter((technology) => !technology.detected && !hasBaselineException(projectModel, technology.id));
3703
4315
  if (missingBaseline.length > 0) recommendations.push(`\u6280\u672F\u57FA\u7EBF\u7F3A\u5C11\u53EF\u9A8C\u8BC1\u4FE1\u53F7\uFF1A${missingBaseline.map((technology) => technology.label).join("\u3001")}\u3002`);
3704
4316
  const missingSelected = projectModel.optionalCapabilities.filter((technology) => technology.selected && !technology.detected);
3705
4317
  if (missingSelected.length > 0) recommendations.push(`\u5DF2\u9009\u80FD\u529B\u7F3A\u5C11\u53EF\u9A8C\u8BC1\u4FE1\u53F7\uFF1A${missingSelected.map((technology) => technology.label).join("\u3001")}\u3002`);
4318
+ if (versions.status === "attention") recommendations.push(`\u6280\u672F\u7248\u672C\u7B56\u7565\u6709 ${versions.attentionCount} \u9879\u6F02\u79FB\u6216\u7F3A\u5931\uFF1B\u58F0\u660E\u7248\u672C\u4E0E\u5DF2\u5B89\u88C5\u7248\u672C\u5FC5\u987B\u7CBE\u786E\u5BF9\u9F50\u3002`);
4319
+ if (verification.status === "attention") recommendations.push(`\u9A8C\u8BC1\u811A\u672C\u7F3A\u5931\uFF1A${verification.missing.join("\u3001")}\uFF1BPGS \u8981\u6C42 typecheck\u3001lint\u3001format:check\u3001verify \u53EF\u53D1\u73B0\u3002`);
4320
+ if (redundancy.status === "attention") recommendations.push("\u53D1\u73B0\u65E7 AI \u76EE\u5F55\u6216\u5927\u578B Playwright \u7F13\u5B58\uFF1B\u4EC5\u63D0\u4F9B\u8BC1\u636E\uFF0C\u786E\u8BA4\u5F52\u5C5E\u540E\u518D\u7531\u4EBA\u5DE5\u6E05\u7406\u3002");
3706
4321
  return {
3707
4322
  id: endpoint.id,
3708
4323
  role,
3709
4324
  path: root,
3710
4325
  profile: "profile" in endpoint ? endpoint.profile : void 0,
3711
- status: deriveStatus(role, entries, git, hooks, skills, secrets, docs, projectModel, Boolean(technologyGovernance)),
4326
+ status: deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, Boolean(technologyGovernance), versions, verification, redundancy),
3712
4327
  recommendations,
3713
4328
  git,
3714
4329
  entries,
3715
4330
  hooks,
3716
4331
  mcp,
3717
4332
  skills,
4333
+ hostSsot,
3718
4334
  secrets,
3719
4335
  docs,
3720
- projectModel
4336
+ projectModel,
4337
+ versions,
4338
+ verification,
4339
+ redundancy
3721
4340
  };
3722
4341
  }
3723
- function deriveStatus(role, entries, git, hooks, skills, secrets, docs, projectModel, technologyGovernanceConfigured) {
4342
+ function deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, technologyGovernanceConfigured, versions, verification, redundancy) {
3724
4343
  if (!git.isRepository || entries.agents === "missing" || entries.claude === "dangling-symlink" || entries.gemini === "dangling-symlink" || skills.canonical.some((item) => item.kind === "dangling-symlink") || secrets.repositoryEnvFiles.some((file) => file.tracked && !file.template && !file.fixture) || hasUnsafeCentralSecretPermissions(secrets)) return "unhealthy";
3725
4344
  const missingBaseline = projectModel.baseline.some((technology) => !technology.detected && !hasBaselineException(projectModel, technology.id));
3726
4345
  const missingSelected = projectModel.optionalCapabilities.some((technology) => technology.selected && !technology.detected);
3727
4346
  const liveEnv = secrets.repositoryEnvFiles.filter((file) => !file.template && !file.fixture);
3728
4347
  const secretMaterializationNeedsReview = liveEnv.some((file) => !file.centralized && !file.localOnly);
3729
4348
  const packageVersionNeedsReview = docs.packages.expected !== void 0 && !docs.packages.aligned;
4349
+ const routerVersionNeedsReview = !docs.routerAligned;
3730
4350
  const targetManifestMissing = role === "target" && !docs.manifest;
3731
- if (entries.agents !== "pgs-router" || entries.claude !== "agents-symlink" || hasWorkflowReminderHooks(hooks) || skills.claudeCompatibility === "duplicate-directory" || skills.claudeCompatibility === "dangling-symlink" || git.branches.length > 1 || git.worktrees.length > 1 || git.dirtyPaths.length > 0 || (git.ahead ?? 0) > 0 || secretMaterializationNeedsReview || technologyGovernanceConfigured && !projectModel.projectType || missingBaseline || missingSelected || packageVersionNeedsReview || targetManifestMissing) return "attention";
4351
+ if (entries.agents !== "pgs-router" || entries.claude !== "agents-symlink" || hasWorkflowReminderHooks(hooks) || skills.claudeCompatibility === "duplicate-directory" || skills.claudeCompatibility === "dangling-symlink" || !hostSsot.compliant || git.branches.length > 1 || git.worktrees.length > 1 || git.dirtyPaths.length > 0 || (git.ahead ?? 0) > 0 || secretMaterializationNeedsReview || technologyGovernanceConfigured && !projectModel.projectType || missingBaseline || missingSelected || packageVersionNeedsReview || routerVersionNeedsReview || targetManifestMissing || versions.status === "attention" || verification.status === "attention" || redundancy.legacyDirectories.length > 0) return "attention";
3732
4352
  return "healthy";
3733
4353
  }
3734
4354
  function inspectProjectModel(root, endpoint, governance) {
@@ -3738,7 +4358,7 @@ function inspectProjectModel(root, endpoint, governance) {
3738
4358
  const detection = (id) => {
3739
4359
  const technology = technologyById.get(id);
3740
4360
  const packageMatch = technology?.packages?.some((name) => packages.has(name)) ?? false;
3741
- const fileMatch = technology?.files?.some((path) => existsSync21(join20(root, path))) ?? false;
4361
+ const fileMatch = technology?.files?.some((path) => existsSync23(join24(root, path))) ?? false;
3742
4362
  return { id, label: technology?.label ?? id, detected: packageMatch || fileMatch };
3743
4363
  };
3744
4364
  const selected = new Set(endpoint.capabilities ?? []);
@@ -3761,10 +4381,10 @@ function collectPackageNames(root) {
3761
4381
  try {
3762
4382
  files = splitLines(execFileSync("git", ["ls-files", "*package.json"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }));
3763
4383
  } catch {
3764
- if (existsSync21(join20(root, "package.json"))) files = ["package.json"];
4384
+ if (existsSync23(join24(root, "package.json"))) files = ["package.json"];
3765
4385
  }
3766
4386
  for (const file of files) {
3767
- const packageJson = readJson3(join20(root, file));
4387
+ const packageJson = readJson4(join24(root, file));
3768
4388
  if (!isRecord3(packageJson)) continue;
3769
4389
  for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
3770
4390
  const dependencies = packageJson[section];
@@ -3802,15 +4422,15 @@ function inspectGit2(root) {
3802
4422
  };
3803
4423
  }
3804
4424
  function inspectEntries(root) {
3805
- const agentsPath = join20(root, "AGENTS.md");
3806
- const agents = !existsSync21(agentsPath) ? "missing" : safeRead(agentsPath).includes("PGS-ROUTER:BEGIN") ? "pgs-router" : "custom";
3807
- const claudePath = join20(root, "CLAUDE.md");
4425
+ const agentsPath = join24(root, "AGENTS.md");
4426
+ const agents = !existsSync23(agentsPath) ? "missing" : safeRead(agentsPath).includes("PGS-ROUTER:BEGIN") ? "pgs-router" : "custom";
4427
+ const claudePath = join24(root, "CLAUDE.md");
3808
4428
  let claude = "missing";
3809
4429
  if (pathLexists(claudePath)) {
3810
- const info = lstatSync6(claudePath);
4430
+ const info = lstatSync8(claudePath);
3811
4431
  if (info.isSymbolicLink()) {
3812
4432
  try {
3813
- claude = realpathSync2(claudePath) === realpathSync2(agentsPath) ? "agents-symlink" : "custom";
4433
+ claude = realpathSync4(claudePath) === realpathSync4(agentsPath) ? "agents-symlink" : "custom";
3814
4434
  } catch {
3815
4435
  claude = "dangling-symlink";
3816
4436
  }
@@ -3822,12 +4442,12 @@ function inspectEntries(root) {
3822
4442
  return { agents, claude, gemini: inspectOptionalEntry(root, "GEMINI.md", agentsPath) };
3823
4443
  }
3824
4444
  function inspectOptionalEntry(root, filename, agentsPath) {
3825
- const path = join20(root, filename);
4445
+ const path = join24(root, filename);
3826
4446
  if (!pathLexists(path)) return "missing";
3827
- const info = lstatSync6(path);
4447
+ const info = lstatSync8(path);
3828
4448
  if (info.isSymbolicLink()) {
3829
4449
  try {
3830
- return realpathSync2(path) === realpathSync2(agentsPath) ? "agents-symlink" : "custom";
4450
+ return realpathSync4(path) === realpathSync4(agentsPath) ? "agents-symlink" : "custom";
3831
4451
  } catch {
3832
4452
  return "dangling-symlink";
3833
4453
  }
@@ -3836,7 +4456,7 @@ function inspectOptionalEntry(root, filename, agentsPath) {
3836
4456
  return /AGENTS\.md/.test(content) && content.length < 2e3 ? "thin-adapter" : "custom";
3837
4457
  }
3838
4458
  function inspectSkills(root, grokInspection) {
3839
- const lock = readJson3(join20(root, ".pro-gov/assets.lock.json"));
4459
+ const lock = readJson4(join24(root, ".pro-gov/assets.lock.json"));
3840
4460
  const managed = /* @__PURE__ */ new Set();
3841
4461
  const bundleIds = stringArray(isRecord3(lock) ? lock.bundleIds : void 0);
3842
4462
  if (isRecord3(lock) && Array.isArray(lock.assets)) {
@@ -3846,14 +4466,14 @@ function inspectSkills(root, grokInspection) {
3846
4466
  if (match) managed.add(match[1]);
3847
4467
  }
3848
4468
  }
3849
- const skillRoot = join20(root, ".agents/skills");
4469
+ const skillRoot = join24(root, ".agents/skills");
3850
4470
  const canonical = pathLexists(skillRoot) && safeIsDirectory(skillRoot) ? safeReadDir(skillRoot).filter((name) => !name.startsWith(".")).map((name) => {
3851
- const path = join20(skillRoot, name);
3852
- const stat = lstatSync6(path);
4471
+ const path = join24(skillRoot, name);
4472
+ const stat = lstatSync8(path);
3853
4473
  let kind = stat.isSymbolicLink() ? "symlink" : stat.isDirectory() ? "directory" : "file";
3854
4474
  if (stat.isSymbolicLink()) {
3855
4475
  try {
3856
- realpathSync2(path);
4476
+ realpathSync4(path);
3857
4477
  } catch {
3858
4478
  kind = "dangling-symlink";
3859
4479
  }
@@ -3874,20 +4494,20 @@ function inspectSkills(root, grokInspection) {
3874
4494
  },
3875
4495
  hosts: {
3876
4496
  codexProject: canonical.filter((item) => item.kind !== "dangling-symlink").length,
3877
- claudeCodeProject: inspectSkillRoot(join20(root, ".claude/skills")).names.length,
3878
- grokNativeProject: inspectSkillRoot(join20(root, ".grok/skills")).names.length,
4497
+ claudeCodeProject: inspectSkillRoot(join24(root, ".claude/skills")).names.length,
4498
+ grokNativeProject: inspectSkillRoot(join24(root, ".grok/skills")).names.length,
3879
4499
  grokEffective: grokInspection.skills
3880
4500
  }
3881
4501
  };
3882
4502
  }
3883
4503
  function inspectClaudeSkillRoot(root) {
3884
- const path = join20(root, ".claude/skills");
4504
+ const path = join24(root, ".claude/skills");
3885
4505
  if (!pathLexists(path)) return "missing";
3886
- const stat = lstatSync6(path);
4506
+ const stat = lstatSync8(path);
3887
4507
  if (stat.isSymbolicLink()) {
3888
4508
  try {
3889
- const target = realpathSync2(path);
3890
- return target === realpathSync2(join20(root, ".agents/skills")) ? "shared-root" : "other";
4509
+ const target = realpathSync4(path);
4510
+ return target === realpathSync4(join24(root, ".agents/skills")) ? "shared-root" : "other";
3891
4511
  } catch {
3892
4512
  return "dangling-symlink";
3893
4513
  }
@@ -3895,7 +4515,7 @@ function inspectClaudeSkillRoot(root) {
3895
4515
  return stat.isDirectory() ? "duplicate-directory" : "other";
3896
4516
  }
3897
4517
  function inspectRepositorySecrets(root, id, secretsRoot, isRepository, environmentPolicy) {
3898
- const centralPath = join20(secretsRoot, id);
4518
+ const centralPath = join24(secretsRoot, id);
3899
4519
  const centralRealPath = safeRealpath(centralPath);
3900
4520
  const localOnlyReasons = new Map(
3901
4521
  (environmentPolicy?.localOnly ?? []).map((entry) => [entry.path, entry.reason])
@@ -3907,16 +4527,16 @@ function inspectRepositorySecrets(root, id, secretsRoot, isRepository, environme
3907
4527
  tracked: isRepository ? gitTracks(root, path) : false,
3908
4528
  template: isEnvironmentTemplate(path),
3909
4529
  fixture: isEnvironmentFixture(path),
3910
- symlink: lstatSync6(join20(root, path)).isSymbolicLink(),
3911
- centralized: pointsInside(join20(root, path), centralRealPath),
4530
+ symlink: lstatSync8(join24(root, path)).isSymbolicLink(),
4531
+ centralized: pointsInside(join24(root, path), centralRealPath),
3912
4532
  localOnly: localOnlyReason !== void 0,
3913
4533
  ...localOnlyReason !== void 0 ? { localOnlyReason } : {}
3914
4534
  };
3915
4535
  });
3916
4536
  return {
3917
- centralDirectory: existsSync21(centralPath) ? "present" : "absent",
3918
- centralMode: existsSync21(centralPath) ? modeString(statSync4(centralPath).mode) : void 0,
3919
- centralFiles: existsSync21(centralPath) ? collectCentralSecretFiles(centralPath) : [],
4537
+ centralDirectory: existsSync23(centralPath) ? "present" : "absent",
4538
+ centralMode: existsSync23(centralPath) ? modeString(statSync5(centralPath).mode) : void 0,
4539
+ centralFiles: existsSync23(centralPath) ? collectCentralSecretFiles(centralPath) : [],
3920
4540
  repositoryEnvFiles: envFiles
3921
4541
  };
3922
4542
  }
@@ -3925,11 +4545,11 @@ function collectEnvironmentFiles(root, current = root, depth = 0) {
3925
4545
  if (depth > 5) return [];
3926
4546
  const found = [];
3927
4547
  try {
3928
- for (const entry of readdirSync8(current, { withFileTypes: true })) {
4548
+ for (const entry of readdirSync9(current, { withFileTypes: true })) {
3929
4549
  if (entry.isDirectory()) {
3930
- if (!SKIP_ENV_DIRECTORIES.has(entry.name)) found.push(...collectEnvironmentFiles(root, join20(current, entry.name), depth + 1));
4550
+ if (!SKIP_ENV_DIRECTORIES.has(entry.name)) found.push(...collectEnvironmentFiles(root, join24(current, entry.name), depth + 1));
3931
4551
  } else if (isEnvironmentFilename(entry.name) && !isProviderGeneratedEnvironmentFile(entry.name)) {
3932
- found.push(relative8(root, join20(current, entry.name)));
4552
+ found.push(relative8(root, join24(current, entry.name)));
3933
4553
  }
3934
4554
  }
3935
4555
  } catch {
@@ -3941,10 +4561,10 @@ function collectCentralSecretFiles(root, current = root, depth = 0) {
3941
4561
  if (depth > 3) return [];
3942
4562
  const found = [];
3943
4563
  try {
3944
- for (const entry of readdirSync8(current, { withFileTypes: true })) {
3945
- const path = join20(current, entry.name);
4564
+ for (const entry of readdirSync9(current, { withFileTypes: true })) {
4565
+ const path = join24(current, entry.name);
3946
4566
  if (entry.isDirectory()) found.push(...collectCentralSecretFiles(root, path, depth + 1));
3947
- else found.push({ path: relative8(root, path), mode: modeString(lstatSync6(path).mode) });
4567
+ else found.push({ path: relative8(root, path), mode: modeString(lstatSync8(path).mode) });
3948
4568
  }
3949
4569
  } catch {
3950
4570
  return found;
@@ -3969,13 +4589,13 @@ function isEnvironmentFixture(path) {
3969
4589
  }
3970
4590
  function safeRealpath(path) {
3971
4591
  try {
3972
- return realpathSync2(path);
4592
+ return realpathSync4(path);
3973
4593
  } catch {
3974
4594
  return void 0;
3975
4595
  }
3976
4596
  }
3977
4597
  function pointsInside(path, expectedRoot) {
3978
- if (!expectedRoot || !lstatSync6(path).isSymbolicLink()) return false;
4598
+ if (!expectedRoot || !lstatSync8(path).isSymbolicLink()) return false;
3979
4599
  const target = safeRealpath(path);
3980
4600
  if (!target) return false;
3981
4601
  const fromRoot = relative8(expectedRoot, target);
@@ -3985,24 +4605,25 @@ function hasUnsafeCentralSecretPermissions(secrets) {
3985
4605
  return secrets.centralDirectory === "present" && (secrets.centralMode !== "700" || secrets.centralFiles.some((file) => file.mode !== "600"));
3986
4606
  }
3987
4607
  function inspectSecretsRoot(path) {
3988
- return existsSync21(path) ? { path, exists: true, mode: modeString(statSync4(path).mode) } : { path, exists: false };
4608
+ return existsSync23(path) ? { path, exists: true, mode: modeString(statSync5(path).mode) } : { path, exists: false };
3989
4609
  }
3990
- function inspectHostEnvironment(homeDir, grokVersion) {
3991
- const codexConfig = join20(homeDir, ".codex/config.toml");
3992
- const claudeConfig = join20(homeDir, ".claude.json");
3993
- const grokConfig = join20(homeDir, ".grok/config.toml");
3994
- return {
4610
+ function inspectHostEnvironment(homeDir, grokVersion, repositoryPaths, devspaceSettings) {
4611
+ const codexConfig = join24(homeDir, ".codex/config.toml");
4612
+ const claudeConfig = join24(homeDir, ".claude.json");
4613
+ const grokConfig = join24(homeDir, ".grok/config.toml");
4614
+ const hostEnvironment = {
3995
4615
  mcp: {
3996
4616
  codexUser: { path: codexConfig, names: tomlMcpNames(codexConfig) },
3997
4617
  claudeCodeUser: { path: claudeConfig, names: jsonObjectKeys(claudeConfig, "mcpServers") },
3998
4618
  grokUser: { path: grokConfig, names: tomlMcpNames(grokConfig) }
3999
4619
  },
4000
4620
  skills: {
4001
- codexUser: inspectSkillRoot(join20(homeDir, ".agents/skills")),
4002
- claudeCodeUser: inspectSkillRoot(join20(homeDir, ".claude/skills")),
4003
- grokUser: inspectSkillRoot(join20(homeDir, ".grok/skills")),
4004
- grokAgentsCompatibility: inspectSkillRoot(join20(homeDir, ".agents/skills")),
4005
- grokClaudeCompatibility: inspectSkillRoot(join20(homeDir, ".claude/skills"))
4621
+ codexUser: inspectSkillRoot(join24(homeDir, ".agents/skills")),
4622
+ claudeCodeUser: inspectSkillRoot(join24(homeDir, ".claude/skills")),
4623
+ grokUser: inspectSkillRoot(join24(homeDir, ".grok/skills")),
4624
+ grokAgentsCompatibility: inspectSkillRoot(join24(homeDir, ".agents/skills")),
4625
+ grokClaudeCompatibility: inspectSkillRoot(join24(homeDir, ".claude/skills")),
4626
+ ssot: inspectUserSkillsSsot(homeDir)
4006
4627
  },
4007
4628
  grok: {
4008
4629
  available: grokVersion !== void 0,
@@ -4010,20 +4631,121 @@ function inspectHostEnvironment(homeDir, grokVersion) {
4010
4631
  inspectionCommand: "grok inspect --json"
4011
4632
  }
4012
4633
  };
4634
+ if (devspaceSettings) {
4635
+ hostEnvironment.devspace = inspectDevSpaceHealth({
4636
+ homeDir,
4637
+ repositoryPaths,
4638
+ expectedToolMode: devspaceSettings.expectedToolMode
4639
+ });
4640
+ }
4641
+ return hostEnvironment;
4642
+ }
4643
+ function inspectDevSpaceHealth(options) {
4644
+ const run = options.run ?? runDevSpaceCommand;
4645
+ const configDirectory = join24(options.homeDir, ".devspace");
4646
+ const configPath = join24(configDirectory, "config.json");
4647
+ const authPath = join24(configDirectory, "auth.json");
4648
+ const installedResult = run("devspace", ["--version"], 3e3);
4649
+ const installedVersion = installedResult.ok ? installedResult.stdout.match(/\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\b/)?.[0] : void 0;
4650
+ const latestResult = run("npm", ["view", "@waishnav/devspace", "version", "--registry=https://registry.npmjs.org/"], 5e3);
4651
+ const latestVersion = latestResult.ok ? latestResult.stdout.match(/\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\b/)?.[0] : void 0;
4652
+ const processResult = run("pgrep", ["-f", "devspace serve"], 3e3);
4653
+ const pid = processResult.ok ? processResult.stdout.match(/\b\d+\b/)?.[0] : void 0;
4654
+ const processEnvironment = pid ? run("ps", ["eww", "-p", pid, "-o", "command="], 3e3) : void 0;
4655
+ const processToolMode = processEnvironment?.ok ? processEnvironment.stdout.match(/(?:^|\s)DEVSPACE_TOOL_MODE=(minimal|full|codex)(?:\s|$)/)?.[1] : void 0;
4656
+ const doctor = !installedResult.ok ? "unavailable" : run("devspace", ["doctor"], 1e4).ok ? "ok" : "failed";
4657
+ const configValue = readJson4(configPath);
4658
+ const configExists = isRecord3(configValue);
4659
+ const allowedRoots = configExists && Array.isArray(configValue.allowedRoots) ? configValue.allowedRoots.filter((value) => typeof value === "string") : [];
4660
+ const portfolioCoverage = !configExists || allowedRoots.length === 0 ? "unknown" : options.repositoryPaths.every((repositoryPath) => allowedRoots.some((root) => isPathInside(repositoryPath, root))) ? "complete" : "partial";
4661
+ const bind = configExists && typeof configValue.host === "string" ? isLoopbackHost(configValue.host) ? "loopback" : "non-loopback" : "unknown";
4662
+ const directoryMode = existsSync23(configDirectory) ? modeString(statSync5(configDirectory).mode) : void 0;
4663
+ const fileMode = existsSync23(configPath) ? modeString(statSync5(configPath).mode) : void 0;
4664
+ const authMode = existsSync23(authPath) ? modeString(statSync5(authPath).mode) : void 0;
4665
+ const update = installedVersion && latestVersion ? installedVersion === latestVersion ? "current" : "available" : "unknown";
4666
+ const recommendations = [];
4667
+ let status = "healthy";
4668
+ const unhealthy = (message) => {
4669
+ status = "unhealthy";
4670
+ recommendations.push(message);
4671
+ };
4672
+ const attention = (message) => {
4673
+ if (status === "healthy") status = "attention";
4674
+ recommendations.push(message);
4675
+ };
4676
+ if (!installedResult.ok) unhealthy("\u672C\u673A\u672A\u53D1\u73B0 DevSpace\uFF1B\u65E0\u6CD5\u4F7F\u7528\u5BBF\u4E3B\u5DE5\u4F5C\u533A\u670D\u52A1\u3002");
4677
+ if (!configExists) unhealthy("\u7F3A\u5C11 ~/.devspace/config.json\u3002");
4678
+ if (!existsSync23(authPath)) unhealthy("\u7F3A\u5C11 ~/.devspace/auth.json\u3002");
4679
+ if (directoryMode && directoryMode !== "700") unhealthy(`~/.devspace \u76EE\u5F55\u6743\u9650\u4E3A ${directoryMode}\uFF0C\u5E94\u6536\u7D27\u4E3A 700\u3002`);
4680
+ if (fileMode && fileMode !== "600") unhealthy(`DevSpace \u914D\u7F6E\u6587\u4EF6\u6743\u9650\u4E3A ${fileMode}\uFF0C\u5E94\u4E3A 600\u3002`);
4681
+ if (authMode && authMode !== "600") unhealthy(`DevSpace \u8BA4\u8BC1\u6587\u4EF6\u6743\u9650\u4E3A ${authMode}\uFF0C\u5E94\u4E3A 600\u3002`);
4682
+ if (bind === "non-loopback") unhealthy("DevSpace \u76F4\u63A5\u7ED1\u5B9A\u5230\u975E\u672C\u673A\u5730\u5740\uFF1B\u5E94\u4F7F\u7528 loopback \u5E76\u7531\u53D7\u63A7\u4EE3\u7406\u66B4\u9732\u3002");
4683
+ if (doctor === "failed") unhealthy("devspace doctor \u672A\u901A\u8FC7\u3002");
4684
+ if (portfolioCoverage === "partial") attention("DevSpace allowedRoots \u6CA1\u6709\u8986\u76D6\u5168\u90E8\u5DF2\u767B\u8BB0\u4ED3\u5E93\u3002");
4685
+ if (installedResult.ok && !pid) attention("DevSpace \u5DF2\u5B89\u88C5\u4F46\u5F53\u524D\u6CA1\u6709\u8FD0\u884C\u3002");
4686
+ if (options.expectedToolMode && pid && processToolMode !== options.expectedToolMode) {
4687
+ attention(`\u8FD0\u884C\u4E2D\u7684 DevSpace \u5DE5\u5177\u6A21\u5F0F\u4E3A ${processToolMode ?? "unknown"}\uFF0C\u671F\u671B ${options.expectedToolMode}\u3002`);
4688
+ }
4689
+ if (update === "available") attention(`DevSpace \u6709\u65B0\u7248\u672C\uFF1A${installedVersion} \u2192 ${latestVersion}\uFF1B\u5347\u7EA7\u524D\u4ECD\u5E94\u68C0\u67E5\u53D8\u66F4\u4E0E\u517C\u5BB9\u6027\u3002`);
4690
+ return {
4691
+ status,
4692
+ installed: installedResult.ok,
4693
+ ...installedVersion ? { installedVersion } : {},
4694
+ ...latestVersion ? { latestVersion } : {},
4695
+ update,
4696
+ doctor,
4697
+ running: Boolean(pid),
4698
+ ...options.expectedToolMode ? { expectedToolMode: options.expectedToolMode } : {},
4699
+ processToolMode: processToolMode ?? "unknown",
4700
+ config: {
4701
+ exists: configExists,
4702
+ ...directoryMode ? { directoryMode } : {},
4703
+ ...fileMode ? { fileMode } : {},
4704
+ authExists: existsSync23(authPath),
4705
+ ...authMode ? { authMode } : {},
4706
+ bind,
4707
+ portValid: configExists && typeof configValue.port === "number" && Number.isInteger(configValue.port) && configValue.port > 0 && configValue.port <= 65535,
4708
+ publicHttpsConfigured: configExists && typeof configValue.publicBaseUrl === "string" && configValue.publicBaseUrl.startsWith("https://"),
4709
+ allowedRootCount: allowedRoots.length,
4710
+ portfolioCoverage
4711
+ },
4712
+ recommendations
4713
+ };
4714
+ }
4715
+ function runDevSpaceCommand(command2, args, timeout) {
4716
+ try {
4717
+ return {
4718
+ ok: true,
4719
+ stdout: execFileSync(command2, args, {
4720
+ encoding: "utf8",
4721
+ stdio: ["ignore", "pipe", "ignore"],
4722
+ timeout
4723
+ })
4724
+ };
4725
+ } catch {
4726
+ return { ok: false, stdout: "" };
4727
+ }
4728
+ }
4729
+ function isLoopbackHost(host) {
4730
+ return ["127.0.0.1", "localhost", "::1"].includes(host.trim().toLowerCase());
4731
+ }
4732
+ function isPathInside(path, root) {
4733
+ const fromRoot = relative8(resolve5(root), resolve5(path));
4734
+ return fromRoot === "" || fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`);
4013
4735
  }
4014
4736
  function inspectSkillRoot(path) {
4015
4737
  const exists = pathLexists(path) && safeIsDirectory(path);
4016
- const names = exists ? safeReadDir(path).filter((name) => !name.startsWith(".") && existsSync21(join20(path, name, "SKILL.md"))) : [];
4738
+ const names = exists ? safeReadDir(path).filter((name) => !name.startsWith(".") && existsSync23(join24(path, name, "SKILL.md"))) : [];
4017
4739
  return { path, exists, names };
4018
4740
  }
4019
4741
  function claudeProjectLocalMcpNames(homeDir, root) {
4020
4742
  if (!homeDir) return [];
4021
- const value = readJson3(join20(homeDir, ".claude.json"));
4743
+ const value = readJson4(join24(homeDir, ".claude.json"));
4022
4744
  if (!isRecord3(value) || !isRecord3(value.projects)) return [];
4023
- const candidates = new Set([resolve4(root), safeRealpath(root)].filter((path) => Boolean(path)));
4745
+ const candidates = new Set([resolve5(root), safeRealpath(root)].filter((path) => Boolean(path)));
4024
4746
  const names = /* @__PURE__ */ new Set();
4025
4747
  for (const [path, project] of Object.entries(value.projects)) {
4026
- const projectPaths = [resolve4(path), safeRealpath(path)].filter((candidate) => Boolean(candidate));
4748
+ const projectPaths = [resolve5(path), safeRealpath(path)].filter((candidate) => Boolean(candidate));
4027
4749
  if (!projectPaths.some((candidate) => candidates.has(candidate)) || !isRecord3(project) || !isRecord3(project.mcpServers)) continue;
4028
4750
  for (const name of Object.keys(project.mcpServers)) names.add(name);
4029
4751
  }
@@ -4054,13 +4776,13 @@ function inspectGrokProject(root, homeDir, grokVersion) {
4054
4776
  const value = JSON.parse(execFileSync("grok", ["inspect", "--json"], {
4055
4777
  cwd: root,
4056
4778
  encoding: "utf8",
4057
- env: { ...process.env, HOME: homeDir, GROK_HOME: join20(homeDir, ".grok") },
4779
+ env: { ...process.env, HOME: homeDir, GROK_HOME: join24(homeDir, ".grok") },
4058
4780
  maxBuffer: 10 * 1024 * 1024,
4059
4781
  stdio: ["ignore", "pipe", "ignore"],
4060
4782
  timeout: 8e3
4061
4783
  }));
4062
4784
  if (!isRecord3(value)) return empty("failed");
4063
- const userClaudeNames = new Set(jsonObjectKeys(join20(homeDir, ".claude.json"), "mcpServers"));
4785
+ const userClaudeNames = new Set(jsonObjectKeys(join24(homeDir, ".claude.json"), "mcpServers"));
4064
4786
  const localClaudeNames = new Set(claudeProjectLocalMcpNames(homeDir, root));
4065
4787
  const effectiveMcp = Array.isArray(value.mcpServers) ? value.mcpServers.flatMap((item) => {
4066
4788
  if (!isRecord3(item) || typeof item.name !== "string") return [];
@@ -4116,15 +4838,15 @@ function inferGrokMcpScope(name, sourceType, sourcePath, root, homeDir, userClau
4116
4838
  if (userClaudeNames.has(name)) return "user";
4117
4839
  return "unknown";
4118
4840
  }
4119
- const resolvedSource = safeRealpath(sourcePath) ?? resolve4(sourcePath);
4120
- const resolvedRoot = safeRealpath(root) ?? resolve4(root);
4121
- if (resolvedSource === join20(resolvedRoot, ".mcp.json")) return "project-shared";
4841
+ const resolvedSource = safeRealpath(sourcePath) ?? resolve5(sourcePath);
4842
+ const resolvedRoot = safeRealpath(root) ?? resolve5(root);
4843
+ if (resolvedSource === join24(resolvedRoot, ".mcp.json")) return "project-shared";
4122
4844
  if (resolvedSource.startsWith(resolvedRoot + sep)) return "project";
4123
- if (homeDir && resolvedSource === join20(resolve4(homeDir), ".claude.json")) {
4845
+ if (homeDir && resolvedSource === join24(resolve5(homeDir), ".claude.json")) {
4124
4846
  if (localClaudeNames.has(name)) return "project-local";
4125
4847
  if (userClaudeNames.has(name)) return "user";
4126
4848
  }
4127
- if (homeDir && resolvedSource.startsWith(resolve4(homeDir) + sep)) return "user";
4849
+ if (homeDir && resolvedSource.startsWith(resolve5(homeDir) + sep)) return "user";
4128
4850
  if (sourceType === "project") return "project";
4129
4851
  return "unknown";
4130
4852
  }
@@ -4155,7 +4877,7 @@ function inspectHooks(root) {
4155
4877
  { host: "codex", path: ".codex/hooks.json" }
4156
4878
  ];
4157
4879
  return configs.map((config) => {
4158
- const value = readJson3(join20(root, config.path));
4880
+ const value = readJson4(join24(root, config.path));
4159
4881
  const counts = /* @__PURE__ */ new Map();
4160
4882
  collectHookEvents(value, counts);
4161
4883
  return {
@@ -4176,16 +4898,18 @@ function collectHookEvents(value, counts) {
4176
4898
  }
4177
4899
  }
4178
4900
  function inspectDocs(root, expected) {
4179
- const packageJson = readJson3(join20(root, "package.json"));
4901
+ const packageJson = readJson4(join24(root, "package.json"));
4180
4902
  const dependencies = isRecord3(packageJson) ? { ...recordOrEmpty(packageJson.dependencies), ...recordOrEmpty(packageJson.devDependencies) } : {};
4181
4903
  const docGov = dependencyVersion(dependencies["@pieai/doc-gov"]);
4182
4904
  const proGov = dependencyVersion(dependencies["@pieai/pro-gov"]);
4183
- const routerMatch = safeRead(join20(root, "AGENTS.md")).match(/PGS-ROUTER:BEGIN\s+v([0-9.]+)/);
4905
+ const routerMatch = safeRead(join24(root, "AGENTS.md")).match(/PGS-ROUTER:BEGIN\s+v([0-9.]+)/);
4184
4906
  const declared = [docGov, proGov].filter((value) => Boolean(value));
4185
4907
  return {
4186
4908
  routerVersion: routerMatch?.[1],
4187
- manifest: existsSync21(join20(root, "docs/governance/MANIFEST.yml")),
4188
- currentWork: existsSync21(join20(root, "docs/reference/execution/current-work.md")),
4909
+ expectedRouterVersion: CURRENT_ROUTER_VERSION,
4910
+ routerAligned: routerMatch?.[1] === CURRENT_ROUTER_VERSION,
4911
+ manifest: existsSync23(join24(root, "docs/governance/MANIFEST.yml")),
4912
+ currentWork: existsSync23(join24(root, "docs/reference/execution/current-work.md")),
4189
4913
  packages: {
4190
4914
  expected,
4191
4915
  docGov,
@@ -4200,7 +4924,7 @@ function dependencyVersion(value) {
4200
4924
  return match?.[1];
4201
4925
  }
4202
4926
  function packageVersion(path) {
4203
- const value = readJson3(path);
4927
+ const value = readJson4(path);
4204
4928
  return isRecord3(value) && typeof value.version === "string" ? value.version : void 0;
4205
4929
  }
4206
4930
  function recordOrEmpty(value) {
@@ -4208,20 +4932,20 @@ function recordOrEmpty(value) {
4208
4932
  }
4209
4933
  function inspectSkillRegistry(executionEngineRoot) {
4210
4934
  if (!executionEngineRoot) return { source: 0, registered: 0, bundled: 0, bundles: 0 };
4211
- const agentAssetsRoot = join20(executionEngineRoot, "agent-assets");
4212
- const registry = readJson3(join20(agentAssetsRoot, "registry.json"));
4935
+ const agentAssetsRoot = join24(executionEngineRoot, "agent-assets");
4936
+ const registry = readJson4(join24(agentAssetsRoot, "registry.json"));
4213
4937
  const assets = isRecord3(registry) && Array.isArray(registry.assets) ? registry.assets : [];
4214
4938
  const registeredSkills = assets.filter((asset) => isRecord3(asset) && asset.kind === "skill");
4215
- const bundleRoot = join20(agentAssetsRoot, "bundles");
4939
+ const bundleRoot = join24(agentAssetsRoot, "bundles");
4216
4940
  const bundleFiles = safeReadDir(bundleRoot).filter((file) => file.endsWith(".json"));
4217
4941
  const bundledIds = /* @__PURE__ */ new Set();
4218
4942
  for (const file of bundleFiles) {
4219
- const bundle = readJson3(join20(bundleRoot, file));
4943
+ const bundle = readJson4(join24(bundleRoot, file));
4220
4944
  if (!isRecord3(bundle) || !Array.isArray(bundle.assets)) continue;
4221
4945
  for (const id of bundle.assets) if (typeof id === "string") bundledIds.add(id);
4222
4946
  }
4223
- const sourceRoots = [join20(agentAssetsRoot, "skills/pie-skills"), join20(agentAssetsRoot, "skills/npx-skills/.agents/skills")];
4224
- const source = sourceRoots.reduce((count, root) => count + safeReadDir(root).filter((name) => existsSync21(join20(root, name, "SKILL.md"))).length, 0);
4947
+ const sourceRoots = [join24(agentAssetsRoot, "skills/pie-skills"), join24(agentAssetsRoot, "skills/npx-skills/.agents/skills")];
4948
+ const source = sourceRoots.reduce((count, root) => count + safeReadDir(root).filter((name) => existsSync23(join24(root, name, "SKILL.md"))).length, 0);
4225
4949
  return {
4226
4950
  source,
4227
4951
  registered: registeredSkills.length,
@@ -4230,12 +4954,12 @@ function inspectSkillRegistry(executionEngineRoot) {
4230
4954
  };
4231
4955
  }
4232
4956
  function jsonObjectKeys(path, key) {
4233
- const value = readJson3(path);
4957
+ const value = readJson4(path);
4234
4958
  if (!isRecord3(value) || !isRecord3(value[key])) return [];
4235
4959
  return Object.keys(value[key]).sort();
4236
4960
  }
4237
4961
  function tomlMcpNames(path) {
4238
- if (!existsSync21(path)) return [];
4962
+ if (!existsSync23(path)) return [];
4239
4963
  const names = /* @__PURE__ */ new Set();
4240
4964
  for (const line of safeRead(path).split(/\r?\n/)) {
4241
4965
  const match = line.match(/^\s*\[mcp_servers\.(?:"([^"]+)"|([^\.\]]+))\]\s*$/);
@@ -4244,37 +4968,37 @@ function tomlMcpNames(path) {
4244
4968
  }
4245
4969
  return [...names].sort();
4246
4970
  }
4247
- function readJson3(path) {
4971
+ function readJson4(path) {
4248
4972
  try {
4249
- return JSON.parse(readFileSync15(path, "utf8"));
4973
+ return JSON.parse(readFileSync17(path, "utf8"));
4250
4974
  } catch {
4251
4975
  return void 0;
4252
4976
  }
4253
4977
  }
4254
4978
  function safeRead(path) {
4255
4979
  try {
4256
- return readFileSync15(path, "utf8");
4980
+ return readFileSync17(path, "utf8");
4257
4981
  } catch {
4258
4982
  return "";
4259
4983
  }
4260
4984
  }
4261
4985
  function safeReadDir(path) {
4262
4986
  try {
4263
- return readdirSync8(path).sort();
4987
+ return readdirSync9(path).sort();
4264
4988
  } catch {
4265
4989
  return [];
4266
4990
  }
4267
4991
  }
4268
4992
  function safeIsDirectory(path) {
4269
4993
  try {
4270
- return statSync4(path).isDirectory();
4994
+ return statSync5(path).isDirectory();
4271
4995
  } catch {
4272
4996
  return false;
4273
4997
  }
4274
4998
  }
4275
4999
  function pathLexists(path) {
4276
5000
  try {
4277
- lstatSync6(path);
5001
+ lstatSync8(path);
4278
5002
  return true;
4279
5003
  } catch {
4280
5004
  return false;
@@ -4301,14 +5025,17 @@ function isRecord3(value) {
4301
5025
  return typeof value === "object" && value !== null && !Array.isArray(value);
4302
5026
  }
4303
5027
  function findDashboardAssets() {
4304
- const packageRoot2 = dirname12(dirname12(fileURLToPath4(import.meta.url)));
5028
+ const packageRoot2 = dirname13(dirname13(fileURLToPath4(import.meta.url)));
4305
5029
  const candidates = [
4306
5030
  process.env.PGS_DASHBOARD_ASSETS_DIR,
4307
- join20(packageRoot2, "assets/portfolio-dashboard"),
4308
- join20(process.cwd(), "assets/portfolio-dashboard"),
4309
- join20(process.cwd(), "packages/pro-gov/assets/portfolio-dashboard")
5031
+ join24(packageRoot2, ".dashboard-build"),
5032
+ join24(packageRoot2, "assets/portfolio-dashboard"),
5033
+ join24(process.cwd(), ".dashboard-build"),
5034
+ join24(process.cwd(), "assets/portfolio-dashboard"),
5035
+ join24(process.cwd(), "packages/pro-gov/.dashboard-build"),
5036
+ join24(process.cwd(), "packages/pro-gov/assets/portfolio-dashboard")
4310
5037
  ].filter((value) => Boolean(value));
4311
- const match = candidates.find((path) => existsSync21(join20(path, "index.html")));
5038
+ const match = candidates.find((path) => existsSync23(join24(path, "index.html")));
4312
5039
  if (!match) throw new Error("Portfolio dashboard assets were not built. Run pnpm --filter @pieai/pro-gov build.");
4313
5040
  return match;
4314
5041
  }
@@ -4344,10 +5071,23 @@ function runPortfolioAiHealth(args) {
4344
5071
  for (const issue of loaded.issues) console.error(`${issue.type}: ${issue.message}`);
4345
5072
  return 1;
4346
5073
  }
4347
- const report = inspectPortfolioAiHealth({
5074
+ const targetId = options.value.targetId && options.value.targetId !== "all" ? options.value.targetId : void 0;
5075
+ if (targetId && !loaded.manifest.targets.some((target) => target.id === targetId)) {
5076
+ console.error(`Unknown portfolio target: ${targetId}`);
5077
+ return 1;
5078
+ }
5079
+ const latest = inspectPortfolioAiHealth({
4348
5080
  manifest: loaded.manifest,
4349
- secretsRoot: options.value.secretsRoot
5081
+ secretsRoot: options.value.secretsRoot,
5082
+ targetId
4350
5083
  });
5084
+ const allRepositoryIds = [
5085
+ loaded.manifest.controlPlane?.id,
5086
+ loaded.manifest.executionEngine?.id,
5087
+ ...loaded.manifest.targets.map((target) => target.id)
5088
+ ].filter((id) => Boolean(id));
5089
+ const existing = targetId ? readExistingAiHealthReport(options.value.outDir, loaded.manifest.portfolioId) : void 0;
5090
+ const report = targetId ? mergePortfolioAiHealthReport(existing, latest, allRepositoryIds) : latest;
4351
5091
  const written = writePortfolioAiHealthReport(report, options.value.outDir);
4352
5092
  if (options.value.json) {
4353
5093
  console.log(JSON.stringify({ ok: true, ...written, summary: report.summary }, null, 2));
@@ -4398,12 +5138,28 @@ function runPortfolioDoctor(args) {
4398
5138
  const output = { configPath: loaded.configPath, ...result };
4399
5139
  if (options.value.json) {
4400
5140
  console.log(JSON.stringify(output, null, 2));
4401
- } else if (result.ok) {
4402
- console.log(`portfolio doctor passed (${targets.length} targets)`);
4403
5141
  } else {
4404
- for (const issue of result.hostTooling.issues) console.log(`${issue.host} ${issue.type}: ${issue.message}`);
5142
+ for (const warning of result.hostSsot.userSkills.issues) {
5143
+ console.log(`user host-ssot-warning: ${warning}`);
5144
+ }
4405
5145
  for (const target of result.targets) {
4406
- for (const issue of target.issues) console.log(`${target.id} ${issue.type}: ${issue.message}`);
5146
+ for (const warning of target.hostSsot.issues) {
5147
+ console.log(`${target.id} host-ssot-warning: ${warning}`);
5148
+ }
5149
+ if (target.verification.status === "attention") {
5150
+ console.log(`${target.id} verification-warning: missing ${target.verification.missing.join(", ")}`);
5151
+ }
5152
+ if (target.versions.status === "attention") {
5153
+ console.log(`${target.id} version-policy-warning: ${target.versions.attentionCount} drift(s)`);
5154
+ }
5155
+ }
5156
+ if (result.ok) {
5157
+ console.log(`portfolio doctor passed (${targets.length} targets)`);
5158
+ } else {
5159
+ for (const issue of result.hostTooling.issues) console.log(`${issue.host} ${issue.type}: ${issue.message}`);
5160
+ for (const target of result.targets) {
5161
+ for (const issue of target.issues) console.log(`${target.id} ${issue.type}: ${issue.message}`);
5162
+ }
4407
5163
  }
4408
5164
  }
4409
5165
  return result.ok ? 0 : 1;
@@ -4660,8 +5416,8 @@ function isHost2(value) {
4660
5416
  return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
4661
5417
  }
4662
5418
  function findPortfolioAgentAssetsDir(manifest) {
4663
- const agentAssetsDir = manifest?.executionEngine?.path ? join21(manifest.executionEngine.path, "agent-assets") : void 0;
4664
- return agentAssetsDir && existsSync22(join21(agentAssetsDir, "registry.json")) ? agentAssetsDir : void 0;
5419
+ const agentAssetsDir = manifest?.executionEngine?.path ? join25(manifest.executionEngine.path, "agent-assets") : void 0;
5420
+ return agentAssetsDir && existsSync24(join25(agentAssetsDir, "registry.json")) ? agentAssetsDir : void 0;
4665
5421
  }
4666
5422
  function printUsage4() {
4667
5423
  console.error("Usage:");
@@ -4669,12 +5425,24 @@ function printUsage4() {
4669
5425
  console.error(" pro-gov portfolio plan --config <path> [--target <id|all>] [--host codex|claude-code|gemini-cli|antigravity] [--json]");
4670
5426
  console.error(" pro-gov portfolio assets-check --config <path> [--target <id|all>] [--json]");
4671
5427
  console.error(" pro-gov portfolio doctor --config <path> [--target <id|all>] [--json]");
4672
- console.error(" pro-gov portfolio ai-health --config <path> --out <directory> [--secrets-root <directory>] [--json]");
5428
+ console.error(" pro-gov portfolio ai-health --config <path> --out <directory> [--target <id|all>] [--secrets-root <directory>] [--json]");
5429
+ }
5430
+ function readExistingAiHealthReport(outDir, portfolioId) {
5431
+ if (!outDir) return void 0;
5432
+ const path = join25(outDir, "portfolio-ai-health.json");
5433
+ if (!existsSync24(path)) return void 0;
5434
+ try {
5435
+ const value = JSON.parse(readFileSync18(path, "utf8"));
5436
+ if (!value || typeof value !== "object" || value.portfolioId !== portfolioId || !Array.isArray(value.repositories)) return void 0;
5437
+ return value;
5438
+ } catch {
5439
+ return void 0;
5440
+ }
4673
5441
  }
4674
5442
 
4675
5443
  // src/commands/sync.ts
4676
- import { existsSync as existsSync23, readFileSync as readFileSync16 } from "node:fs";
4677
- import { join as join22 } from "node:path";
5444
+ import { existsSync as existsSync25, lstatSync as lstatSync9, readFileSync as readFileSync19, readlinkSync as readlinkSync4 } from "node:fs";
5445
+ import { join as join26 } from "node:path";
4678
5446
  function runSync(args) {
4679
5447
  const check = args.includes("--check");
4680
5448
  if (!check) {
@@ -4702,8 +5470,9 @@ function runSync(args) {
4702
5470
  console.log("pro-gov sync check");
4703
5471
  console.log(`profile: ${profile}`);
4704
5472
  for (const file of planStarterFiles(profile)) {
4705
- const targetPath = join22(process.cwd(), file.targetPath);
4706
- if (!existsSync23(targetPath)) {
5473
+ const targetPath = join26(process.cwd(), file.targetPath);
5474
+ const stat = safeLstat3(targetPath);
5475
+ if (!stat) {
4707
5476
  if (file.ownership === "optional-guardrail") continue;
4708
5477
  console.log(`missing: ${file.targetPath}`);
4709
5478
  differences += 1;
@@ -4711,8 +5480,22 @@ function runSync(args) {
4711
5480
  }
4712
5481
  if (file.ownership === "optional-guardrail") continue;
4713
5482
  if (file.ownership === "project-local-seed") continue;
4714
- const source = readFileSync16(file.absoluteSourcePath, "utf8");
4715
- const target = readFileSync16(targetPath, "utf8");
5483
+ if (file.kind === "directory") {
5484
+ if (!stat.isDirectory()) {
5485
+ console.log(`different: ${file.targetPath}`);
5486
+ differences += 1;
5487
+ }
5488
+ continue;
5489
+ }
5490
+ if (file.kind === "symlink") {
5491
+ if (!stat.isSymbolicLink() || readlinkSync4(targetPath) !== file.linkTarget) {
5492
+ console.log(`different: ${file.targetPath}`);
5493
+ differences += 1;
5494
+ }
5495
+ continue;
5496
+ }
5497
+ const source = readFileSync19(file.absoluteSourcePath, "utf8");
5498
+ const target = readFileSync19(targetPath, "utf8");
4716
5499
  if (!matchesExpectedContent(file.targetPath, source, target)) {
4717
5500
  console.log(`different: ${file.targetPath}`);
4718
5501
  differences += 1;
@@ -4746,10 +5529,17 @@ function normalizeMarkdownTableCell(cell) {
4746
5529
  }
4747
5530
  function inferInstalledProfile(root) {
4748
5531
  const installed = ["engineering-runtime", "doc-only"].filter(
4749
- (profile) => existsSync23(join22(root, `docs/governance/agents-routing/${profile}-v1.0.md`))
5532
+ (profile) => existsSync25(join26(root, `docs/governance/agents-routing/${profile}-v1.1.md`))
4750
5533
  );
4751
5534
  return installed.length === 1 ? installed[0] : void 0;
4752
5535
  }
5536
+ function safeLstat3(path) {
5537
+ try {
5538
+ return lstatSync9(path);
5539
+ } catch {
5540
+ return void 0;
5541
+ }
5542
+ }
4753
5543
  function readFlag2(args, flag) {
4754
5544
  const index = args.indexOf(flag);
4755
5545
  const value = index >= 0 ? args[index + 1] : void 0;