@pieai/pro-gov 0.5.4 → 0.6.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,9 @@ 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)}`,
2474
2587
  `package-scripts: ${formatList(report.packageJson?.scripts ?? [])}`,
2475
2588
  `dependencies: ${formatList(report.packageJson?.dependencies ?? [])}`,
2476
2589
  `dev-dependencies: ${formatList(report.packageJson?.devDependencies ?? [])}`,
@@ -2503,6 +2616,15 @@ function renderProjectLensMarkdownReport(report) {
2503
2616
  "",
2504
2617
  bulletList(report.aiConfigFiles),
2505
2618
  "",
2619
+ "## AI Host SSOT",
2620
+ "",
2621
+ `- Status: ${report.hostSsot.compliant ? "compliant" : "non-compliant"}`,
2622
+ `- Canonical entry: \`${report.hostSsot.agentsEntry.path}\` (${report.hostSsot.agentsEntry.status})`,
2623
+ `- Claude entry: ${formatLink(report.hostSsot.claudeEntry)}`,
2624
+ `- Canonical skills: \`${report.hostSsot.canonicalSkills.path}\` (${report.hostSsot.canonicalSkills.status})`,
2625
+ `- Claude skills: ${formatLink(report.hostSsot.claudeSkills)}`,
2626
+ `- Issues: ${formatList(report.hostSsot.issues)}`,
2627
+ "",
2506
2628
  "## Package",
2507
2629
  "",
2508
2630
  `- Scripts: ${formatList(report.packageJson?.scripts ?? [])}`,
@@ -2539,11 +2661,141 @@ function bulletList(values) {
2539
2661
  if (values.length === 0) return "- none";
2540
2662
  return values.map((value) => `- \`${value}\``).join("\n");
2541
2663
  }
2664
+ function formatLink(link) {
2665
+ const rawTarget = link.rawTarget ? ` -> \`${link.rawTarget}\`` : "";
2666
+ const resolvedTarget = link.resolvedTarget ? `; resolved: \`${link.resolvedTarget}\`` : "";
2667
+ return `\`${link.path}\`${rawTarget} (${link.status}${resolvedTarget})`;
2668
+ }
2542
2669
 
2543
2670
  // src/lens/scan.ts
2544
2671
  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";
2672
+ import { existsSync as existsSync16, readdirSync as readdirSync7, readFileSync as readFileSync11, statSync as statSync3 } from "node:fs";
2673
+ import { join as join18, relative as relative7 } from "node:path";
2674
+
2675
+ // src/host-ssot.ts
2676
+ import {
2677
+ lstatSync as lstatSync6,
2678
+ readlinkSync as readlinkSync3,
2679
+ realpathSync as realpathSync3
2680
+ } from "node:fs";
2681
+ import { dirname as dirname9, isAbsolute as isAbsolute3, join as join17, resolve as resolve3 } from "node:path";
2682
+ function inspectProjectHostSsot(root) {
2683
+ const agentsEntry = inspectCanonicalPath(root, "AGENTS.md");
2684
+ const canonicalSkills = inspectCanonicalPath(root, ".agents/skills");
2685
+ const claudeEntry = inspectCompatibilityLink(root, "CLAUDE.md", "AGENTS.md");
2686
+ const claudeSkills = inspectCompatibilityLink(
2687
+ root,
2688
+ ".claude/skills",
2689
+ "../.agents/skills"
2690
+ );
2691
+ const issues = [
2692
+ ...agentsEntry.status === "file" ? [] : [`AGENTS.md must be the canonical project entry file; found ${agentsEntry.status}.`],
2693
+ ...canonicalSkills.status === "directory" ? [] : [`.agents/skills must be the canonical project skill directory; found ${canonicalSkills.status}.`],
2694
+ ...claudeEntry.compliant ? [] : [`CLAUDE.md must be the relative symlink AGENTS.md; found ${claudeEntry.status}.`],
2695
+ ...claudeSkills.compliant ? [] : [`.claude/skills must be the relative symlink ../.agents/skills; found ${claudeSkills.status}.`]
2696
+ ];
2697
+ return {
2698
+ agentsEntry,
2699
+ canonicalSkills,
2700
+ claudeEntry,
2701
+ claudeSkills,
2702
+ compliant: issues.length === 0,
2703
+ issues
2704
+ };
2705
+ }
2706
+ function inspectUserSkillsSsot(homeDir) {
2707
+ const canonicalSkills = inspectCanonicalPath(homeDir, ".agents/skills");
2708
+ const claudeSkills = inspectCompatibilityLink(
2709
+ homeDir,
2710
+ ".claude/skills",
2711
+ "../.agents/skills"
2712
+ );
2713
+ const issues = [
2714
+ ...canonicalSkills.status === "directory" ? [] : [`~/.agents/skills must be the canonical user skill directory; found ${canonicalSkills.status}.`],
2715
+ ...claudeSkills.compliant ? [] : [`~/.claude/skills must be the relative symlink ../.agents/skills; found ${claudeSkills.status}.`]
2716
+ ];
2717
+ return {
2718
+ canonicalSkills,
2719
+ claudeSkills,
2720
+ compliant: issues.length === 0,
2721
+ issues
2722
+ };
2723
+ }
2724
+ function inspectCanonicalPath(root, path) {
2725
+ const absolutePath = join17(root, path);
2726
+ const stat = safeLstat2(absolutePath);
2727
+ if (!stat) return { path, status: "missing" };
2728
+ if (stat.isSymbolicLink()) {
2729
+ try {
2730
+ realpathSync3(absolutePath);
2731
+ return { path, status: "symlink" };
2732
+ } catch {
2733
+ return { path, status: "dangling-symlink" };
2734
+ }
2735
+ }
2736
+ if (stat.isFile()) return { path, status: "file" };
2737
+ if (stat.isDirectory()) return { path, status: "directory" };
2738
+ return { path, status: "other" };
2739
+ }
2740
+ function inspectCompatibilityLink(root, path, expectedRawTarget) {
2741
+ const absolutePath = join17(root, path);
2742
+ const stat = safeLstat2(absolutePath);
2743
+ const base = { path, expectedRawTarget, compliant: false };
2744
+ if (!stat) return { ...base, status: "missing" };
2745
+ if (!stat.isSymbolicLink()) {
2746
+ if (stat.isFile()) return { ...base, status: "file" };
2747
+ if (stat.isDirectory()) return { ...base, status: "directory" };
2748
+ return { ...base, status: "other" };
2749
+ }
2750
+ const rawTarget = readlinkSync3(absolutePath);
2751
+ const resolvedTarget = resolve3(dirname9(absolutePath), rawTarget);
2752
+ const expectedPath = resolve3(dirname9(absolutePath), expectedRawTarget);
2753
+ const targetStat = safeLstat2(resolvedTarget);
2754
+ if (!targetStat) {
2755
+ return {
2756
+ ...base,
2757
+ rawTarget,
2758
+ resolvedTarget,
2759
+ status: rawTarget === expectedRawTarget ? "target-missing" : "dangling-symlink"
2760
+ };
2761
+ }
2762
+ let targetMatches = false;
2763
+ try {
2764
+ targetMatches = realpathSync3(absolutePath) === realpathSync3(expectedPath);
2765
+ } catch {
2766
+ return { ...base, rawTarget, resolvedTarget, status: "dangling-symlink" };
2767
+ }
2768
+ if (!targetMatches) {
2769
+ return { ...base, rawTarget, resolvedTarget, status: "wrong-target" };
2770
+ }
2771
+ if (isAbsolute3(rawTarget)) {
2772
+ return { ...base, rawTarget, resolvedTarget, status: "absolute-symlink" };
2773
+ }
2774
+ if (rawTarget !== expectedRawTarget) {
2775
+ return {
2776
+ ...base,
2777
+ rawTarget,
2778
+ resolvedTarget,
2779
+ status: "noncanonical-relative-symlink"
2780
+ };
2781
+ }
2782
+ return {
2783
+ ...base,
2784
+ rawTarget,
2785
+ resolvedTarget,
2786
+ status: "compliant-relative-symlink",
2787
+ compliant: true
2788
+ };
2789
+ }
2790
+ function safeLstat2(path) {
2791
+ try {
2792
+ return lstatSync6(path);
2793
+ } catch {
2794
+ return void 0;
2795
+ }
2796
+ }
2797
+
2798
+ // src/lens/scan.ts
2547
2799
  var ignoredDirectories = /* @__PURE__ */ new Set([
2548
2800
  ".git",
2549
2801
  ".next",
@@ -2569,22 +2821,23 @@ function scanProjectLensTarget(targetDir, options = {}) {
2569
2821
  excludedFileCount: candidateFiles.length - files.length
2570
2822
  },
2571
2823
  aiEntryFiles: ["AGENTS.md", "CLAUDE.md"].filter(
2572
- (file) => existsSync17(join17(targetDir, file))
2824
+ (file) => existsSync16(join18(targetDir, file))
2573
2825
  ),
2574
2826
  aiConfigFiles: [],
2827
+ hostSsot: inspectProjectHostSsot(targetDir),
2575
2828
  packageJson,
2576
2829
  docs: {
2577
- hasDocsDirectory: existsSync17(join17(targetDir, "docs")),
2830
+ hasDocsDirectory: existsSync16(join18(targetDir, "docs")),
2578
2831
  markdownFileCount: markdownFiles.length,
2579
2832
  governanceFiles: markdownFiles.filter((file) => file.startsWith("docs/governance/") || file.startsWith("docs/policy/")).sort()
2580
2833
  },
2581
2834
  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)
2835
+ largeFiles: files.map((file) => ({ path: file, bytes: statSync3(join18(targetDir, file)).size })).filter((file) => file.bytes >= largeFileBytes).sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path)).slice(0, 25)
2583
2836
  };
