@pieai/pro-gov 0.5.3 → 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,34 +3864,50 @@ 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 { 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";
3589
3878
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3879
+ var CURRENT_ROUTER_VERSION = "1.1";
3590
3880
  function inspectPortfolioAiHealth(options) {
3591
3881
  const endpoints = collectEndpoints(options.manifest);
3592
- const secretsRoot = options.secretsRoot ?? join20(dirname12(options.manifest.controlPlane?.path ?? endpoints[0]?.endpoint.path ?? process.cwd()), ".secrets");
3593
- const homeDir = options.homeDir ?? process.env.HOME ?? "";
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();
3884
+ const grokVersion = commandVersion("grok");
3594
3885
  const executionEngineRoot = options.manifest.executionEngine?.path;
3595
3886
  const skillRegistry = inspectSkillRegistry(executionEngineRoot);
3596
- const expectedPackageVersion = packageVersion(join20(executionEngineRoot ?? "", "packages/pro-gov/package.json"));
3597
- const repositories = endpoints.map(({ endpoint, role }) => inspectRepository(endpoint, role, secretsRoot, expectedPackageVersion, options.manifest.technologyGovernance));
3887
+ const expectedPackageVersion = packageVersion(join21(executionEngineRoot ?? "", "packages/pro-gov/package.json"));
3888
+ const repositories = endpoints.map(({ endpoint, role }) => inspectRepository(
3889
+ endpoint,
3890
+ role,
3891
+ secretsRoot,
3892
+ homeDir,
3893
+ expectedPackageVersion,
3894
+ options.manifest.technologyGovernance,
3895
+ grokVersion
3896
+ ));
3598
3897
  const summary = { healthy: 0, attention: 0, unhealthy: 0 };
3599
3898
  for (const repository of repositories) summary[repository.status] += 1;
3600
3899
  return {
3601
- schemaVersion: 1,
3900
+ schemaVersion: 4,
3602
3901
  portfolioId: options.manifest.portfolioId,
3603
3902
  generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3604
- privacy: "Names, paths, counts, and configuration structure only. Secret values, environment values, MCP commands, arguments, and MCP environment maps are never collected.",
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.",
3605
3904
  secretsRoot: inspectSecretsRoot(secretsRoot),
3606
- userMcp: inspectUserMcp(homeDir),
3905
+ hostEnvironment: inspectHostEnvironment(
3906
+ homeDir,
3907
+ grokVersion,
3908
+ endpoints.map(({ endpoint }) => endpoint.path),
3909
+ options.manifest.specialistChecks?.devspace
3910
+ ),
3607
3911
  skillRegistry,
3608
3912
  technologyGovernance: {
3609
3913
  strategySource: options.manifest.technologyGovernance?.strategySource,
@@ -3618,15 +3922,15 @@ function writePortfolioAiHealthReport(report, outDir) {
3618
3922
  mkdirSync8(outDir, { recursive: true });
3619
3923
  const dashboardAssets = findDashboardAssets();
3620
3924
  for (const file of ["index.html", "app.js", "app.css"]) {
3621
- const source = join20(dashboardAssets, file);
3622
- if (!existsSync21(source)) throw new Error(`Portfolio dashboard asset is missing: ${source}`);
3623
- 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));
3624
3928
  }
3625
- const jsonPath = join20(outDir, "portfolio-ai-health.json");
3626
- const htmlPath = join20(outDir, "index.html");
3929
+ const jsonPath = join21(outDir, "portfolio-ai-health.json");
3930
+ const htmlPath = join21(outDir, "index.html");
3627
3931
  writeFileSync7(jsonPath, `${JSON.stringify(report, null, 2)}
3628
3932
  `);
3629
- writeFileSync7(join20(outDir, "data.js"), `window.__PORTFOLIO_AI_HEALTH__ = ${safeJavaScriptJson(report)};
3933
+ writeFileSync7(join21(outDir, "data.js"), `window.__PORTFOLIO_AI_HEALTH__ = ${safeJavaScriptJson(report)};
3630
3934
  `);
3631
3935
  return { jsonPath, htmlPath };
3632
3936
  }
@@ -3637,23 +3941,28 @@ function collectEndpoints(manifest) {
3637
3941
  for (const target of manifest.targets) result.push({ endpoint: target, role: "target" });
3638
3942
  const seen = /* @__PURE__ */ new Set();
3639
3943
  return result.filter(({ endpoint }) => {
3640
- const key = resolve4(endpoint.path);
3944
+ const key = resolve5(endpoint.path);
3641
3945
  if (seen.has(key)) return false;
3642
3946
  seen.add(key);
3643
3947
  return true;
3644
3948
  });
3645
3949
  }
3646
- function inspectRepository(endpoint, role, secretsRoot, expectedPackageVersion, technologyGovernance) {
3950
+ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackageVersion, technologyGovernance, grokVersion) {
3647
3951
  const root = endpoint.path;
3648
3952
  const git = inspectGit2(root);
3649
3953
  const entries = inspectEntries(root);
3650
- const skills = inspectSkills(root);
3954
+ const grokInspection = inspectGrokProject(root, homeDir, grokVersion);
3955
+ const skills = inspectSkills(root, grokInspection);
3956
+ const hostSsot = inspectProjectHostSsot(root);
3651
3957
  const hooks = inspectHooks(root);
3652
3958
  const docs = inspectDocs(root, role === "execution-engine" ? void 0 : expectedPackageVersion);
3653
3959
  const mcp = {
3654
- root: jsonObjectKeys(join20(root, ".mcp.json"), "mcpServers"),
3655
- claudeCode: jsonObjectKeys(join20(root, ".claude/settings.json"), "mcpServers"),
3656
- codex: tomlMcpNames(join20(root, ".codex/config.toml"))
3960
+ codexProject: tomlMcpNames(join21(root, ".codex/config.toml")),
3961
+ claudeCodeProjectShared: jsonObjectKeys(join21(root, ".mcp.json"), "mcpServers"),
3962
+ claudeCodeProjectLocal: claudeProjectLocalMcpNames(homeDir, root),
3963
+ grokProject: tomlMcpNames(join21(root, ".grok/config.toml")),
3964
+ grokEffective: grokInspection.effectiveMcp,
3965
+ grokInspection: grokInspection.inspection
3657
3966
  };
3658
3967
  const secrets = inspectRepositorySecrets(root, endpoint.id, secretsRoot, git.isRepository, endpoint.environmentPolicy);
3659
3968
  const projectModel = inspectProjectModel(root, endpoint, technologyGovernance);
@@ -3675,6 +3984,9 @@ function inspectRepository(endpoint, role, secretsRoot, expectedPackageVersion,
3675
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");
3676
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");
3677
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`);
3678
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");
3679
3991
  const liveEnv = secrets.repositoryEnvFiles.filter((file) => !file.template && !file.fixture);
3680
3992
  const envNeedsCentralReview = liveEnv.filter((file) => !file.localOnly);
@@ -3683,6 +3995,7 @@ function inspectRepository(endpoint, role, secretsRoot, expectedPackageVersion,
3683
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");
3684
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");
3685
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`);
3686
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");
3687
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");
3688
4001
  const missingBaseline = projectModel.baseline.filter((technology) => !technology.detected && !hasBaselineException(projectModel, technology.id));
@@ -3694,27 +4007,29 @@ function inspectRepository(endpoint, role, secretsRoot, expectedPackageVersion,
3694
4007
  role,
3695
4008
  path: root,
3696
4009
  profile: "profile" in endpoint ? endpoint.profile : void 0,
3697
- 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)),
3698
4011
  recommendations,
3699
4012
  git,
3700
4013
  entries,
3701
4014
  hooks,
3702
4015
  mcp,
3703
4016
  skills,
4017
+ hostSsot,
3704
4018
  secrets,
3705
4019
  docs,
3706
4020
  projectModel
3707
4021
  };
3708
4022
  }
3709
- function deriveStatus(role, entries, git, hooks, skills, secrets, docs, projectModel, technologyGovernanceConfigured) {
4023
+ function deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, technologyGovernanceConfigured) {
3710
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";
3711
4025
  const missingBaseline = projectModel.baseline.some((technology) => !technology.detected && !hasBaselineException(projectModel, technology.id));
3712
4026
  const missingSelected = projectModel.optionalCapabilities.some((technology) => technology.selected && !technology.detected);
3713
4027
  const liveEnv = secrets.repositoryEnvFiles.filter((file) => !file.template && !file.fixture);
3714
4028
  const secretMaterializationNeedsReview = liveEnv.some((file) => !file.centralized && !file.localOnly);
3715
4029
  const packageVersionNeedsReview = docs.packages.expected !== void 0 && !docs.packages.aligned;
4030
+ const routerVersionNeedsReview = !docs.routerAligned;
3716
4031
  const targetManifestMissing = role === "target" && !docs.manifest;
3717
- 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";
3718
4033
  return "healthy";
3719
4034
  }
3720
4035
  function inspectProjectModel(root, endpoint, governance) {
@@ -3724,7 +4039,7 @@ function inspectProjectModel(root, endpoint, governance) {
3724
4039
  const detection = (id) => {
3725
4040
  const technology = technologyById.get(id);
3726
4041
  const packageMatch = technology?.packages?.some((name) => packages.has(name)) ?? false;
3727
- const fileMatch = technology?.files?.some((path) => existsSync21(join20(root, path))) ?? false;
4042
+ const fileMatch = technology?.files?.some((path) => existsSync20(join21(root, path))) ?? false;
3728
4043
  return { id, label: technology?.label ?? id, detected: packageMatch || fileMatch };
3729
4044
  };
3730
4045
  const selected = new Set(endpoint.capabilities ?? []);
@@ -3747,10 +4062,10 @@ function collectPackageNames(root) {
3747
4062
  try {
3748
4063
  files = splitLines(execFileSync("git", ["ls-files", "*package.json"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }));
3749
4064
  } catch {
3750
- if (existsSync21(join20(root, "package.json"))) files = ["package.json"];
4065
+ if (existsSync20(join21(root, "package.json"))) files = ["package.json"];
3751
4066
  }
3752
4067
  for (const file of files) {
3753
- const packageJson = readJson3(join20(root, file));
4068
+ const packageJson = readJson3(join21(root, file));
3754
4069
  if (!isRecord3(packageJson)) continue;
3755
4070
  for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
3756
4071
  const dependencies = packageJson[section];
@@ -3788,15 +4103,15 @@ function inspectGit2(root) {
3788
4103
  };
3789
4104
  }
3790
4105
  function inspectEntries(root) {
3791
- const agentsPath = join20(root, "AGENTS.md");
3792
- const agents = !existsSync21(agentsPath) ? "missing" : safeRead(agentsPath).includes("PGS-ROUTER:BEGIN") ? "pgs-router" : "custom";
3793
- 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");
3794
4109
  let claude = "missing";
3795
4110
  if (pathLexists(claudePath)) {
3796
- const info = lstatSync6(claudePath);
4111
+ const info = lstatSync8(claudePath);
3797
4112
  if (info.isSymbolicLink()) {
3798
4113
  try {
3799
- claude = realpathSync2(claudePath) === realpathSync2(agentsPath) ? "agents-symlink" : "custom";
4114
+ claude = realpathSync4(claudePath) === realpathSync4(agentsPath) ? "agents-symlink" : "custom";
3800
4115
  } catch {
3801
4116
  claude = "dangling-symlink";
3802
4117
  }
@@ -3808,12 +4123,12 @@ function inspectEntries(root) {
3808
4123
  return { agents, claude, gemini: inspectOptionalEntry(root, "GEMINI.md", agentsPath) };
3809
4124
  }
3810
4125
  function inspectOptionalEntry(root, filename, agentsPath) {
3811
- const path = join20(root, filename);
4126
+ const path = join21(root, filename);
3812
4127
  if (!pathLexists(path)) return "missing";
3813
- const info = lstatSync6(path);
4128
+ const info = lstatSync8(path);
3814
4129
  if (info.isSymbolicLink()) {
3815
4130
  try {
3816
- return realpathSync2(path) === realpathSync2(agentsPath) ? "agents-symlink" : "custom";
4131
+ return realpathSync4(path) === realpathSync4(agentsPath) ? "agents-symlink" : "custom";
3817
4132
  } catch {
3818
4133
  return "dangling-symlink";
3819
4134
  }
@@ -3821,8 +4136,8 @@ function inspectOptionalEntry(root, filename, agentsPath) {
3821
4136
  const content = safeRead(path);
3822
4137
  return /AGENTS\.md/.test(content) && content.length < 2e3 ? "thin-adapter" : "custom";
3823
4138
  }
3824
- function inspectSkills(root) {
3825
- const lock = readJson3(join20(root, ".pro-gov/assets.lock.json"));
4139
+ function inspectSkills(root, grokInspection) {
4140
+ const lock = readJson3(join21(root, ".pro-gov/assets.lock.json"));
3826
4141
  const managed = /* @__PURE__ */ new Set();
3827
4142
  const bundleIds = stringArray(isRecord3(lock) ? lock.bundleIds : void 0);
3828
4143
  if (isRecord3(lock) && Array.isArray(lock.assets)) {
@@ -3832,14 +4147,14 @@ function inspectSkills(root) {
3832
4147
  if (match) managed.add(match[1]);
3833
4148
  }
3834
4149
  }
3835
- const skillRoot = join20(root, ".agents/skills");
4150
+ const skillRoot = join21(root, ".agents/skills");
3836
4151
  const canonical = pathLexists(skillRoot) && safeIsDirectory(skillRoot) ? safeReadDir(skillRoot).filter((name) => !name.startsWith(".")).map((name) => {
3837
- const path = join20(skillRoot, name);
3838
- const stat = lstatSync6(path);
4152
+ const path = join21(skillRoot, name);
4153
+ const stat = lstatSync8(path);
3839
4154
  let kind = stat.isSymbolicLink() ? "symlink" : stat.isDirectory() ? "directory" : "file";
3840
4155
  if (stat.isSymbolicLink()) {
3841
4156
  try {
3842
- realpathSync2(path);
4157
+ realpathSync4(path);
3843
4158
  } catch {
3844
4159
  kind = "dangling-symlink";
3845
4160
  }
@@ -3857,17 +4172,23 @@ function inspectSkills(root) {
3857
4172
  installed: canonical.filter((item) => item.managed && item.kind !== "dangling-symlink").length,
3858
4173
  discoverable: canonical.filter((item) => item.kind !== "dangling-symlink").length,
3859
4174
  runtime: "unobservable"
4175
+ },
4176
+ hosts: {
4177
+ codexProject: canonical.filter((item) => item.kind !== "dangling-symlink").length,
4178
+ claudeCodeProject: inspectSkillRoot(join21(root, ".claude/skills")).names.length,
4179
+ grokNativeProject: inspectSkillRoot(join21(root, ".grok/skills")).names.length,
4180
+ grokEffective: grokInspection.skills
3860
4181
  }
3861
4182
  };
3862
4183
  }
3863
4184
  function inspectClaudeSkillRoot(root) {
3864
- const path = join20(root, ".claude/skills");
4185
+ const path = join21(root, ".claude/skills");
3865
4186
  if (!pathLexists(path)) return "missing";
3866
- const stat = lstatSync6(path);
4187
+ const stat = lstatSync8(path);
3867
4188
  if (stat.isSymbolicLink()) {
3868
4189
  try {
3869
- const target = realpathSync2(path);
3870
- 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";
3871
4192
  } catch {
3872
4193
  return "dangling-symlink";
3873
4194
  }
@@ -3875,7 +4196,7 @@ function inspectClaudeSkillRoot(root) {
3875
4196
  return stat.isDirectory() ? "duplicate-directory" : "other";
3876
4197
  }
3877
4198
  function inspectRepositorySecrets(root, id, secretsRoot, isRepository, environmentPolicy) {
3878
- const centralPath = join20(secretsRoot, id);
4199
+ const centralPath = join21(secretsRoot, id);
3879
4200
  const centralRealPath = safeRealpath(centralPath);
3880
4201
  const localOnlyReasons = new Map(
3881
4202
  (environmentPolicy?.localOnly ?? []).map((entry) => [entry.path, entry.reason])
@@ -3887,16 +4208,16 @@ function inspectRepositorySecrets(root, id, secretsRoot, isRepository, environme
3887
4208
  tracked: isRepository ? gitTracks(root, path) : false,
3888
4209
  template: isEnvironmentTemplate(path),
3889
4210
  fixture: isEnvironmentFixture(path),
3890
- symlink: lstatSync6(join20(root, path)).isSymbolicLink(),
3891
- centralized: pointsInside(join20(root, path), centralRealPath),
4211
+ symlink: lstatSync8(join21(root, path)).isSymbolicLink(),
4212
+ centralized: pointsInside(join21(root, path), centralRealPath),
3892
4213
  localOnly: localOnlyReason !== void 0,
3893
4214
  ...localOnlyReason !== void 0 ? { localOnlyReason } : {}
3894
4215
  };
3895
4216
  });
3896
4217
  return {
3897
- centralDirectory: existsSync21(centralPath) ? "present" : "absent",
3898
- centralMode: existsSync21(centralPath) ? modeString(statSync4(centralPath).mode) : void 0,
3899
- 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) : [],
3900
4221
  repositoryEnvFiles: envFiles
3901
4222
  };
3902
4223
  }
@@ -3907,9 +4228,9 @@ function collectEnvironmentFiles(root, current = root, depth = 0) {
3907
4228
  try {
3908
4229
  for (const entry of readdirSync8(current, { withFileTypes: true })) {
3909
4230
  if (entry.isDirectory()) {
3910
- 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));
3911
4232
  } else if (isEnvironmentFilename(entry.name) && !isProviderGeneratedEnvironmentFile(entry.name)) {
3912
- found.push(relative8(root, join20(current, entry.name)));
4233
+ found.push(relative8(root, join21(current, entry.name)));
3913
4234
  }
3914
4235
  }
3915
4236
  } catch {
@@ -3922,9 +4243,9 @@ function collectCentralSecretFiles(root, current = root, depth = 0) {
3922
4243
  const found = [];
3923
4244
  try {
3924
4245
  for (const entry of readdirSync8(current, { withFileTypes: true })) {
3925
- const path = join20(current, entry.name);
4246
+ const path = join21(current, entry.name);
3926
4247
  if (entry.isDirectory()) found.push(...collectCentralSecretFiles(root, path, depth + 1));
3927
- 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) });
3928
4249
  }
3929
4250
  } catch {
3930
4251
  return found;
@@ -3949,13 +4270,13 @@ function isEnvironmentFixture(path) {
3949
4270
  }
3950
4271
  function safeRealpath(path) {
3951
4272
  try {
3952
- return realpathSync2(path);
4273
+ return realpathSync4(path);
3953
4274
  } catch {
3954
4275
  return void 0;
3955
4276
  }
3956
4277
  }
3957
4278
  function pointsInside(path, expectedRoot) {
3958
- if (!expectedRoot || !lstatSync6(path).isSymbolicLink()) return false;
4279
+ if (!expectedRoot || !lstatSync8(path).isSymbolicLink()) return false;
3959
4280
  const target = safeRealpath(path);
3960
4281
  if (!target) return false;
3961
4282
  const fromRoot = relative8(expectedRoot, target);
@@ -3965,15 +4286,251 @@ function hasUnsafeCentralSecretPermissions(secrets) {
3965
4286
  return secrets.centralDirectory === "present" && (secrets.centralMode !== "700" || secrets.centralFiles.some((file) => file.mode !== "600"));
3966
4287
  }
3967
4288
  function inspectSecretsRoot(path) {
3968
- return existsSync21(path) ? { path, exists: true, mode: modeString(statSync4(path).mode) } : { path, exists: false };
3969
- }
3970
- function inspectUserMcp(homeDir) {
3971
- if (!homeDir) return { codex: [], claudeCode: [] };
4289
+ return existsSync20(path) ? { path, exists: true, mode: modeString(statSync4(path).mode) } : { path, exists: false };
4290
+ }
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 = {
4296
+ mcp: {
4297
+ codexUser: { path: codexConfig, names: tomlMcpNames(codexConfig) },
4298
+ claudeCodeUser: { path: claudeConfig, names: jsonObjectKeys(claudeConfig, "mcpServers") },
4299
+ grokUser: { path: grokConfig, names: tomlMcpNames(grokConfig) }
4300
+ },
4301
+ 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)
4308
+ },
4309
+ grok: {
4310
+ available: grokVersion !== void 0,
4311
+ ...grokVersion ? { version: grokVersion } : {},
4312
+ inspectionCommand: "grok inspect --json"
4313
+ }
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`);
3972
4371
  return {
3973
- codex: tomlMcpNames(join20(homeDir, ".codex/config.toml")),
3974
- claudeCode: jsonObjectKeys(join20(homeDir, ".claude/settings.json"), "mcpServers")
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
3975
4394
  };
3976
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}`);
4416
+ }
4417
+ function inspectSkillRoot(path) {
4418
+ const exists = pathLexists(path) && safeIsDirectory(path);
4419
+ const names = exists ? safeReadDir(path).filter((name) => !name.startsWith(".") && existsSync20(join21(path, name, "SKILL.md"))) : [];
4420
+ return { path, exists, names };
4421
+ }
4422
+ function claudeProjectLocalMcpNames(homeDir, root) {
4423
+ if (!homeDir) return [];
4424
+ const value = readJson3(join21(homeDir, ".claude.json"));
4425
+ if (!isRecord3(value) || !isRecord3(value.projects)) return [];
4426
+ const candidates = new Set([resolve5(root), safeRealpath(root)].filter((path) => Boolean(path)));
4427
+ const names = /* @__PURE__ */ new Set();
4428
+ for (const [path, project] of Object.entries(value.projects)) {
4429
+ const projectPaths = [resolve5(path), safeRealpath(path)].filter((candidate) => Boolean(candidate));
4430
+ if (!projectPaths.some((candidate) => candidates.has(candidate)) || !isRecord3(project) || !isRecord3(project.mcpServers)) continue;
4431
+ for (const name of Object.keys(project.mcpServers)) names.add(name);
4432
+ }
4433
+ return [...names].sort();
4434
+ }
4435
+ function commandVersion(command2) {
4436
+ try {
4437
+ const output = execFileSync(command2, ["version"], {
4438
+ encoding: "utf8",
4439
+ stdio: ["ignore", "pipe", "ignore"],
4440
+ timeout: 3e3
4441
+ });
4442
+ return output.match(/\b\d+\.\d+\.\d+\b/)?.[0];
4443
+ } catch {
4444
+ return void 0;
4445
+ }
4446
+ }
4447
+ function inspectGrokProject(root, homeDir, grokVersion) {
4448
+ const empty = (inspection) => ({
4449
+ inspection,
4450
+ effectiveMcp: [],
4451
+ skills: { inspection, total: 0, project: 0, userOrCompatible: 0, plugin: 0, bundled: 0 },
4452
+ claudeCompatibility: "unknown",
4453
+ cursorCompatibility: "unknown"
4454
+ });
4455
+ if (!grokVersion) return empty("unavailable");
4456
+ try {
4457
+ const value = JSON.parse(execFileSync("grok", ["inspect", "--json"], {
4458
+ cwd: root,
4459
+ encoding: "utf8",
4460
+ env: { ...process.env, HOME: homeDir, GROK_HOME: join21(homeDir, ".grok") },
4461
+ maxBuffer: 10 * 1024 * 1024,
4462
+ stdio: ["ignore", "pipe", "ignore"],
4463
+ timeout: 8e3
4464
+ }));
4465
+ if (!isRecord3(value)) return empty("failed");
4466
+ const userClaudeNames = new Set(jsonObjectKeys(join21(homeDir, ".claude.json"), "mcpServers"));
4467
+ const localClaudeNames = new Set(claudeProjectLocalMcpNames(homeDir, root));
4468
+ const effectiveMcp = Array.isArray(value.mcpServers) ? value.mcpServers.flatMap((item) => {
4469
+ if (!isRecord3(item) || typeof item.name !== "string") return [];
4470
+ const source = isRecord3(item.source) ? item.source : {};
4471
+ const sourceType = typeof source.type === "string" ? source.type : "unknown";
4472
+ const sourcePath = typeof source.path === "string" ? source.path : void 0;
4473
+ const vendor = typeof item.vendor === "string" ? item.vendor : sourceType;
4474
+ return [{
4475
+ name: item.name,
4476
+ vendor,
4477
+ scope: inferGrokMcpScope(item.name, sourceType, sourcePath, root, homeDir, userClaudeNames, localClaudeNames),
4478
+ sourceType,
4479
+ ...sourcePath ? { sourcePath } : {}
4480
+ }];
4481
+ }) : [];
4482
+ const skillCounts = { project: 0, userOrCompatible: 0, plugin: 0, bundled: 0 };
4483
+ if (Array.isArray(value.skills)) {
4484
+ for (const item of value.skills) {
4485
+ if (!isRecord3(item) || !isRecord3(item.source)) continue;
4486
+ const type = item.source.type;
4487
+ const path = item.source.path;
4488
+ if (typeof path === "string" && path.includes(sep + ".grok" + sep + "bundled" + sep)) skillCounts.bundled += 1;
4489
+ else if (type === "project") skillCounts.project += 1;
4490
+ else if (type === "plugin") skillCounts.plugin += 1;
4491
+ else skillCounts.userOrCompatible += 1;
4492
+ }
4493
+ }
4494
+ const cells = isRecord3(value.externalCompat) && Array.isArray(value.externalCompat.cells) ? value.externalCompat.cells : [];
4495
+ return {
4496
+ inspection: "ok",
4497
+ effectiveMcp,
4498
+ skills: {
4499
+ inspection: "ok",
4500
+ total: skillCounts.project + skillCounts.userOrCompatible + skillCounts.plugin + skillCounts.bundled,
4501
+ ...skillCounts
4502
+ },
4503
+ claudeCompatibility: compatibilityEnabled(cells, "claude"),
4504
+ cursorCompatibility: compatibilityEnabled(cells, "cursor")
4505
+ };
4506
+ } catch {
4507
+ return empty("failed");
4508
+ }
4509
+ }
4510
+ function compatibilityEnabled(cells, vendor) {
4511
+ const matching = cells.filter((cell) => isRecord3(cell) && cell.vendor === vendor);
4512
+ if (matching.length === 0) return "unknown";
4513
+ return matching.some((cell) => isRecord3(cell) && cell.enabled === true);
4514
+ }
4515
+ function inferGrokMcpScope(name, sourceType, sourcePath, root, homeDir, userClaudeNames, localClaudeNames) {
4516
+ if (!sourcePath) {
4517
+ if (sourceType === "project") return "project";
4518
+ if (localClaudeNames.has(name)) return "project-local";
4519
+ if (userClaudeNames.has(name)) return "user";
4520
+ return "unknown";
4521
+ }
4522
+ const resolvedSource = safeRealpath(sourcePath) ?? resolve5(sourcePath);
4523
+ const resolvedRoot = safeRealpath(root) ?? resolve5(root);
4524
+ if (resolvedSource === join21(resolvedRoot, ".mcp.json")) return "project-shared";
4525
+ if (resolvedSource.startsWith(resolvedRoot + sep)) return "project";
4526
+ if (homeDir && resolvedSource === join21(resolve5(homeDir), ".claude.json")) {
4527
+ if (localClaudeNames.has(name)) return "project-local";
4528
+ if (userClaudeNames.has(name)) return "user";
4529
+ }
4530
+ if (homeDir && resolvedSource.startsWith(resolve5(homeDir) + sep)) return "user";
4531
+ if (sourceType === "project") return "project";
4532
+ return "unknown";
4533
+ }
3977
4534
  var HOOK_EVENT_NAMES = /* @__PURE__ */ new Set([
3978
4535
  "PreToolUse",
3979
4536
  "PostToolUse",
@@ -4001,7 +4558,7 @@ function inspectHooks(root) {
4001
4558
  { host: "codex", path: ".codex/hooks.json" }
4002
4559
  ];
4003
4560
  return configs.map((config) => {
4004
- const value = readJson3(join20(root, config.path));
4561
+ const value = readJson3(join21(root, config.path));
4005
4562
  const counts = /* @__PURE__ */ new Map();
4006
4563
  collectHookEvents(value, counts);
4007
4564
  return {
@@ -4022,16 +4579,18 @@ function collectHookEvents(value, counts) {
4022
4579
  }
4023
4580
  }
4024
4581
  function inspectDocs(root, expected) {
4025
- const packageJson = readJson3(join20(root, "package.json"));
4582
+ const packageJson = readJson3(join21(root, "package.json"));
4026
4583
  const dependencies = isRecord3(packageJson) ? { ...recordOrEmpty(packageJson.dependencies), ...recordOrEmpty(packageJson.devDependencies) } : {};
4027
4584
  const docGov = dependencyVersion(dependencies["@pieai/doc-gov"]);
4028
4585
  const proGov = dependencyVersion(dependencies["@pieai/pro-gov"]);
4029
- 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.]+)/);
4030
4587
  const declared = [docGov, proGov].filter((value) => Boolean(value));
4031
4588
  return {
4032
4589
  routerVersion: routerMatch?.[1],
4033
- manifest: existsSync21(join20(root, "docs/governance/MANIFEST.yml")),
4034
- 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")),
4035
4594
  packages: {
4036
4595
  expected,
4037
4596
  docGov,
@@ -4054,20 +4613,20 @@ function recordOrEmpty(value) {
4054
4613
  }
4055
4614
  function inspectSkillRegistry(executionEngineRoot) {
4056
4615
  if (!executionEngineRoot) return { source: 0, registered: 0, bundled: 0, bundles: 0 };
4057
- const agentAssetsRoot = join20(executionEngineRoot, "agent-assets");
4058
- const registry = readJson3(join20(agentAssetsRoot, "registry.json"));
4616
+ const agentAssetsRoot = join21(executionEngineRoot, "agent-assets");
4617
+ const registry = readJson3(join21(agentAssetsRoot, "registry.json"));
4059
4618
  const assets = isRecord3(registry) && Array.isArray(registry.assets) ? registry.assets : [];
4060
4619
  const registeredSkills = assets.filter((asset) => isRecord3(asset) && asset.kind === "skill");
4061
- const bundleRoot = join20(agentAssetsRoot, "bundles");
4620
+ const bundleRoot = join21(agentAssetsRoot, "bundles");
4062
4621
  const bundleFiles = safeReadDir(bundleRoot).filter((file) => file.endsWith(".json"));
4063
4622
  const bundledIds = /* @__PURE__ */ new Set();
4064
4623
  for (const file of bundleFiles) {
4065
- const bundle = readJson3(join20(bundleRoot, file));
4624
+ const bundle = readJson3(join21(bundleRoot, file));
4066
4625
  if (!isRecord3(bundle) || !Array.isArray(bundle.assets)) continue;
4067
4626
  for (const id of bundle.assets) if (typeof id === "string") bundledIds.add(id);
4068
4627
  }
4069
- const sourceRoots = [join20(agentAssetsRoot, "skills/pie-skills"), join20(agentAssetsRoot, "skills/npx-skills/.agents/skills")];
4070
- 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);
4071
4630
  return {
4072
4631
  source,
4073
4632
  registered: registeredSkills.length,
@@ -4081,7 +4640,7 @@ function jsonObjectKeys(path, key) {
4081
4640
  return Object.keys(value[key]).sort();
4082
4641
  }
4083
4642
  function tomlMcpNames(path) {
4084
- if (!existsSync21(path)) return [];
4643
+ if (!existsSync20(path)) return [];
4085
4644
  const names = /* @__PURE__ */ new Set();
4086
4645
  for (const line of safeRead(path).split(/\r?\n/)) {
4087
4646
  const match = line.match(/^\s*\[mcp_servers\.(?:"([^"]+)"|([^\.\]]+))\]\s*$/);
@@ -4120,7 +4679,7 @@ function safeIsDirectory(path) {
4120
4679
  }
4121
4680
  function pathLexists(path) {
4122
4681
  try {
4123
- lstatSync6(path);
4682
+ lstatSync8(path);
4124
4683
  return true;
4125
4684
  } catch {
4126
4685
  return false;
@@ -4147,14 +4706,17 @@ function isRecord3(value) {
4147
4706
  return typeof value === "object" && value !== null && !Array.isArray(value);
4148
4707
  }
4149
4708
  function findDashboardAssets() {
4150
- const packageRoot2 = dirname12(dirname12(fileURLToPath4(import.meta.url)));
4709
+ const packageRoot2 = dirname13(dirname13(fileURLToPath4(import.meta.url)));
4151
4710
  const candidates = [
4152
4711
  process.env.PGS_DASHBOARD_ASSETS_DIR,
4153
- join20(packageRoot2, "assets/portfolio-dashboard"),
4154
- join20(process.cwd(), "assets/portfolio-dashboard"),
4155
- 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")
4156
4718
  ].filter((value) => Boolean(value));
4157
- const match = candidates.find((path) => existsSync21(join20(path, "index.html")));
4719
+ const match = candidates.find((path) => existsSync20(join21(path, "index.html")));
4158
4720
  if (!match) throw new Error("Portfolio dashboard assets were not built. Run pnpm --filter @pieai/pro-gov build.");
4159
4721
  return match;
4160
4722
  }
@@ -4244,12 +4806,22 @@ function runPortfolioDoctor(args) {
4244
4806
  const output = { configPath: loaded.configPath, ...result };
4245
4807
  if (options.value.json) {
4246
4808
  console.log(JSON.stringify(output, null, 2));
4247
- } else if (result.ok) {
4248
- console.log(`portfolio doctor passed (${targets.length} targets)`);
4249
4809
  } else {
4250
- 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
+ }
4251
4813
  for (const target of result.targets) {
4252
- 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
+ }
4253
4825
  }
4254
4826
  }
4255
4827
  return result.ok ? 0 : 1;
@@ -4506,8 +5078,8 @@ function isHost2(value) {
4506
5078
  return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
4507
5079
  }
4508
5080
  function findPortfolioAgentAssetsDir(manifest) {
4509
- const agentAssetsDir = manifest?.executionEngine?.path ? join21(manifest.executionEngine.path, "agent-assets") : void 0;
4510
- 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;
4511
5083
  }
4512
5084
  function printUsage4() {
4513
5085
  console.error("Usage:");
@@ -4519,8 +5091,8 @@ function printUsage4() {
4519
5091
  }
4520
5092
 
4521
5093
  // src/commands/sync.ts
4522
- import { existsSync as existsSync23, readFileSync as readFileSync16 } from "node:fs";
4523
- 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";
4524
5096
  function runSync(args) {
4525
5097
  const check = args.includes("--check");
4526
5098
  if (!check) {
@@ -4548,8 +5120,9 @@ function runSync(args) {
4548
5120
  console.log("pro-gov sync check");
4549
5121
  console.log(`profile: ${profile}`);
4550
5122
  for (const file of planStarterFiles(profile)) {
4551
- const targetPath = join22(process.cwd(), file.targetPath);
4552
- if (!existsSync23(targetPath)) {
5123
+ const targetPath = join23(process.cwd(), file.targetPath);
5124
+ const stat = safeLstat3(targetPath);
5125
+ if (!stat) {
4553
5126
  if (file.ownership === "optional-guardrail") continue;
4554
5127
  console.log(`missing: ${file.targetPath}`);
4555
5128
  differences += 1;
@@ -4557,6 +5130,20 @@ function runSync(args) {
4557
5130
  }
4558
5131
  if (file.ownership === "optional-guardrail") continue;
4559
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
+ }
4560
5147
  const source = readFileSync16(file.absoluteSourcePath, "utf8");
4561
5148
  const target = readFileSync16(targetPath, "utf8");
4562
5149
  if (!matchesExpectedContent(file.targetPath, source, target)) {
@@ -4592,10 +5179,17 @@ function normalizeMarkdownTableCell(cell) {
4592
5179
  }
4593
5180
  function inferInstalledProfile(root) {
4594
5181
  const installed = ["engineering-runtime", "doc-only"].filter(
4595
- (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`))
4596
5183
  );
4597
5184
  return installed.length === 1 ? installed[0] : void 0;
4598
5185
  }
5186
+ function safeLstat3(path) {
5187
+ try {
5188
+ return lstatSync9(path);
5189
+ } catch {
5190
+ return void 0;
5191
+ }
5192
+ }
4599
5193
  function readFlag2(args, flag) {
4600
5194
  const index = args.indexOf(flag);
4601
5195
  const value = index >= 0 ? args[index + 1] : void 0;