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