2584
2837
  }
2585
2838
  function readPackageJson(targetDir) {
2586
- const packageJsonPath = join17(targetDir, "package.json");
2587
- if (!existsSync17(packageJsonPath)) return void 0;
2839
+ const packageJsonPath = join18(targetDir, "package.json");
2840
+ if (!existsSync16(packageJsonPath)) return void 0;
2588
2841
  try {
2589
2842
  const packageJson = JSON.parse(readFileSync11(packageJsonPath, "utf8"));
2590
2843
  return {
@@ -2624,7 +2877,7 @@ function listProjectFiles(targetDir) {
2624
2877
  "-z"
2625
2878
  ]);
2626
2879
  if (gitFiles.ok) {
2627
- return gitFiles.stdout.split("\0").filter(Boolean).map(toUnixPath4).filter((file) => existsSync17(join17(targetDir, file))).sort();
2880
+ return gitFiles.stdout.split("\0").filter(Boolean).map(toUnixPath4).filter((file) => existsSync16(join18(targetDir, file))).sort();
2628
2881
  }
2629
2882
  const files = [];
2630
2883
  collectFiles2(targetDir, targetDir, files);
@@ -2642,13 +2895,13 @@ function isFirstPartyEvidenceFile(file) {
2642
2895
  return !excludedEvidencePrefixes.some((prefix) => file.startsWith(prefix));
2643
2896
  }
2644
2897
  function collectFiles2(rootDir, currentDir, files) {
2645
- if (!existsSync17(currentDir)) return;
2898
+ if (!existsSync16(currentDir)) return;
2646
2899
  for (const entry of readdirSync7(currentDir, { withFileTypes: true })) {
2647
2900
  if (entry.isDirectory()) {
2648
2901
  if (ignoredDirectories.has(entry.name)) continue;
2649
- collectFiles2(rootDir, join17(currentDir, entry.name), files);
2902
+ collectFiles2(rootDir, join18(currentDir, entry.name), files);
2650
2903
  } else if (entry.isFile()) {
2651
- files.push(toUnixPath4(relative7(rootDir, join17(currentDir, entry.name))));
2904
+ files.push(toUnixPath4(relative7(rootDir, join18(currentDir, entry.name))));
2652
2905
  }
2653
2906
  }
2654
2907
  }
@@ -2700,7 +2953,7 @@ function runLensReport(args) {
2700
2953
  }
2701
2954
  const report = scanProjectLensTarget(options.value.targetDir);
2702
2955
  const markdown = renderProjectLensMarkdownReport(report);
2703
- mkdirSync7(dirname9(options.value.outPath), { recursive: true });
2956
+ mkdirSync7(dirname10(options.value.outPath), { recursive: true });
2704
2957
  writeFileSync6(options.value.outPath, markdown);
2705
2958
  console.log(`report: ${options.value.outPath}`);
2706
2959
  return 0;
@@ -2804,12 +3057,12 @@ function printUsage3() {
2804
3057
  }
2805
3058
 
2806
3059
  // src/commands/portfolio.ts
2807
- import { existsSync as existsSync22 } from "node:fs";
2808
- import { join as join21 } from "node:path";
3060
+ import { existsSync as existsSync21 } from "node:fs";
3061
+ import { join as join22 } from "node:path";
2809
3062
 
2810
3063
  // 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";
3064
+ import { existsSync as existsSync17, readFileSync as readFileSync12 } from "node:fs";
3065
+ import { dirname as dirname11, isAbsolute as isAbsolute4, resolve as resolve4 } from "node:path";
2813
3066
  function loadPortfolioManifest(configPath) {
2814
3067
  let parsed;
2815
3068
  try {
@@ -2825,7 +3078,7 @@ function loadPortfolioManifest(configPath) {
2825
3078
  ]
2826
3079
  };
2827
3080
  }
2828
- const normalized = resolveManifestPaths(parsed, dirname10(resolve3(configPath)));
3081
+ const normalized = resolveManifestPaths(parsed, dirname11(resolve4(configPath)));
2829
3082
  const issues = validatePortfolioManifest(normalized);
2830
3083
  return {
2831
3084
  configPath,
@@ -2836,14 +3089,14 @@ function loadPortfolioManifest(configPath) {
2836
3089
  function resolveManifestPaths(value, configDir) {
2837
3090
  if (!isRecord(value)) return value;
2838
3091
  const resolveEndpoint = (endpoint) => {
2839
- if (!isRecord(endpoint) || typeof endpoint.path !== "string" || isAbsolute3(endpoint.path)) {
3092
+ if (!isRecord(endpoint) || typeof endpoint.path !== "string" || isAbsolute4(endpoint.path)) {
2840
3093
  return endpoint;
2841
3094
  }
2842
- return { ...endpoint, path: resolve3(configDir, endpoint.path) };
3095
+ return { ...endpoint, path: resolve4(configDir, endpoint.path) };
2843
3096
  };
2844
3097
  return {
2845
3098
  ...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,
3099
+ technologyGovernance: isRecord(value.technologyGovernance) && typeof value.technologyGovernance.strategySource === "string" && !isAbsolute4(value.technologyGovernance.strategySource) ? { ...value.technologyGovernance, strategySource: resolve4(configDir, value.technologyGovernance.strategySource) } : value.technologyGovernance,
2847
3100
  controlPlane: resolveEndpoint(value.controlPlane),
2848
3101
  executionEngine: resolveEndpoint(value.executionEngine),
2849
3102
  targets: Array.isArray(value.targets) ? value.targets.map(resolveEndpoint) : value.targets
@@ -2877,13 +3130,14 @@ function validatePortfolioManifest(value) {
2877
3130
  validateAllowedFields(
2878
3131
  value,
2879
3132
  "root",
2880
- ["schemaVersion", "portfolioId", "technologyGovernance", "controlPlane", "executionEngine", "hostTooling", "targets"],
3133
+ ["schemaVersion", "portfolioId", "technologyGovernance", "controlPlane", "executionEngine", "hostTooling", "specialistChecks", "targets"],
2881
3134
  issues
2882
3135
  );
2883
3136
  const technologyCatalog = validateTechnologyGovernance(value.technologyGovernance, issues);
2884
3137
  validateEndpoint(value.controlPlane, "controlPlane", issues, technologyCatalog);
2885
3138
  validateEndpoint(value.executionEngine, "executionEngine", issues, technologyCatalog);
2886
3139
  validateHostTooling(value.hostTooling, issues);
3140
+ validateSpecialistChecks(value.specialistChecks, issues);
2887
3141
  if (!Array.isArray(value.targets)) {
2888
3142
  issues.push({
2889
3143
  type: "invalid-field",
@@ -2981,7 +3235,7 @@ function validateEndpoint(value, field, issues, technologyCatalog) {
2981
3235
  });
2982
3236
  return;
2983
3237
  }
2984
- if (!existsSync18(value.path)) {
3238
+ if (!existsSync17(value.path)) {
2985
3239
  issues.push({
2986
3240
  type: "missing-path",
2987
3241
  id: typeof value.id === "string" ? value.id : void 0,
@@ -3076,13 +3330,13 @@ function validateTechnologyGovernance(value, issues) {
3076
3330
  return { technologies, projectTypes };
3077
3331
  }
3078
3332
  function isRepositoryRelativePath(value) {
3079
- if (value.length === 0 || isAbsolute3(value)) return false;
3333
+ if (value.length === 0 || isAbsolute4(value)) return false;
3080
3334
  const segments = value.replaceAll("\\", "/").split("/");
3081
3335
  return !segments.includes("..");
3082
3336
  }
3083
3337
  function isExactRepositoryRelativePath(value) {
3084
3338
  if (value.length === 0) return false;
3085
- if (isAbsolute3(value)) return false;
3339
+ if (isAbsolute4(value)) return false;
3086
3340
  if (/^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\") || value.startsWith("//")) return false;
3087
3341
  if (value.includes("\\")) return false;
3088
3342
  const segments = value.split("/");
@@ -3214,15 +3468,45 @@ function validateHostTooling(value, issues) {
3214
3468
  }
3215
3469
  }
3216
3470
  }
3471
+ function validateSpecialistChecks(value, issues) {
3472
+ if (value === void 0) return;
3473
+ if (!isRecord(value)) {
3474
+ issues.push({
3475
+ type: "invalid-field",
3476
+ field: "specialistChecks",
3477
+ message: "Portfolio specialistChecks must be an object."
3478
+ });
3479
+ return;
3480
+ }
3481
+ validateAllowedFields(value, "specialistChecks", ["devspace"], issues);
3482
+ if (value.devspace === void 0) return;
3483
+ if (!isRecord(value.devspace)) {
3484
+ issues.push({
3485
+ type: "invalid-field",
3486
+ field: "specialistChecks.devspace",
3487
+ message: "Portfolio specialistChecks.devspace must be an object."
3488
+ });
3489
+ return;
3490
+ }
3491
+ validateAllowedFields(value.devspace, "specialistChecks.devspace", ["expectedToolMode"], issues);
3492
+ if (value.devspace.expectedToolMode !== void 0 && !["minimal", "full", "codex"].includes(String(value.devspace.expectedToolMode))) {
3493
+ issues.push({
3494
+ type: "invalid-field",
3495
+ field: "specialistChecks.devspace.expectedToolMode",
3496
+ message: "DevSpace expectedToolMode must be minimal, full, or codex."
3497
+ });
3498
+ }
3499
+ }
3217
3500
  function isRecord(value) {
3218
3501
  return typeof value === "object" && value !== null && !Array.isArray(value);
3219
3502
  }
3220
3503
 
3221
3504
  // src/portfolio/doctor.ts
3222
3505
  import { spawnSync as spawnSync5 } from "node:child_process";
3223
- import { existsSync as existsSync20, readFileSync as readFileSync14 } from "node:fs";
3506
+ import { existsSync as existsSync19, readFileSync as readFileSync14 } from "node:fs";
3224
3507
  import { createRequire as createRequire2 } from "node:module";
3225
- import { dirname as dirname11, join as join19 } from "node:path";
3508
+ import { homedir } from "node:os";
3509
+ import { dirname as dirname12, join as join20 } from "node:path";
3226
3510
  import { fileURLToPath as fileURLToPath3 } from "node:url";
3227
3511
 
3228
3512
  // src/host-tooling/inventory.ts
@@ -3313,13 +3597,13 @@ function isRecord2(value) {
3313
3597
  }
3314
3598
 
3315
3599
  // 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";
3600
+ import { existsSync as existsSync18, lstatSync as lstatSync7, readFileSync as readFileSync13 } from "node:fs";
3601
+ import { join as join19 } from "node:path";
3318
3602
  function comparePortfolioAssetState(options) {
3319
3603
  const expectedManifest = readPlanDocument(options.expectedPlan, ".pro-gov/assets.json");
3320
3604
  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"));
3605
+ const currentManifest = readJsonFile(join19(options.targetDir, ".pro-gov/assets.json"));
3606
+ const currentLock = readJsonFile(join19(options.targetDir, ".pro-gov/assets.lock.json"));
3323
3607
  const issues = [];
3324
3608
  if (!sameStrings(currentManifest?.bundleIds, expectedManifest?.bundleIds)) {
3325
3609
  issues.push({
@@ -3342,7 +3626,8 @@ function comparePortfolioAssetState(options) {
3342
3626
  const expectedTargets = new Set((expectedLock?.assets ?? []).map((entry) => entry.targetPath));
3343
3627
  for (const entry of currentLock?.assets ?? []) {
3344
3628
  if (expectedTargets.has(entry.targetPath)) continue;
3345
- const targetAbsolutePath = join18(options.targetDir, entry.targetPath);
3629
+ if (options.expectedPlan.actions.some((action) => action.type === "adopt-symlink" && action.assetId === entry.id && action.legacyTargetPath === entry.targetPath)) continue;
3630
+ const targetAbsolutePath = join19(options.targetDir, entry.targetPath);
3346
3631
  if (!pathIsSymlink(targetAbsolutePath)) continue;
3347
3632
  issues.push({
3348
3633
  type: "orphaned-managed-symlink",
@@ -3364,7 +3649,7 @@ function readPlanDocument(plan, targetPath) {
3364
3649
  }
3365
3650
  }
3366
3651
  function readJsonFile(path) {
3367
- if (!existsSync19(path)) return void 0;
3652
+ if (!existsSync18(path)) return void 0;
3368
3653
  try {
3369
3654
  return JSON.parse(readFileSync13(path, "utf8"));
3370
3655
  } catch {
@@ -3387,7 +3672,7 @@ function normalizeLock(lock) {
3387
3672
  }
3388
3673
  function pathIsSymlink(path) {
3389
3674
  try {
3390
- return lstatSync5(path).isSymbolicLink();
3675
+ return lstatSync7(path).isSymbolicLink();
3391
3676
  } catch {
3392
3677
  return false;
3393
3678
  }
@@ -3409,17 +3694,19 @@ function inspectPortfolio(options) {
3409
3694
  portfolioId: options.manifest.portfolioId,
3410
3695
  expectedPackageVersions,
3411
3696
  hostTooling,
3697
+ hostSsot: { userSkills: inspectUserSkillsSsot(options.homeDir ?? homedir()) },
3412
3698
  targets
3413
3699
  };
3414
3700
  }
3415
3701
  function inspectTarget(options) {
3416
3702
  const { target } = options;
3703
+ const hostSsot = inspectProjectHostSsot(target.path);
3417
3704
  const issues = [];
3418
- const packageJson = readJson2(join19(target.path, "package.json"));
3705
+ const packageJson = readJson2(join20(target.path, "package.json"));
3419
3706
  const packages = {};
3420
3707
  for (const packageName of ["@pieai/pro-gov", "@pieai/doc-gov"]) {
3421
3708
  const declared = packageJson?.devDependencies?.[packageName] ?? packageJson?.dependencies?.[packageName];
3422
- const installedPackage = readJson2(join19(target.path, "node_modules", packageName, "package.json"));
3709
+ const installedPackage = readJson2(join20(target.path, "node_modules", packageName, "package.json"));
3423
3710
  const installed = installedPackage?.version;
3424
3711
  const expected = options.expectedPackageVersions[packageName];
3425
3712
  packages[packageName] = { declared, installed, expected };
@@ -3473,7 +3760,7 @@ function inspectTarget(options) {
3473
3760
  type: "asset-lock-drift",
3474
3761
  message: error instanceof Error ? error.message : String(error)
3475
3762
  });
3476
- if (!existsSync20(join19(target.path, ".pro-gov/assets.json"))) {
3763
+ if (!existsSync19(join20(target.path, ".pro-gov/assets.json"))) {
3477
3764
  issues.push({ type: "bundle-drift", message: "Target asset manifest is missing." });
3478
3765
  }
3479
3766
  }
@@ -3484,19 +3771,20 @@ function inspectTarget(options) {
3484
3771
  packages,
3485
3772
  git: inspectGit(target.path),
3486
3773
  checks,
3774
+ hostSsot,
3487
3775
  issues: deduplicateIssues(issues)
3488
3776
  };
3489
3777
  }
3490
3778
  function readTargetAssetHost(targetDir) {
3491
- const lockfile = readJson2(join19(targetDir, ".pro-gov/assets.lock.json"));
3779
+ const lockfile = readJson2(join20(targetDir, ".pro-gov/assets.lock.json"));
3492
3780
  return isAssetRegistryHost(lockfile?.host) ? lockfile.host : void 0;
3493
3781
  }
3494
3782
  function isAssetRegistryHost(value) {
3495
3783
  return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
3496
3784
  }
3497
3785
  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");
3786
+ const proGovCli = join20(target.path, "node_modules/@pieai/pro-gov/dist/cli.js");
3787
+ const docGovCli = join20(target.path, "node_modules/@pieai/doc-gov/dist/cli.js");
3500
3788
  const commands = [
3501
3789
  {
3502
3790
  name: "pro-gov doctor",
@@ -3507,7 +3795,7 @@ function runTargetChecks(target) {
3507
3795
  { name: "doc-gov scan --check", cli: docGovCli, args: ["scan", "--check"] }
3508
3796
  ];
3509
3797
  return commands.map((command2) => {
3510
- if (!existsSync20(command2.cli)) return { name: command2.name, status: null };
3798
+ if (!existsSync19(command2.cli)) return { name: command2.name, status: null };
3511
3799
  const result = spawnSync5(process.execPath, [command2.cli, ...command2.args], {
3512
3800
  cwd: target.path,
3513
3801
  encoding: "utf8",
@@ -3546,16 +3834,16 @@ function getExpectedPackageVersions() {
3546
3834
  };
3547
3835
  }
3548
3836
  function findOwnPackageJson() {
3549
- let current = dirname11(fileURLToPath3(import.meta.url));
3837
+ let current = dirname12(fileURLToPath3(import.meta.url));
3550
3838
  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);
3839
+ const candidate = join20(current, "package.json");
3840
+ if (existsSync19(candidate)) return candidate;
3841
+ current = dirname12(current);
3554
3842
  }
3555
3843
  return "";
3556
3844
  }
3557
3845
  function readJson2(path) {
3558
- if (!path || !existsSync20(path)) return void 0;
3846
+ if (!path || !existsSync19(path)) return void 0;
3559
3847
  try {
3560
3848
  return JSON.parse(readFileSync14(path, "utf8"));
3561
3849
  } catch {
@@ -3576,26 +3864,27 @@ function deduplicateIssues(issues) {
3576
3864
  import { execFileSync } from "node:child_process";
3577
3865
  import {
3578
3866
  cpSync as cpSync2,
3579
- existsSync as existsSync21,
3580
- lstatSync as lstatSync6,
3867
+ existsSync as existsSync20,
3868
+ lstatSync as lstatSync8,
3581
3869
  mkdirSync as mkdirSync8,
3582
3870
  readFileSync as readFileSync15,
3583
3871
  readdirSync as readdirSync8,
3584
- realpathSync as realpathSync2,
3872
+ realpathSync as realpathSync4,
3585
3873
  statSync as statSync4,
3586
3874
  writeFileSync as writeFileSync7
3587
3875
  } 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";
3876
+ import { homedir as homedir2 } from "node:os";
3877
+ import { dirname as dirname13, join as join21, relative as relative8, resolve as resolve5, sep } from "node:path";
3590
3878
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3879
+ var CURRENT_ROUTER_VERSION = "1.1";
3591
3880
  function inspectPortfolioAiHealth(options) {
3592
3881
  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();
3882
+ const secretsRoot = options.secretsRoot ?? join21(dirname13(options.manifest.controlPlane?.path ?? endpoints[0]?.endpoint.path ?? process.cwd()), ".secrets");
3883
+ const homeDir = options.homeDir ?? process.env.HOME ?? homedir2();
3595
3884
  const grokVersion = commandVersion("grok");
3596
3885
  const executionEngineRoot = options.manifest.executionEngine?.path;
3597
3886
  const skillRegistry = inspectSkillRegistry(executionEngineRoot);
3598
- const expectedPackageVersion = packageVersion(join20(executionEngineRoot ?? "", "packages/pro-gov/package.json"));
3887
+ const expectedPackageVersion = packageVersion(join21(executionEngineRoot ?? "", "packages/pro-gov/package.json"));
3599
3888
  const repositories = endpoints.map(({ endpoint, role }) => inspectRepository(
3600
3889
  endpoint,
3601
3890
  role,
@@ -3608,12 +3897,17 @@ function inspectPortfolioAiHealth(options) {
3608
3897
  const summary = { healthy: 0, attention: 0, unhealthy: 0 };
3609
3898
  for (const repository of repositories) summary[repository.status] += 1;
3610
3899
  return {
3611
- schemaVersion: 2,
3900
+ schemaVersion: 4,
3612
3901
  portfolioId: options.manifest.portfolioId,
3613
3902
  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.",
3903
+ 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
3904
  secretsRoot: inspectSecretsRoot(secretsRoot),
3616
- hostEnvironment: inspectHostEnvironment(homeDir, grokVersion),
3905
+ hostEnvironment: inspectHostEnvironment(
3906
+ homeDir,
3907
+ grokVersion,
3908
+ endpoints.map(({ endpoint }) => endpoint.path),
3909
+ options.manifest.specialistChecks?.devspace
3910
+ ),
3617
3911
  skillRegistry,
3618
3912
  technologyGovernance: {
3619
3913
  strategySource: options.manifest.technologyGovernance?.strategySource,
@@ -3628,15 +3922,15 @@ function writePortfolioAiHealthReport(report, outDir) {
3628
3922
  mkdirSync8(outDir, { recursive: true });
3629
3923
  const dashboardAssets = findDashboardAssets();
3630
3924
  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));
3925
+ const source = join21(dashboardAssets, file);
3926
+ if (!existsSync20(source)) throw new Error(`Portfolio dashboard asset is missing: ${source}`);
3927
+ cpSync2(source, join21(outDir, file));
3634
3928
  }
3635
- const jsonPath = join20(outDir, "portfolio-ai-health.json");
3636
- const htmlPath = join20(outDir, "index.html");
3929
+ const jsonPath = join21(outDir, "portfolio-ai-health.json");
3930
+ const htmlPath = join21(outDir, "index.html");
3637
3931
  writeFileSync7(jsonPath, `${JSON.stringify(report, null, 2)}
3638
3932
  `);
3639
- writeFileSync7(join20(outDir, "data.js"), `window.__PORTFOLIO_AI_HEALTH__ = ${safeJavaScriptJson(report)};
3933
+ writeFileSync7(join21(outDir, "data.js"), `window.__PORTFOLIO_AI_HEALTH__ = ${safeJavaScriptJson(report)};
3640
3934
  `);
3641
3935
  return { jsonPath, htmlPath };
3642
3936
  }
@@ -3647,7 +3941,7 @@ function collectEndpoints(manifest) {
3647
3941
  for (const target of manifest.targets) result.push({ endpoint: target, role: "target" });
3648
3942
  const seen = /* @__PURE__ */ new Set();
3649
3943
  return result.filter(({ endpoint }) => {
3650
- const key = resolve4(endpoint.path);
3944
+ const key = resolve5(endpoint.path);
3651
3945
  if (seen.has(key)) return false;
3652
3946
  seen.add(key);
3653
3947
  return true;
@@ -3659,13 +3953,14 @@ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackage
3659
3953
  const entries = inspectEntries(root);
3660
3954
  const grokInspection = inspectGrokProject(root, homeDir, grokVersion);
3661
3955
  const skills = inspectSkills(root, grokInspection);
3956
+ const hostSsot = inspectProjectHostSsot(root);
3662
3957
  const hooks = inspectHooks(root);
3663
3958
  const docs = inspectDocs(root, role === "execution-engine" ? void 0 : expectedPackageVersion);
3664
3959
  const mcp = {
3665
- codexProject: tomlMcpNames(join20(root, ".codex/config.toml")),
3666
- claudeCodeProjectShared: jsonObjectKeys(join20(root, ".mcp.json"), "mcpServers"),
3960
+ codexProject: tomlMcpNames(join21(root, ".codex/config.toml")),
3961
+ claudeCodeProjectShared: jsonObjectKeys(join21(root, ".mcp.json"), "mcpServers"),
3667
3962
  claudeCodeProjectLocal: claudeProjectLocalMcpNames(homeDir, root),
3668
- grokProject: tomlMcpNames(join20(root, ".grok/config.toml")),
3963
+ grokProject: tomlMcpNames(join21(root, ".grok/config.toml")),
3669
3964
  grokEffective: grokInspection.effectiveMcp,
3670
3965
  grokInspection: grokInspection.inspection
3671
3966
  };
@@ -3689,6 +3984,9 @@ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackage
3689
3984
  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
3985
  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
3986
  if (skills.claudeCompatibility === "dangling-symlink") recommendations.push("`.claude/skills` \u662F\u65AD\u5F00\u7684\u94FE\u63A5\u3002");
3987
+ 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`);
3988
+ 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`);
3989
+ 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
3990
  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
3991
  const liveEnv = secrets.repositoryEnvFiles.filter((file) => !file.template && !file.fixture);
3694
3992
  const envNeedsCentralReview = liveEnv.filter((file) => !file.localOnly);
@@ -3697,6 +3995,7 @@ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackage
3697
3995
  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
3996
  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
3997
  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`);
3998
+ 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
3999
  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
4000
  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
4001
  const missingBaseline = projectModel.baseline.filter((technology) => !technology.detected && !hasBaselineException(projectModel, technology.id));
@@ -3708,27 +4007,29 @@ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackage
3708
4007
  role,
3709
4008
  path: root,
3710
4009
  profile: "profile" in endpoint ? endpoint.profile : void 0,
3711
- status: deriveStatus(role, entries, git, hooks, skills, secrets, docs, projectModel, Boolean(technologyGovernance)),
4010
+ status: deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, Boolean(technologyGovernance)),
3712
4011
  recommendations,
3713
4012
  git,
3714
4013
  entries,
3715
4014
  hooks,
3716
4015
  mcp,
3717
4016
  skills,
4017
+ hostSsot,
3718
4018
  secrets,
3719
4019
  docs,
3720
4020
  projectModel
3721
4021
  };
3722
4022
  }
3723
- function deriveStatus(role, entries, git, hooks, skills, secrets, docs, projectModel, technologyGovernanceConfigured) {
4023
+ function deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, technologyGovernanceConfigured) {
3724
4024
  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
4025
  const missingBaseline = projectModel.baseline.some((technology) => !technology.detected && !hasBaselineException(projectModel, technology.id));
3726
4026
  const missingSelected = projectModel.optionalCapabilities.some((technology) => technology.selected && !technology.detected);
3727
4027
  const liveEnv = secrets.repositoryEnvFiles.filter((file) => !file.template && !file.fixture);
3728
4028
  const secretMaterializationNeedsReview = liveEnv.some((file) => !file.centralized && !file.localOnly);
3729
4029
  const packageVersionNeedsReview = docs.packages.expected !== void 0 && !docs.packages.aligned;
4030
+ const routerVersionNeedsReview = !docs.routerAligned;
3730
4031
  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";
4032
+ 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) return "attention";
3732
4033
  return "healthy";
3733
4034
  }
3734
4035
  function inspectProjectModel(root, endpoint, governance) {
@@ -3738,7 +4039,7 @@ function inspectProjectModel(root, endpoint, governance) {
3738
4039
  const detection = (id) => {
3739
4040
  const technology = technologyById.get(id);
3740
4041
  const packageMatch = technology?.packages?.some((name) => packages.has(name)) ?? false;
3741
- const fileMatch = technology?.files?.some((path) => existsSync21(join20(root, path))) ?? false;
4042
+ const fileMatch = technology?.files?.some((path) => existsSync20(join21(root, path))) ?? false;
3742
4043
  return { id, label: technology?.label ?? id, detected: packageMatch || fileMatch };
3743
4044
  };
3744
4045
  const selected = new Set(endpoint.capabilities ?? []);
@@ -3761,10 +4062,10 @@ function collectPackageNames(root) {
3761
4062
  try {
3762
4063
  files = splitLines(execFileSync("git", ["ls-files", "*package.json"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }));
3763
4064
  } catch {
3764
- if (existsSync21(join20(root, "package.json"))) files = ["package.json"];
4065
+ if (existsSync20(join21(root, "package.json"))) files = ["package.json"];
3765
4066
  }
3766
4067
  for (const file of files) {
3767
- const packageJson = readJson3(join20(root, file));
4068
+ const packageJson = readJson3(join21(root, file));
3768
4069
  if (!isRecord3(packageJson)) continue;
3769
4070
  for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
3770
4071
  const dependencies = packageJson[section];
@@ -3802,15 +4103,15 @@ function inspectGit2(root) {
3802
4103
  };
3803
4104
  }
3804
4105
  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");
4106
+ const agentsPath = join21(root, "AGENTS.md");
4107
+ const agents = !existsSync20(agentsPath) ? "missing" : safeRead(agentsPath).includes("PGS-ROUTER:BEGIN") ? "pgs-router" : "custom";
4108
+ const claudePath = join21(root, "CLAUDE.md");
3808
4109
  let claude = "missing";
3809
4110
  if (pathLexists(claudePath)) {
3810
- const info = lstatSync6(claudePath);
4111
+ const info = lstatSync8(claudePath);
3811
4112
  if (info.isSymbolicLink()) {
3812
4113
  try {
3813
- claude = realpathSync2(claudePath) === realpathSync2(agentsPath) ? "agents-symlink" : "custom";
4114
+ claude = realpathSync4(claudePath) === realpathSync4(agentsPath) ? "agents-symlink" : "custom";
3814
4115
  } catch {
3815
4116
  claude = "dangling-symlink";
3816
4117
  }
@@ -3822,12 +4123,12 @@ function inspectEntries(root) {
3822
4123
  return { agents, claude, gemini: inspectOptionalEntry(root, "GEMINI.md", agentsPath) };
3823
4124
  }
3824
4125
  function inspectOptionalEntry(root, filename, agentsPath) {
3825
- const path = join20(root, filename);
4126
+ const path = join21(root, filename);
3826
4127
  if (!pathLexists(path)) return "missing";
3827
- const info = lstatSync6(path);
4128
+ const info = lstatSync8(path);
3828
4129
  if (info.isSymbolicLink()) {
3829
4130
  try {
3830
- return realpathSync2(path) === realpathSync2(agentsPath) ? "agents-symlink" : "custom";
4131
+ return realpathSync4(path) === realpathSync4(agentsPath) ? "agents-symlink" : "custom";
3831
4132
  } catch {
3832
4133
  return "dangling-symlink";
3833
4134
  }
@@ -3836,7 +4137,7 @@ function inspectOptionalEntry(root, filename, agentsPath) {
3836
4137
  return /AGENTS\.md/.test(content) && content.length < 2e3 ? "thin-adapter" : "custom";
3837
4138
  }
3838
4139
  function inspectSkills(root, grokInspection) {
3839
- const lock = readJson3(join20(root, ".pro-gov/assets.lock.json"));
4140
+ const lock = readJson3(join21(root, ".pro-gov/assets.lock.json"));
3840
4141
  const managed = /* @__PURE__ */ new Set();
3841
4142
  const bundleIds = stringArray(isRecord3(lock) ? lock.bundleIds : void 0);
3842
4143
  if (isRecord3(lock) && Array.isArray(lock.assets)) {
@@ -3846,14 +4147,14 @@ function inspectSkills(root, grokInspection) {
3846
4147
  if (match) managed.add(match[1]);
3847
4148
  }
3848
4149
  }
3849
- const skillRoot = join20(root, ".agents/skills");
4150
+ const skillRoot = join21(root, ".agents/skills");
3850
4151
  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);
4152
+ const path = join21(skillRoot, name);
4153
+ const stat = lstatSync8(path);
3853
4154
  let kind = stat.isSymbolicLink() ? "symlink" : stat.isDirectory() ? "directory" : "file";
3854
4155
  if (stat.isSymbolicLink()) {
3855
4156
  try {
3856
- realpathSync2(path);
4157
+ realpathSync4(path);
3857
4158
  } catch {
3858
4159
  kind = "dangling-symlink";
3859
4160
  }
@@ -3874,20 +4175,20 @@ function inspectSkills(root, grokInspection) {
3874
4175
  },
3875
4176
  hosts: {
3876
4177
  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,
4178
+ claudeCodeProject: inspectSkillRoot(join21(root, ".claude/skills")).names.length,
4179
+ grokNativeProject: inspectSkillRoot(join21(root, ".grok/skills")).names.length,
3879
4180
  grokEffective: grokInspection.skills
3880
4181
  }
3881
4182
  };
3882
4183
  }
3883
4184
  function inspectClaudeSkillRoot(root) {
3884
- const path = join20(root, ".claude/skills");
4185
+ const path = join21(root, ".claude/skills");
3885
4186
  if (!pathLexists(path)) return "missing";
3886
- const stat = lstatSync6(path);
4187
+ const stat = lstatSync8(path);
3887
4188
  if (stat.isSymbolicLink()) {
3888
4189
  try {
3889
- const target = realpathSync2(path);
3890
- return target === realpathSync2(join20(root, ".agents/skills")) ? "shared-root" : "other";
4190
+ const target = realpathSync4(path);
4191
+ return target === realpathSync4(join21(root, ".agents/skills")) ? "shared-root" : "other";
3891
4192
  } catch {
3892
4193
  return "dangling-symlink";
3893
4194
  }
@@ -3895,7 +4196,7 @@ function inspectClaudeSkillRoot(root) {
3895
4196
  return stat.isDirectory() ? "duplicate-directory" : "other";
3896
4197
  }
3897
4198
  function inspectRepositorySecrets(root, id, secretsRoot, isRepository, environmentPolicy) {
3898
- const centralPath = join20(secretsRoot, id);
4199
+ const centralPath = join21(secretsRoot, id);
3899
4200
  const centralRealPath = safeRealpath(centralPath);
3900
4201
  const localOnlyReasons = new Map(
3901
4202
  (environmentPolicy?.localOnly ?? []).map((entry) => [entry.path, entry.reason])
@@ -3907,16 +4208,16 @@ function inspectRepositorySecrets(root, id, secretsRoot, isRepository, environme
3907
4208
  tracked: isRepository ? gitTracks(root, path) : false,
3908
4209
  template: isEnvironmentTemplate(path),
3909
4210
  fixture: isEnvironmentFixture(path),
3910
- symlink: lstatSync6(join20(root, path)).isSymbolicLink(),
3911
- centralized: pointsInside(join20(root, path), centralRealPath),
4211
+ symlink: lstatSync8(join21(root, path)).isSymbolicLink(),
4212
+ centralized: pointsInside(join21(root, path), centralRealPath),
3912
4213
  localOnly: localOnlyReason !== void 0,
3913
4214
  ...localOnlyReason !== void 0 ? { localOnlyReason } : {}
3914
4215
  };
3915
4216
  });
3916
4217
  return {
3917
- centralDirectory: existsSync21(centralPath) ? "present" : "absent",
3918
- centralMode: existsSync21(centralPath) ? modeString(statSync4(centralPath).mode) : void 0,
3919
- centralFiles: existsSync21(centralPath) ? collectCentralSecretFiles(centralPath) : [],
4218
+ centralDirectory: existsSync20(centralPath) ? "present" : "absent",
4219
+ centralMode: existsSync20(centralPath) ? modeString(statSync4(centralPath).mode) : void 0,
4220
+ centralFiles: existsSync20(centralPath) ? collectCentralSecretFiles(centralPath) : [],
3920
4221
  repositoryEnvFiles: envFiles
3921
4222
  };
3922
4223
  }
@@ -3927,9 +4228,9 @@ function collectEnvironmentFiles(root, current = root, depth = 0) {
3927
4228
  try {
3928
4229
  for (const entry of readdirSync8(current, { withFileTypes: true })) {
3929
4230
  if (entry.isDirectory()) {
3930
- if (!SKIP_ENV_DIRECTORIES.has(entry.name)) found.push(...collectEnvironmentFiles(root, join20(current, entry.name), depth + 1));
4231
+ if (!SKIP_ENV_DIRECTORIES.has(entry.name)) found.push(...collectEnvironmentFiles(root, join21(current, entry.name), depth + 1));
3931
4232
  } else if (isEnvironmentFilename(entry.name) && !isProviderGeneratedEnvironmentFile(entry.name)) {
3932
- found.push(relative8(root, join20(current, entry.name)));
4233
+ found.push(relative8(root, join21(current, entry.name)));
3933
4234
  }
3934
4235
  }
3935
4236
  } catch {
@@ -3942,9 +4243,9 @@ function collectCentralSecretFiles(root, current = root, depth = 0) {
3942
4243
  const found = [];
3943
4244
  try {
3944
4245
  for (const entry of readdirSync8(current, { withFileTypes: true })) {
3945
- const path = join20(current, entry.name);
4246
+ const path = join21(current, entry.name);
3946
4247
  if (entry.isDirectory()) found.push(...collectCentralSecretFiles(root, path, depth + 1));
3947
- else found.push({ path: relative8(root, path), mode: modeString(lstatSync6(path).mode) });
4248
+ else found.push({ path: relative8(root, path), mode: modeString(lstatSync8(path).mode) });
3948
4249
  }
3949
4250
  } catch {
3950
4251
  return found;
@@ -3969,13 +4270,13 @@ function isEnvironmentFixture(path) {
3969
4270
  }
3970
4271
  function safeRealpath(path) {
3971
4272
  try {
3972
- return realpathSync2(path);
4273
+ return realpathSync4(path);
3973
4274
  } catch {
3974
4275
  return void 0;
3975
4276
  }
3976
4277
  }
3977
4278
  function pointsInside(path, expectedRoot) {
3978
- if (!expectedRoot || !lstatSync6(path).isSymbolicLink()) return false;
4279
+ if (!expectedRoot || !lstatSync8(path).isSymbolicLink()) return false;
3979
4280
  const target = safeRealpath(path);
3980
4281
  if (!target) return false;
3981
4282
  const fromRoot = relative8(expectedRoot, target);
@@ -3985,24 +4286,25 @@ function hasUnsafeCentralSecretPermissions(secrets) {
3985
4286
  return secrets.centralDirectory === "present" && (secrets.centralMode !== "700" || secrets.centralFiles.some((file) => file.mode !== "600"));
3986
4287
  }
3987
4288
  function inspectSecretsRoot(path) {
3988
- return existsSync21(path) ? { path, exists: true, mode: modeString(statSync4(path).mode) } : { path, exists: false };
4289
+ return existsSync20(path) ? { path, exists: true, mode: modeString(statSync4(path).mode) } : { path, exists: false };
3989
4290
  }
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 {
4291
+ function inspectHostEnvironment(homeDir, grokVersion, repositoryPaths, devspaceSettings) {
4292
+ const codexConfig = join21(homeDir, ".codex/config.toml");
4293
+ const claudeConfig = join21(homeDir, ".claude.json");
4294
+ const grokConfig = join21(homeDir, ".grok/config.toml");
4295
+ const hostEnvironment = {
3995
4296
  mcp: {
3996
4297
  codexUser: { path: codexConfig, names: tomlMcpNames(codexConfig) },
3997
4298
  claudeCodeUser: { path: claudeConfig, names: jsonObjectKeys(claudeConfig, "mcpServers") },
3998
4299
  grokUser: { path: grokConfig, names: tomlMcpNames(grokConfig) }
3999
4300
  },
4000
4301
  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"))
4302
+ codexUser: inspectSkillRoot(join21(homeDir, ".agents/skills")),
4303
+ claudeCodeUser: inspectSkillRoot(join21(homeDir, ".claude/skills")),
4304
+ grokUser: inspectSkillRoot(join21(homeDir, ".grok/skills")),
4305
+ grokAgentsCompatibility: inspectSkillRoot(join21(homeDir, ".agents/skills")),
4306
+ grokClaudeCompatibility: inspectSkillRoot(join21(homeDir, ".claude/skills")),
4307
+ ssot: inspectUserSkillsSsot(homeDir)
4006
4308
  },
4007
4309
  grok: {
4008
4310
  available: grokVersion !== void 0,
@@ -4010,20 +4312,121 @@ function inspectHostEnvironment(homeDir, grokVersion) {
4010
4312
  inspectionCommand: "grok inspect --json"
4011
4313
  }
4012
4314
  };
4315
+ if (devspaceSettings) {
4316
+ hostEnvironment.devspace = inspectDevSpaceHealth({
4317
+ homeDir,
4318
+ repositoryPaths,
4319
+ expectedToolMode: devspaceSettings.expectedToolMode
4320
+ });
4321
+ }
4322
+ return hostEnvironment;
4323
+ }
4324
+ function inspectDevSpaceHealth(options) {
4325
+ const run = options.run ?? runDevSpaceCommand;
4326
+ const configDirectory = join21(options.homeDir, ".devspace");
4327
+ const configPath = join21(configDirectory, "config.json");
4328
+ const authPath = join21(configDirectory, "auth.json");
4329
+ const installedResult = run("devspace", ["--version"], 3e3);
4330
+ const installedVersion = installedResult.ok ? installedResult.stdout.match(/\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\b/)?.[0] : void 0;
4331
+ const latestResult = run("npm", ["view", "@waishnav/devspace", "version", "--registry=https://registry.npmjs.org/"], 5e3);
4332
+ const latestVersion = latestResult.ok ? latestResult.stdout.match(/\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\b/)?.[0] : void 0;
4333
+ const processResult = run("pgrep", ["-f", "devspace serve"], 3e3);
4334
+ const pid = processResult.ok ? processResult.stdout.match(/\b\d+\b/)?.[0] : void 0;
4335
+ const processEnvironment = pid ? run("ps", ["eww", "-p", pid, "-o", "command="], 3e3) : void 0;
4336
+ const processToolMode = processEnvironment?.ok ? processEnvironment.stdout.match(/(?:^|\s)DEVSPACE_TOOL_MODE=(minimal|full|codex)(?:\s|$)/)?.[1] : void 0;
4337
+ const doctor = !installedResult.ok ? "unavailable" : run("devspace", ["doctor"], 1e4).ok ? "ok" : "failed";
4338
+ const configValue = readJson3(configPath);
4339
+ const configExists = isRecord3(configValue);
4340
+ const allowedRoots = configExists && Array.isArray(configValue.allowedRoots) ? configValue.allowedRoots.filter((value) => typeof value === "string") : [];
4341
+ const portfolioCoverage = !configExists || allowedRoots.length === 0 ? "unknown" : options.repositoryPaths.every((repositoryPath) => allowedRoots.some((root) => isPathInside(repositoryPath, root))) ? "complete" : "partial";
4342
+ const bind = configExists && typeof configValue.host === "string" ? isLoopbackHost(configValue.host) ? "loopback" : "non-loopback" : "unknown";
4343
+ const directoryMode = existsSync20(configDirectory) ? modeString(statSync4(configDirectory).mode) : void 0;
4344
+ const fileMode = existsSync20(configPath) ? modeString(statSync4(configPath).mode) : void 0;
4345
+ const authMode = existsSync20(authPath) ? modeString(statSync4(authPath).mode) : void 0;
4346
+ const update = installedVersion && latestVersion ? installedVersion === latestVersion ? "current" : "available" : "unknown";
4347
+ const recommendations = [];
4348
+ let status = "healthy";
4349
+ const unhealthy = (message) => {
4350
+ status = "unhealthy";
4351
+ recommendations.push(message);
4352
+ };
4353
+ const attention = (message) => {
4354
+ if (status === "healthy") status = "attention";
4355
+ recommendations.push(message);
4356
+ };
4357
+ if (!installedResult.ok) unhealthy("\u672C\u673A\u672A\u53D1\u73B0 DevSpace\uFF1B\u65E0\u6CD5\u4F7F\u7528\u5BBF\u4E3B\u5DE5\u4F5C\u533A\u670D\u52A1\u3002");
4358
+ if (!configExists) unhealthy("\u7F3A\u5C11 ~/.devspace/config.json\u3002");
4359
+ if (!existsSync20(authPath)) unhealthy("\u7F3A\u5C11 ~/.devspace/auth.json\u3002");
4360
+ if (directoryMode && directoryMode !== "700") unhealthy(`~/.devspace \u76EE\u5F55\u6743\u9650\u4E3A ${directoryMode}\uFF0C\u5E94\u6536\u7D27\u4E3A 700\u3002`);
4361
+ if (fileMode && fileMode !== "600") unhealthy(`DevSpace \u914D\u7F6E\u6587\u4EF6\u6743\u9650\u4E3A ${fileMode}\uFF0C\u5E94\u4E3A 600\u3002`);
4362
+ if (authMode && authMode !== "600") unhealthy(`DevSpace \u8BA4\u8BC1\u6587\u4EF6\u6743\u9650\u4E3A ${authMode}\uFF0C\u5E94\u4E3A 600\u3002`);
4363
+ 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");
4364
+ if (doctor === "failed") unhealthy("devspace doctor \u672A\u901A\u8FC7\u3002");
4365
+ if (portfolioCoverage === "partial") attention("DevSpace allowedRoots \u6CA1\u6709\u8986\u76D6\u5168\u90E8\u5DF2\u767B\u8BB0\u4ED3\u5E93\u3002");
4366
+ if (installedResult.ok && !pid) attention("DevSpace \u5DF2\u5B89\u88C5\u4F46\u5F53\u524D\u6CA1\u6709\u8FD0\u884C\u3002");
4367
+ if (options.expectedToolMode && pid && processToolMode !== options.expectedToolMode) {
4368
+ attention(`\u8FD0\u884C\u4E2D\u7684 DevSpace \u5DE5\u5177\u6A21\u5F0F\u4E3A ${processToolMode ?? "unknown"}\uFF0C\u671F\u671B ${options.expectedToolMode}\u3002`);
4369
+ }
4370
+ 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`);
4371
+ return {
4372
+ status,
4373
+ installed: installedResult.ok,
4374
+ ...installedVersion ? { installedVersion } : {},
4375
+ ...latestVersion ? { latestVersion } : {},
4376
+ update,
4377
+ doctor,
4378
+ running: Boolean(pid),
4379
+ ...options.expectedToolMode ? { expectedToolMode: options.expectedToolMode } : {},
4380
+ processToolMode: processToolMode ?? "unknown",
4381
+ config: {
4382
+ exists: configExists,
4383
+ ...directoryMode ? { directoryMode } : {},
4384
+ ...fileMode ? { fileMode } : {},
4385
+ authExists: existsSync20(authPath),
4386
+ ...authMode ? { authMode } : {},
4387
+ bind,
4388
+ portValid: configExists && typeof configValue.port === "number" && Number.isInteger(configValue.port) && configValue.port > 0 && configValue.port <= 65535,
4389
+ publicHttpsConfigured: configExists && typeof configValue.publicBaseUrl === "string" && configValue.publicBaseUrl.startsWith("https://"),
4390
+ allowedRootCount: allowedRoots.length,
4391
+ portfolioCoverage
4392
+ },
4393
+ recommendations
4394
+ };
4395
+ }
4396
+ function runDevSpaceCommand(command2, args, timeout) {
4397
+ try {
4398
+ return {
4399
+ ok: true,
4400
+ stdout: execFileSync(command2, args, {
4401
+ encoding: "utf8",
4402
+ stdio: ["ignore", "pipe", "ignore"],
4403
+ timeout
4404
+ })
4405
+ };
4406
+ } catch {
4407
+ return { ok: false, stdout: "" };
4408
+ }
4409
+ }
4410
+ function isLoopbackHost(host) {
4411
+ return ["127.0.0.1", "localhost", "::1"].includes(host.trim().toLowerCase());
4412
+ }
4413
+ function isPathInside(path, root) {
4414
+ const fromRoot = relative8(resolve5(root), resolve5(path));
4415
+ return fromRoot === "" || fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`);
4013
4416
  }
4014
4417
  function inspectSkillRoot(path) {
4015
4418
  const exists = pathLexists(path) && safeIsDirectory(path);
4016
- const names = exists ? safeReadDir(path).filter((name) => !name.startsWith(".") && existsSync21(join20(path, name, "SKILL.md"))) : [];
4419
+ const names = exists ? safeReadDir(path).filter((name) => !name.startsWith(".") && existsSync20(join21(path, name, "SKILL.md"))) : [];
4017
4420
  return { path, exists, names };
4018
4421
  }
4019
4422
  function claudeProjectLocalMcpNames(homeDir, root) {
4020
4423
  if (!homeDir) return [];
4021
- const value = readJson3(join20(homeDir, ".claude.json"));
4424
+ const value = readJson3(join21(homeDir, ".claude.json"));
4022
4425
  if (!isRecord3(value) || !isRecord3(value.projects)) return [];
4023
- const candidates = new Set([resolve4(root), safeRealpath(root)].filter((path) => Boolean(path)));
4426
+ const candidates = new Set([resolve5(root), safeRealpath(root)].filter((path) => Boolean(path)));
4024
4427
  const names = /* @__PURE__ */ new Set();
4025
4428
  for (const [path, project] of Object.entries(value.projects)) {
4026
- const projectPaths = [resolve4(path), safeRealpath(path)].filter((candidate) => Boolean(candidate));
4429
+ const projectPaths = [resolve5(path), safeRealpath(path)].filter((candidate) => Boolean(candidate));
4027
4430
  if (!projectPaths.some((candidate) => candidates.has(candidate)) || !isRecord3(project) || !isRecord3(project.mcpServers)) continue;
4028
4431
  for (const name of Object.keys(project.mcpServers)) names.add(name);
4029
4432
  }
@@ -4054,13 +4457,13 @@ function inspectGrokProject(root, homeDir, grokVersion) {
4054
4457
  const value = JSON.parse(execFileSync("grok", ["inspect", "--json"], {
4055
4458
  cwd: root,
4056
4459
  encoding: "utf8",
4057
- env: { ...process.env, HOME: homeDir, GROK_HOME: join20(homeDir, ".grok") },
4460
+ env: { ...process.env, HOME: homeDir, GROK_HOME: join21(homeDir, ".grok") },
4058
4461
  maxBuffer: 10 * 1024 * 1024,
4059
4462
  stdio: ["ignore", "pipe", "ignore"],
4060
4463
  timeout: 8e3
4061
4464
  }));
4062
4465
  if (!isRecord3(value)) return empty("failed");
4063
- const userClaudeNames = new Set(jsonObjectKeys(join20(homeDir, ".claude.json"), "mcpServers"));
4466
+ const userClaudeNames = new Set(jsonObjectKeys(join21(homeDir, ".claude.json"), "mcpServers"));
4064
4467
  const localClaudeNames = new Set(claudeProjectLocalMcpNames(homeDir, root));
4065
4468
  const effectiveMcp = Array.isArray(value.mcpServers) ? value.mcpServers.flatMap((item) => {
4066
4469
  if (!isRecord3(item) || typeof item.name !== "string") return [];
@@ -4116,15 +4519,15 @@ function inferGrokMcpScope(name, sourceType, sourcePath, root, homeDir, userClau
4116
4519
  if (userClaudeNames.has(name)) return "user";
4117
4520
  return "unknown";
4118
4521
  }
4119
- const resolvedSource = safeRealpath(sourcePath) ?? resolve4(sourcePath);
4120
- const resolvedRoot = safeRealpath(root) ?? resolve4(root);
4121
- if (resolvedSource === join20(resolvedRoot, ".mcp.json")) return "project-shared";
4522
+ const resolvedSource = safeRealpath(sourcePath) ?? resolve5(sourcePath);
4523
+ const resolvedRoot = safeRealpath(root) ?? resolve5(root);
4524
+ if (resolvedSource === join21(resolvedRoot, ".mcp.json")) return "project-shared";
4122
4525
  if (resolvedSource.startsWith(resolvedRoot + sep)) return "project";
4123
- if (homeDir && resolvedSource === join20(resolve4(homeDir), ".claude.json")) {
4526
+ if (homeDir && resolvedSource === join21(resolve5(homeDir), ".claude.json")) {
4124
4527
  if (localClaudeNames.has(name)) return "project-local";
4125
4528
  if (userClaudeNames.has(name)) return "user";
4126
4529
  }
4127
- if (homeDir && resolvedSource.startsWith(resolve4(homeDir) + sep)) return "user";
4530
+ if (homeDir && resolvedSource.startsWith(resolve5(homeDir) + sep)) return "user";
4128
4531
  if (sourceType === "project") return "project";
4129
4532
  return "unknown";
4130
4533
  }
@@ -4155,7 +4558,7 @@ function inspectHooks(root) {
4155
4558
  { host: "codex", path: ".codex/hooks.json" }
4156
4559
  ];
4157
4560
  return configs.map((config) => {
4158
- const value = readJson3(join20(root, config.path));
4561
+ const value = readJson3(join21(root, config.path));
4159
4562
  const counts = /* @__PURE__ */ new Map();
4160
4563
  collectHookEvents(value, counts);
4161
4564
  return {
@@ -4176,16 +4579,18 @@ function collectHookEvents(value, counts) {
4176
4579
  }
4177
4580
  }
4178
4581
  function inspectDocs(root, expected) {
4179
- const packageJson = readJson3(join20(root, "package.json"));
4582
+ const packageJson = readJson3(join21(root, "package.json"));
4180
4583
  const dependencies = isRecord3(packageJson) ? { ...recordOrEmpty(packageJson.dependencies), ...recordOrEmpty(packageJson.devDependencies) } : {};
4181
4584
  const docGov = dependencyVersion(dependencies["@pieai/doc-gov"]);
4182
4585
  const proGov = dependencyVersion(dependencies["@pieai/pro-gov"]);
4183
- const routerMatch = safeRead(join20(root, "AGENTS.md")).match(/PGS-ROUTER:BEGIN\s+v([0-9.]+)/);
4586
+ const routerMatch = safeRead(join21(root, "AGENTS.md")).match(/PGS-ROUTER:BEGIN\s+v([0-9.]+)/);
4184
4587
  const declared = [docGov, proGov].filter((value) => Boolean(value));
4185
4588
  return {
4186
4589
  routerVersion: routerMatch?.[1],
4187
- manifest: existsSync21(join20(root, "docs/governance/MANIFEST.yml")),
4188
- currentWork: existsSync21(join20(root, "docs/reference/execution/current-work.md")),
4590
+ expectedRouterVersion: CURRENT_ROUTER_VERSION,
4591
+ routerAligned: routerMatch?.[1] === CURRENT_ROUTER_VERSION,
4592
+ manifest: existsSync20(join21(root, "docs/governance/MANIFEST.yml")),
4593
+ currentWork: existsSync20(join21(root, "docs/reference/execution/current-work.md")),
4189
4594
  packages: {
4190
4595
  expected,
4191
4596
  docGov,
@@ -4208,20 +4613,20 @@ function recordOrEmpty(value) {
4208
4613
  }
4209
4614
  function inspectSkillRegistry(executionEngineRoot) {
4210
4615
  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"));
4616
+ const agentAssetsRoot = join21(executionEngineRoot, "agent-assets");
4617
+ const registry = readJson3(join21(agentAssetsRoot, "registry.json"));
4213
4618
  const assets = isRecord3(registry) && Array.isArray(registry.assets) ? registry.assets : [];
4214
4619
  const registeredSkills = assets.filter((asset) => isRecord3(asset) && asset.kind === "skill");
4215
- const bundleRoot = join20(agentAssetsRoot, "bundles");
4620
+ const bundleRoot = join21(agentAssetsRoot, "bundles");
4216
4621
  const bundleFiles = safeReadDir(bundleRoot).filter((file) => file.endsWith(".json"));
4217
4622
  const bundledIds = /* @__PURE__ */ new Set();
4218
4623
  for (const file of bundleFiles) {
4219
- const bundle = readJson3(join20(bundleRoot, file));
4624
+ const bundle = readJson3(join21(bundleRoot, file));
4220
4625
  if (!isRecord3(bundle) || !Array.isArray(bundle.assets)) continue;
4221
4626
  for (const id of bundle.assets) if (typeof id === "string") bundledIds.add(id);
4222
4627
  }
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);
4628
+ const sourceRoots = [join21(agentAssetsRoot, "skills/pie-skills"), join21(agentAssetsRoot, "skills/npx-skills/.agents/skills")];
4629
+ const source = sourceRoots.reduce((count, root) => count + safeReadDir(root).filter((name) => existsSync20(join21(root, name, "SKILL.md"))).length, 0);
4225
4630
  return {
4226
4631
  source,
4227
4632
  registered: registeredSkills.length,
@@ -4235,7 +4640,7 @@ function jsonObjectKeys(path, key) {
4235
4640
  return Object.keys(value[key]).sort();
4236
4641
  }
4237
4642
  function tomlMcpNames(path) {
4238
- if (!existsSync21(path)) return [];
4643
+ if (!existsSync20(path)) return [];
4239
4644
  const names = /* @__PURE__ */ new Set();
4240
4645
  for (const line of safeRead(path).split(/\r?\n/)) {
4241
4646
  const match = line.match(/^\s*\[mcp_servers\.(?:"([^"]+)"|([^\.\]]+))\]\s*$/);
@@ -4274,7 +4679,7 @@ function safeIsDirectory(path) {
4274
4679
  }
4275
4680
  function pathLexists(path) {
4276
4681
  try {
4277
- lstatSync6(path);
4682
+ lstatSync8(path);
4278
4683
  return true;
4279
4684
  } catch {
4280
4685
  return false;
@@ -4301,14 +4706,17 @@ function isRecord3(value) {
4301
4706
  return typeof value === "object" && value !== null && !Array.isArray(value);
4302
4707
  }
4303
4708
  function findDashboardAssets() {
4304
- const packageRoot2 = dirname12(dirname12(fileURLToPath4(import.meta.url)));
4709
+ const packageRoot2 = dirname13(dirname13(fileURLToPath4(import.meta.url)));
4305
4710
  const candidates = [
4306
4711
  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")
4712
+ join21(packageRoot2, ".dashboard-build"),
4713
+ join21(packageRoot2, "assets/portfolio-dashboard"),
4714
+ join21(process.cwd(), ".dashboard-build"),
4715
+ join21(process.cwd(), "assets/portfolio-dashboard"),
4716
+ join21(process.cwd(), "packages/pro-gov/.dashboard-build"),
4717
+ join21(process.cwd(), "packages/pro-gov/assets/portfolio-dashboard")
4310
4718
  ].filter((value) => Boolean(value));
4311
- const match = candidates.find((path) => existsSync21(join20(path, "index.html")));
4719
+ const match = candidates.find((path) => existsSync20(join21(path, "index.html")));
4312
4720
  if (!match) throw new Error("Portfolio dashboard assets were not built. Run pnpm --filter @pieai/pro-gov build.");
4313
4721
  return match;
4314
4722
  }
@@ -4398,12 +4806,22 @@ function runPortfolioDoctor(args) {
4398
4806
  const output = { configPath: loaded.configPath, ...result };
4399
4807
  if (options.value.json) {
4400
4808
  console.log(JSON.stringify(output, null, 2));
4401
- } else if (result.ok) {
4402
- console.log(`portfolio doctor passed (${targets.length} targets)`);
4403
4809
  } else {
4404
- for (const issue of result.hostTooling.issues) console.log(`${issue.host} ${issue.type}: ${issue.message}`);
4810
+ for (const warning of result.hostSsot.userSkills.issues) {
4811
+ console.log(`user host-ssot-warning: ${warning}`);
4812
+ }
4405
4813
  for (const target of result.targets) {
4406
- for (const issue of target.issues) console.log(`${target.id} ${issue.type}: ${issue.message}`);
4814
+ for (const warning of target.hostSsot.issues) {
4815
+ console.log(`${target.id} host-ssot-warning: ${warning}`);
4816
+ }
4817
+ }
4818
+ if (result.ok) {
4819
+ console.log(`portfolio doctor passed (${targets.length} targets)`);
4820
+ } else {
4821
+ for (const issue of result.hostTooling.issues) console.log(`${issue.host} ${issue.type}: ${issue.message}`);
4822
+ for (const target of result.targets) {
4823
+ for (const issue of target.issues) console.log(`${target.id} ${issue.type}: ${issue.message}`);
4824
+ }
4407
4825
  }
4408
4826
  }
4409
4827
  return result.ok ? 0 : 1;
@@ -4660,8 +5078,8 @@ function isHost2(value) {
4660
5078
  return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
4661
5079
  }
4662
5080
  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;
5081
+ const agentAssetsDir = manifest?.executionEngine?.path ? join22(manifest.executionEngine.path, "agent-assets") : void 0;
5082
+ return agentAssetsDir && existsSync21(join22(agentAssetsDir, "registry.json")) ? agentAssetsDir : void 0;
4665
5083
  }
4666
5084
  function printUsage4() {
4667
5085
  console.error("Usage:");
@@ -4673,8 +5091,8 @@ function printUsage4() {
4673
5091
  }
4674
5092
 
4675
5093
  // src/commands/sync.ts
4676
- import { existsSync as existsSync23, readFileSync as readFileSync16 } from "node:fs";
4677
- import { join as join22 } from "node:path";
5094
+ import { existsSync as existsSync22, lstatSync as lstatSync9, readFileSync as readFileSync16, readlinkSync as readlinkSync4 } from "node:fs";
5095
+ import { join as join23 } from "node:path";
4678
5096
  function runSync(args) {
4679
5097
  const check = args.includes("--check");
4680
5098
  if (!check) {
@@ -4702,8 +5120,9 @@ function runSync(args) {
4702
5120
  console.log("pro-gov sync check");
4703
5121
  console.log(`profile: ${profile}`);
4704
5122
  for (const file of planStarterFiles(profile)) {
4705
- const targetPath = join22(process.cwd(), file.targetPath);
4706
- if (!existsSync23(targetPath)) {
5123
+ const targetPath = join23(process.cwd(), file.targetPath);
5124
+ const stat = safeLstat3(targetPath);
5125
+ if (!stat) {
4707
5126
  if (file.ownership === "optional-guardrail") continue;
4708
5127
  console.log(`missing: ${file.targetPath}`);
4709
5128
  differences += 1;
@@ -4711,6 +5130,20 @@ function runSync(args) {
4711
5130
  }
4712
5131
  if (file.ownership === "optional-guardrail") continue;
4713
5132
  if (file.ownership === "project-local-seed") continue;
5133
+ if (file.kind === "directory") {
5134
+ if (!stat.isDirectory()) {
5135
+ console.log(`different: ${file.targetPath}`);
5136
+ differences += 1;
5137
+ }
5138
+ continue;
5139
+ }
5140
+ if (file.kind === "symlink") {
5141
+ if (!stat.isSymbolicLink() || readlinkSync4(targetPath) !== file.linkTarget) {
5142
+ console.log(`different: ${file.targetPath}`);
5143
+ differences += 1;
5144
+ }
5145
+ continue;
5146
+ }
4714
5147
  const source = readFileSync16(file.absoluteSourcePath, "utf8");
4715
5148
  const target = readFileSync16(targetPath, "utf8");
4716
5149
  if (!matchesExpectedContent(file.targetPath, source, target)) {
@@ -4746,10 +5179,17 @@ function normalizeMarkdownTableCell(cell) {
4746
5179
  }
4747
5180
  function inferInstalledProfile(root) {
4748
5181
  const installed = ["engineering-runtime", "doc-only"].filter(
4749
- (profile) => existsSync23(join22(root, `docs/governance/agents-routing/${profile}-v1.0.md`))
5182
+ (profile) => existsSync22(join23(root, `docs/governance/agents-routing/${profile}-v1.1.md`))
4750
5183
  );
4751
5184
  return installed.length === 1 ? installed[0] : void 0;
4752
5185
  }
5186
+ function safeLstat3(path) {
5187
+ try {
5188
+ return lstatSync9(path);
5189
+ } catch {
5190
+ return void 0;
5191
+ }
5192
+ }
4753
5193
  function readFlag2(args, flag) {
4754
5194
  const index = args.indexOf(flag);
4755
5195
  const value = index >= 0 ? args[index + 1] : void 0;