@pieai/pro-gov 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2584,6 +2584,9 @@ function formatProjectLensInspection(report) {
2584
2584
  `ai-host-ssot: ${report.hostSsot.compliant ? "compliant" : "non-compliant"}`,
2585
2585
  `claude-entry-link: ${formatLink(report.hostSsot.claudeEntry)}`,
2586
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}`,
2587
2590
  `package-scripts: ${formatList(report.packageJson?.scripts ?? [])}`,
2588
2591
  `dependencies: ${formatList(report.packageJson?.dependencies ?? [])}`,
2589
2592
  `dev-dependencies: ${formatList(report.packageJson?.devDependencies ?? [])}`,
@@ -2624,6 +2627,20 @@ function renderProjectLensMarkdownReport(report) {
2624
2627
  `- Canonical skills: \`${report.hostSsot.canonicalSkills.path}\` (${report.hostSsot.canonicalSkills.status})`,
2625
2628
  `- Claude skills: ${formatLink(report.hostSsot.claudeSkills)}`,
2626
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)`))}`,
2627
2644
  "",
2628
2645
  "## Package",
2629
2646
  "",
@@ -2669,8 +2686,9 @@ function formatLink(link) {
2669
2686
 
2670
2687
  // src/lens/scan.ts
2671
2688
  import { spawnSync as spawnSync3 } from "node:child_process";
2672
- import { existsSync as existsSync16, readdirSync as readdirSync7, readFileSync as readFileSync11, statSync as statSync3 } from "node:fs";
2673
- import { join as join18, relative as relative7 } from "node:path";
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";
2674
2692
 
2675
2693
  // src/host-ssot.ts
2676
2694
  import {
@@ -2795,6 +2813,117 @@ function safeLstat2(path) {
2795
2813
  }
2796
2814
  }
2797
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
+
2798
2927
  // src/lens/scan.ts
2799
2928
  var ignoredDirectories = /* @__PURE__ */ new Set([
2800
2929
  ".git",
@@ -2811,7 +2940,7 @@ function scanProjectLensTarget(targetDir, options = {}) {
2811
2940
  const markdownFiles = files.filter(
2812
2941
  (file) => file.startsWith("docs/") && file.endsWith(".md")
2813
2942
  );
2814
- const packageJson = readPackageJson(targetDir);
2943
+ const packageJson = readPackageJson2(targetDir);
2815
2944
  return {
2816
2945
  targetDir,
2817
2946
  scanScope: {
@@ -2821,25 +2950,31 @@ function scanProjectLensTarget(targetDir, options = {}) {
2821
2950
  excludedFileCount: candidateFiles.length - files.length
2822
2951
  },
2823
2952
  aiEntryFiles: ["AGENTS.md", "CLAUDE.md"].filter(
2824
- (file) => existsSync16(join18(targetDir, file))
2953
+ (file) => existsSync18(join20(targetDir, file))
2825
2954
  ),
2826
2955
  aiConfigFiles: [],
2827
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
+ }),
2828
2963
  packageJson,
2829
2964
  docs: {
2830
- hasDocsDirectory: existsSync16(join18(targetDir, "docs")),
2965
+ hasDocsDirectory: existsSync18(join20(targetDir, "docs")),
2831
2966
  markdownFileCount: markdownFiles.length,
2832
2967
  governanceFiles: markdownFiles.filter((file) => file.startsWith("docs/governance/") || file.startsWith("docs/policy/")).sort()
2833
2968
  },
2834
2969
  git: readGitState(targetDir),
2835
- largeFiles: files.map((file) => ({ path: file, bytes: statSync3(join18(targetDir, file)).size })).filter((file) => file.bytes >= largeFileBytes).sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path)).slice(0, 25)
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)
2836
2971
  };
2837
2972
  }
2838
- function readPackageJson(targetDir) {
2839
- const packageJsonPath = join18(targetDir, "package.json");
2840
- if (!existsSync16(packageJsonPath)) return void 0;
2973
+ function readPackageJson2(targetDir) {
2974
+ const packageJsonPath = join20(targetDir, "package.json");
2975
+ if (!existsSync18(packageJsonPath)) return void 0;
2841
2976
  try {
2842
- const packageJson = JSON.parse(readFileSync11(packageJsonPath, "utf8"));
2977
+ const packageJson = JSON.parse(readFileSync12(packageJsonPath, "utf8"));
2843
2978
  return {
2844
2979
  scripts: Object.keys(packageJson.scripts ?? {}).sort(),
2845
2980
  dependencies: Object.keys(packageJson.dependencies ?? {}).sort(),
@@ -2877,7 +3012,7 @@ function listProjectFiles(targetDir) {
2877
3012
  "-z"
2878
3013
  ]);
2879
3014
  if (gitFiles.ok) {
2880
- return gitFiles.stdout.split("\0").filter(Boolean).map(toUnixPath4).filter((file) => existsSync16(join18(targetDir, file))).sort();
3015
+ return gitFiles.stdout.split("\0").filter(Boolean).map(toUnixPath4).filter((file) => existsSync18(join20(targetDir, file))).sort();
2881
3016
  }
2882
3017
  const files = [];
2883
3018
  collectFiles2(targetDir, targetDir, files);
@@ -2895,13 +3030,13 @@ function isFirstPartyEvidenceFile(file) {
2895
3030
  return !excludedEvidencePrefixes.some((prefix) => file.startsWith(prefix));
2896
3031
  }
2897
3032
  function collectFiles2(rootDir, currentDir, files) {
2898
- if (!existsSync16(currentDir)) return;
2899
- for (const entry of readdirSync7(currentDir, { withFileTypes: true })) {
3033
+ if (!existsSync18(currentDir)) return;
3034
+ for (const entry of readdirSync8(currentDir, { withFileTypes: true })) {
2900
3035
  if (entry.isDirectory()) {
2901
3036
  if (ignoredDirectories.has(entry.name)) continue;
2902
- collectFiles2(rootDir, join18(currentDir, entry.name), files);
3037
+ collectFiles2(rootDir, join20(currentDir, entry.name), files);
2903
3038
  } else if (entry.isFile()) {
2904
- files.push(toUnixPath4(relative7(rootDir, join18(currentDir, entry.name))));
3039
+ files.push(toUnixPath4(relative7(rootDir, join20(currentDir, entry.name))));
2905
3040
  }
2906
3041
  }
2907
3042
  }
@@ -3057,16 +3192,16 @@ function printUsage3() {
3057
3192
  }
3058
3193
 
3059
3194
  // src/commands/portfolio.ts
3060
- import { existsSync as existsSync21 } from "node:fs";
3061
- import { join as join22 } from "node:path";
3195
+ import { existsSync as existsSync24, readFileSync as readFileSync18 } from "node:fs";
3196
+ import { join as join25 } from "node:path";
3062
3197
 
3063
3198
  // src/portfolio/manifest.ts
3064
- import { existsSync as existsSync17, readFileSync as readFileSync12 } from "node:fs";
3199
+ import { existsSync as existsSync19, readFileSync as readFileSync13 } from "node:fs";
3065
3200
  import { dirname as dirname11, isAbsolute as isAbsolute4, resolve as resolve4 } from "node:path";
3066
3201
  function loadPortfolioManifest(configPath) {
3067
3202
  let parsed;
3068
3203
  try {
3069
- parsed = JSON.parse(readFileSync12(configPath, "utf8"));
3204
+ parsed = JSON.parse(readFileSync13(configPath, "utf8"));
3070
3205
  } catch (error) {
3071
3206
  return {
3072
3207
  configPath,
@@ -3235,7 +3370,7 @@ function validateEndpoint(value, field, issues, technologyCatalog) {
3235
3370
  });
3236
3371
  return;
3237
3372
  }
3238
- if (!existsSync17(value.path)) {
3373
+ if (!existsSync19(value.path)) {
3239
3374
  issues.push({
3240
3375
  type: "missing-path",
3241
3376
  id: typeof value.id === "string" ? value.id : void 0,
@@ -3264,7 +3399,7 @@ function validateTechnologyGovernance(value, issues) {
3264
3399
  issues.push({ type: "invalid-field", field: "technologyGovernance", message: "Portfolio technologyGovernance must be an object." });
3265
3400
  return { technologies, projectTypes };
3266
3401
  }
3267
- validateAllowedFields(value, "technologyGovernance", ["strategySource", "technologies", "projectTypes"], issues);
3402
+ validateAllowedFields(value, "technologyGovernance", ["strategySource", "versionPolicy", "technologies", "projectTypes"], issues);
3268
3403
  if (value.strategySource !== void 0 && (typeof value.strategySource !== "string" || value.strategySource.length === 0)) {
3269
3404
  issues.push({ type: "invalid-field", field: "technologyGovernance.strategySource", message: "Technology strategySource must be a non-empty string." });
3270
3405
  }
@@ -3327,8 +3462,68 @@ function validateTechnologyGovernance(value, issues) {
3327
3462
  if (!technologies.has(technology)) issues.push({ type: "invalid-field", field: "technologyGovernance.projectTypes", message: `Project type ${projectType.id} references unknown technology: ${technology}` });
3328
3463
  }
3329
3464
  }
3465
+ validateVersionPolicy(value.versionPolicy, projectTypes, issues);
3330
3466
  return { technologies, projectTypes };
3331
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
+ }
3332
3527
  function isRepositoryRelativePath(value) {
3333
3528
  if (value.length === 0 || isAbsolute4(value)) return false;
3334
3529
  const segments = value.replaceAll("\\", "/").split("/");
@@ -3503,10 +3698,10 @@ function isRecord(value) {
3503
3698
 
3504
3699
  // src/portfolio/doctor.ts
3505
3700
  import { spawnSync as spawnSync5 } from "node:child_process";
3506
- import { existsSync as existsSync19, readFileSync as readFileSync14 } from "node:fs";
3701
+ import { existsSync as existsSync22, readFileSync as readFileSync16 } from "node:fs";
3507
3702
  import { createRequire as createRequire2 } from "node:module";
3508
- import { homedir } from "node:os";
3509
- import { dirname as dirname12, join as join20 } from "node:path";
3703
+ import { homedir as homedir3 } from "node:os";
3704
+ import { dirname as dirname12, join as join23 } from "node:path";
3510
3705
  import { fileURLToPath as fileURLToPath3 } from "node:url";
3511
3706
 
3512
3707
  // src/host-tooling/inventory.ts
@@ -3597,13 +3792,13 @@ function isRecord2(value) {
3597
3792
  }
3598
3793
 
3599
3794
  // src/portfolio/asset-state.ts
3600
- import { existsSync as existsSync18, lstatSync as lstatSync7, readFileSync as readFileSync13 } from "node:fs";
3601
- import { join as join19 } from "node:path";
3795
+ import { existsSync as existsSync20, lstatSync as lstatSync7, readFileSync as readFileSync14 } from "node:fs";
3796
+ import { join as join21 } from "node:path";
3602
3797
  function comparePortfolioAssetState(options) {
3603
3798
  const expectedManifest = readPlanDocument(options.expectedPlan, ".pro-gov/assets.json");
3604
3799
  const expectedLock = readPlanDocument(options.expectedPlan, ".pro-gov/assets.lock.json");
3605
- const currentManifest = readJsonFile(join19(options.targetDir, ".pro-gov/assets.json"));
3606
- const currentLock = readJsonFile(join19(options.targetDir, ".pro-gov/assets.lock.json"));
3800
+ const currentManifest = readJsonFile(join21(options.targetDir, ".pro-gov/assets.json"));
3801
+ const currentLock = readJsonFile(join21(options.targetDir, ".pro-gov/assets.lock.json"));
3607
3802
  const issues = [];
3608
3803
  if (!sameStrings(currentManifest?.bundleIds, expectedManifest?.bundleIds)) {
3609
3804
  issues.push({
@@ -3627,7 +3822,7 @@ function comparePortfolioAssetState(options) {
3627
3822
  for (const entry of currentLock?.assets ?? []) {
3628
3823
  if (expectedTargets.has(entry.targetPath)) continue;
3629
3824
  if (options.expectedPlan.actions.some((action) => action.type === "adopt-symlink" && action.assetId === entry.id && action.legacyTargetPath === entry.targetPath)) continue;
3630
- const targetAbsolutePath = join19(options.targetDir, entry.targetPath);
3825
+ const targetAbsolutePath = join21(options.targetDir, entry.targetPath);
3631
3826
  if (!pathIsSymlink(targetAbsolutePath)) continue;
3632
3827
  issues.push({
3633
3828
  type: "orphaned-managed-symlink",
@@ -3649,9 +3844,9 @@ function readPlanDocument(plan, targetPath) {
3649
3844
  }
3650
3845
  }
3651
3846
  function readJsonFile(path) {
3652
- if (!existsSync18(path)) return void 0;
3847
+ if (!existsSync20(path)) return void 0;
3653
3848
  try {
3654
- return JSON.parse(readFileSync13(path, "utf8"));
3849
+ return JSON.parse(readFileSync14(path, "utf8"));
3655
3850
  } catch {
3656
3851
  return void 0;
3657
3852
  }
@@ -3678,6 +3873,80 @@ function pathIsSymlink(path) {
3678
3873
  }
3679
3874
  }
3680
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
+
3681
3950
  // src/portfolio/doctor.ts
3682
3951
  function inspectPortfolio(options) {
3683
3952
  const expectedPackageVersions = getExpectedPackageVersions();
@@ -3687,14 +3956,15 @@ function inspectPortfolio(options) {
3687
3956
  agentAssetsDir: options.agentAssetsDir,
3688
3957
  registry: options.registry,
3689
3958
  bundles: options.bundles,
3690
- expectedPackageVersions
3959
+ expectedPackageVersions,
3960
+ versionPolicy: options.manifest.technologyGovernance?.versionPolicy
3691
3961
  }));
3692
3962
  return {
3693
3963
  ok: hostTooling.issues.length === 0 && targets.every((target) => target.issues.length === 0),
3694
3964
  portfolioId: options.manifest.portfolioId,
3695
3965
  expectedPackageVersions,
3696
3966
  hostTooling,
3697
- hostSsot: { userSkills: inspectUserSkillsSsot(options.homeDir ?? homedir()) },
3967
+ hostSsot: { userSkills: inspectUserSkillsSsot(options.homeDir ?? homedir3()) },
3698
3968
  targets
3699
3969
  };
3700
3970
  }
@@ -3702,11 +3972,11 @@ function inspectTarget(options) {
3702
3972
  const { target } = options;
3703
3973
  const hostSsot = inspectProjectHostSsot(target.path);
3704
3974
  const issues = [];
3705
- const packageJson = readJson2(join20(target.path, "package.json"));
3975
+ const packageJson = readJson3(join23(target.path, "package.json"));
3706
3976
  const packages = {};
3707
3977
  for (const packageName of ["@pieai/pro-gov", "@pieai/doc-gov"]) {
3708
3978
  const declared = packageJson?.devDependencies?.[packageName] ?? packageJson?.dependencies?.[packageName];
3709
- const installedPackage = readJson2(join20(target.path, "node_modules", packageName, "package.json"));
3979
+ const installedPackage = readJson3(join23(target.path, "node_modules", packageName, "package.json"));
3710
3980
  const installed = installedPackage?.version;
3711
3981
  const expected = options.expectedPackageVersions[packageName];
3712
3982
  packages[packageName] = { declared, installed, expected };
@@ -3726,6 +3996,8 @@ function inspectTarget(options) {
3726
3996
  }
3727
3997
  }
3728
3998
  const checks = runTargetChecks(target);
3999
+ const versions = inspectVersionPolicy(target.path, options.versionPolicy, target.projectType);
4000
+ const verification = inspectProjectVerification(target.path);
3729
4001
  for (const check of checks) {
3730
4002
  if (check.status === 0) continue;
3731
4003
  issues.push({
@@ -3760,7 +4032,7 @@ function inspectTarget(options) {
3760
4032
  type: "asset-lock-drift",
3761
4033
  message: error instanceof Error ? error.message : String(error)
3762
4034
  });
3763
- if (!existsSync19(join20(target.path, ".pro-gov/assets.json"))) {
4035
+ if (!existsSync22(join23(target.path, ".pro-gov/assets.json"))) {
3764
4036
  issues.push({ type: "bundle-drift", message: "Target asset manifest is missing." });
3765
4037
  }
3766
4038
  }
@@ -3772,19 +4044,21 @@ function inspectTarget(options) {
3772
4044
  git: inspectGit(target.path),
3773
4045
  checks,
3774
4046
  hostSsot,
4047
+ versions,
4048
+ verification,
3775
4049
  issues: deduplicateIssues(issues)
3776
4050
  };
3777
4051
  }
3778
4052
  function readTargetAssetHost(targetDir) {
3779
- const lockfile = readJson2(join20(targetDir, ".pro-gov/assets.lock.json"));
4053
+ const lockfile = readJson3(join23(targetDir, ".pro-gov/assets.lock.json"));
3780
4054
  return isAssetRegistryHost(lockfile?.host) ? lockfile.host : void 0;
3781
4055
  }
3782
4056
  function isAssetRegistryHost(value) {
3783
4057
  return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
3784
4058
  }
3785
4059
  function runTargetChecks(target) {
3786
- const proGovCli = join20(target.path, "node_modules/@pieai/pro-gov/dist/cli.js");
3787
- const docGovCli = join20(target.path, "node_modules/@pieai/doc-gov/dist/cli.js");
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");
3788
4062
  const commands = [
3789
4063
  {
3790
4064
  name: "pro-gov doctor",
@@ -3795,7 +4069,7 @@ function runTargetChecks(target) {
3795
4069
  { name: "doc-gov scan --check", cli: docGovCli, args: ["scan", "--check"] }
3796
4070
  ];
3797
4071
  return commands.map((command2) => {
3798
- if (!existsSync19(command2.cli)) return { name: command2.name, status: null };
4072
+ if (!existsSync22(command2.cli)) return { name: command2.name, status: null };
3799
4073
  const result = spawnSync5(process.execPath, [command2.cli, ...command2.args], {
3800
4074
  cwd: target.path,
3801
4075
  encoding: "utf8",
@@ -3819,11 +4093,11 @@ function inspectGit(path) {
3819
4093
  };
3820
4094
  }
3821
4095
  function getExpectedPackageVersions() {
3822
- const proGovPackage = readJson2(findOwnPackageJson());
4096
+ const proGovPackage = readJson3(findOwnPackageJson());
3823
4097
  let docGovVersion;
3824
4098
  try {
3825
4099
  const require2 = createRequire2(import.meta.url);
3826
- const docGovPackage = readJson2(require2.resolve("@pieai/doc-gov/package.json"));
4100
+ const docGovPackage = readJson3(require2.resolve("@pieai/doc-gov/package.json"));
3827
4101
  docGovVersion = docGovPackage?.version;
3828
4102
  } catch {
3829
4103
  docGovVersion = void 0;
@@ -3836,16 +4110,16 @@ function getExpectedPackageVersions() {
3836
4110
  function findOwnPackageJson() {
3837
4111
  let current = dirname12(fileURLToPath3(import.meta.url));
3838
4112
  for (let depth = 0; depth < 5; depth += 1) {
3839
- const candidate = join20(current, "package.json");
3840
- if (existsSync19(candidate)) return candidate;
4113
+ const candidate = join23(current, "package.json");
4114
+ if (existsSync22(candidate)) return candidate;
3841
4115
  current = dirname12(current);
3842
4116
  }
3843
4117
  return "";
3844
4118
  }
3845
- function readJson2(path) {
3846
- if (!path || !existsSync19(path)) return void 0;
4119
+ function readJson3(path) {
4120
+ if (!path || !existsSync22(path)) return void 0;
3847
4121
  try {
3848
- return JSON.parse(readFileSync14(path, "utf8"));
4122
+ return JSON.parse(readFileSync16(path, "utf8"));
3849
4123
  } catch {
3850
4124
  return void 0;
3851
4125
  }
@@ -3864,27 +4138,31 @@ function deduplicateIssues(issues) {
3864
4138
  import { execFileSync } from "node:child_process";
3865
4139
  import {
3866
4140
  cpSync as cpSync2,
3867
- existsSync as existsSync20,
4141
+ existsSync as existsSync23,
3868
4142
  lstatSync as lstatSync8,
3869
4143
  mkdirSync as mkdirSync8,
3870
- readFileSync as readFileSync15,
3871
- readdirSync as readdirSync8,
4144
+ readFileSync as readFileSync17,
4145
+ readdirSync as readdirSync9,
3872
4146
  realpathSync as realpathSync4,
3873
- statSync as statSync4,
4147
+ statSync as statSync5,
3874
4148
  writeFileSync as writeFileSync7
3875
4149
  } from "node:fs";
3876
- import { homedir as homedir2 } from "node:os";
3877
- import { dirname as dirname13, join as join21, relative as relative8, resolve as resolve5, sep } from "node:path";
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";
3878
4152
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3879
4153
  var CURRENT_ROUTER_VERSION = "1.1";
3880
4154
  function inspectPortfolioAiHealth(options) {
3881
- const endpoints = collectEndpoints(options.manifest);
3882
- const secretsRoot = options.secretsRoot ?? join21(dirname13(options.manifest.controlPlane?.path ?? endpoints[0]?.endpoint.path ?? process.cwd()), ".secrets");
3883
- const homeDir = options.homeDir ?? process.env.HOME ?? homedir2();
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();
3884
4162
  const grokVersion = commandVersion("grok");
3885
4163
  const executionEngineRoot = options.manifest.executionEngine?.path;
3886
4164
  const skillRegistry = inspectSkillRegistry(executionEngineRoot);
3887
- const expectedPackageVersion = packageVersion(join21(executionEngineRoot ?? "", "packages/pro-gov/package.json"));
4165
+ const expectedPackageVersion = packageVersion(join24(executionEngineRoot ?? "", "packages/pro-gov/package.json"));
3888
4166
  const repositories = endpoints.map(({ endpoint, role }) => inspectRepository(
3889
4167
  endpoint,
3890
4168
  role,
@@ -3897,20 +4175,27 @@ function inspectPortfolioAiHealth(options) {
3897
4175
  const summary = { healthy: 0, attention: 0, unhealthy: 0 };
3898
4176
  for (const repository of repositories) summary[repository.status] += 1;
3899
4177
  return {
3900
- schemaVersion: 4,
4178
+ schemaVersion: 5,
3901
4179
  portfolioId: options.manifest.portfolioId,
3902
4180
  generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
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
+ },
3903
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.",
3904
4188
  secretsRoot: inspectSecretsRoot(secretsRoot),
3905
4189
  hostEnvironment: inspectHostEnvironment(
3906
4190
  homeDir,
3907
4191
  grokVersion,
3908
- endpoints.map(({ endpoint }) => endpoint.path),
4192
+ allEndpoints.map(({ endpoint }) => endpoint.path),
3909
4193
  options.manifest.specialistChecks?.devspace
3910
4194
  ),
3911
4195
  skillRegistry,
3912
4196
  technologyGovernance: {
3913
4197
  strategySource: options.manifest.technologyGovernance?.strategySource,
4198
+ versionPolicy: options.manifest.technologyGovernance?.versionPolicy,
3914
4199
  projectTypes: options.manifest.technologyGovernance?.projectTypes.length ?? 0,
3915
4200
  technologies: options.manifest.technologyGovernance?.technologies.length ?? 0
3916
4201
  },
@@ -3918,19 +4203,44 @@ function inspectPortfolioAiHealth(options) {
3918
4203
  repositories
3919
4204
  };
3920
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
+ }
3921
4231
  function writePortfolioAiHealthReport(report, outDir) {
3922
4232
  mkdirSync8(outDir, { recursive: true });
3923
4233
  const dashboardAssets = findDashboardAssets();
3924
4234
  for (const file of ["index.html", "app.js", "app.css"]) {
3925
- const source = join21(dashboardAssets, file);
3926
- if (!existsSync20(source)) throw new Error(`Portfolio dashboard asset is missing: ${source}`);
3927
- cpSync2(source, join21(outDir, file));
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));
3928
4238
  }
3929
- const jsonPath = join21(outDir, "portfolio-ai-health.json");
3930
- const htmlPath = join21(outDir, "index.html");
4239
+ const jsonPath = join24(outDir, "portfolio-ai-health.json");
4240
+ const htmlPath = join24(outDir, "index.html");
3931
4241
  writeFileSync7(jsonPath, `${JSON.stringify(report, null, 2)}
3932
4242
  `);
3933
- writeFileSync7(join21(outDir, "data.js"), `window.__PORTFOLIO_AI_HEALTH__ = ${safeJavaScriptJson(report)};
4243
+ writeFileSync7(join24(outDir, "data.js"), `window.__PORTFOLIO_AI_HEALTH__ = ${safeJavaScriptJson(report)};
3934
4244
  `);
3935
4245
  return { jsonPath, htmlPath };
3936
4246
  }
@@ -3957,15 +4267,18 @@ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackage
3957
4267
  const hooks = inspectHooks(root);
3958
4268
  const docs = inspectDocs(root, role === "execution-engine" ? void 0 : expectedPackageVersion);
3959
4269
  const mcp = {
3960
- codexProject: tomlMcpNames(join21(root, ".codex/config.toml")),
3961
- claudeCodeProjectShared: jsonObjectKeys(join21(root, ".mcp.json"), "mcpServers"),
4270
+ codexProject: tomlMcpNames(join24(root, ".codex/config.toml")),
4271
+ claudeCodeProjectShared: jsonObjectKeys(join24(root, ".mcp.json"), "mcpServers"),
3962
4272
  claudeCodeProjectLocal: claudeProjectLocalMcpNames(homeDir, root),
3963
- grokProject: tomlMcpNames(join21(root, ".grok/config.toml")),
4273
+ grokProject: tomlMcpNames(join24(root, ".grok/config.toml")),
3964
4274
  grokEffective: grokInspection.effectiveMcp,
3965
4275
  grokInspection: grokInspection.inspection
3966
4276
  };
3967
4277
  const secrets = inspectRepositorySecrets(root, endpoint.id, secretsRoot, git.isRepository, endpoint.environmentPolicy);
3968
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 });
3969
4282
  const recommendations = [];
3970
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");
3971
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`);
@@ -4002,12 +4315,15 @@ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackage
4002
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`);
4003
4316
  const missingSelected = projectModel.optionalCapabilities.filter((technology) => technology.selected && !technology.detected);
4004
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");
4005
4321
  return {
4006
4322
  id: endpoint.id,
4007
4323
  role,
4008
4324
  path: root,
4009
4325
  profile: "profile" in endpoint ? endpoint.profile : void 0,
4010
- status: deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, Boolean(technologyGovernance)),
4326
+ status: deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, Boolean(technologyGovernance), versions, verification, redundancy),
4011
4327
  recommendations,
4012
4328
  git,
4013
4329
  entries,
@@ -4017,10 +4333,13 @@ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackage
4017
4333
  hostSsot,
4018
4334
  secrets,
4019
4335
  docs,
4020
- projectModel
4336
+ projectModel,
4337
+ versions,
4338
+ verification,
4339
+ redundancy
4021
4340
  };
4022
4341
  }
4023
- function deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, technologyGovernanceConfigured) {
4342
+ function deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, technologyGovernanceConfigured, versions, verification, redundancy) {
4024
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";
4025
4344
  const missingBaseline = projectModel.baseline.some((technology) => !technology.detected && !hasBaselineException(projectModel, technology.id));
4026
4345
  const missingSelected = projectModel.optionalCapabilities.some((technology) => technology.selected && !technology.detected);
@@ -4029,7 +4348,7 @@ function deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs
4029
4348
  const packageVersionNeedsReview = docs.packages.expected !== void 0 && !docs.packages.aligned;
4030
4349
  const routerVersionNeedsReview = !docs.routerAligned;
4031
4350
  const targetManifestMissing = role === "target" && !docs.manifest;
4032
- if (entries.agents !== "pgs-router" || entries.claude !== "agents-symlink" || hasWorkflowReminderHooks(hooks) || skills.claudeCompatibility === "duplicate-directory" || skills.claudeCompatibility === "dangling-symlink" || !hostSsot.compliant || git.branches.length > 1 || git.worktrees.length > 1 || git.dirtyPaths.length > 0 || (git.ahead ?? 0) > 0 || secretMaterializationNeedsReview || technologyGovernanceConfigured && !projectModel.projectType || missingBaseline || missingSelected || packageVersionNeedsReview || routerVersionNeedsReview || targetManifestMissing) return "attention";
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";
4033
4352
  return "healthy";
4034
4353
  }
4035
4354
  function inspectProjectModel(root, endpoint, governance) {
@@ -4039,7 +4358,7 @@ function inspectProjectModel(root, endpoint, governance) {
4039
4358
  const detection = (id) => {
4040
4359
  const technology = technologyById.get(id);
4041
4360
  const packageMatch = technology?.packages?.some((name) => packages.has(name)) ?? false;
4042
- const fileMatch = technology?.files?.some((path) => existsSync20(join21(root, path))) ?? false;
4361
+ const fileMatch = technology?.files?.some((path) => existsSync23(join24(root, path))) ?? false;
4043
4362
  return { id, label: technology?.label ?? id, detected: packageMatch || fileMatch };
4044
4363
  };
4045
4364
  const selected = new Set(endpoint.capabilities ?? []);
@@ -4062,10 +4381,10 @@ function collectPackageNames(root) {
4062
4381
  try {
4063
4382
  files = splitLines(execFileSync("git", ["ls-files", "*package.json"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }));
4064
4383
  } catch {
4065
- if (existsSync20(join21(root, "package.json"))) files = ["package.json"];
4384
+ if (existsSync23(join24(root, "package.json"))) files = ["package.json"];
4066
4385
  }
4067
4386
  for (const file of files) {
4068
- const packageJson = readJson3(join21(root, file));
4387
+ const packageJson = readJson4(join24(root, file));
4069
4388
  if (!isRecord3(packageJson)) continue;
4070
4389
  for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
4071
4390
  const dependencies = packageJson[section];
@@ -4103,9 +4422,9 @@ function inspectGit2(root) {
4103
4422
  };
4104
4423
  }
4105
4424
  function inspectEntries(root) {
4106
- const agentsPath = join21(root, "AGENTS.md");
4107
- const agents = !existsSync20(agentsPath) ? "missing" : safeRead(agentsPath).includes("PGS-ROUTER:BEGIN") ? "pgs-router" : "custom";
4108
- const claudePath = join21(root, "CLAUDE.md");
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");
4109
4428
  let claude = "missing";
4110
4429
  if (pathLexists(claudePath)) {
4111
4430
  const info = lstatSync8(claudePath);
@@ -4123,7 +4442,7 @@ function inspectEntries(root) {
4123
4442
  return { agents, claude, gemini: inspectOptionalEntry(root, "GEMINI.md", agentsPath) };
4124
4443
  }
4125
4444
  function inspectOptionalEntry(root, filename, agentsPath) {
4126
- const path = join21(root, filename);
4445
+ const path = join24(root, filename);
4127
4446
  if (!pathLexists(path)) return "missing";
4128
4447
  const info = lstatSync8(path);
4129
4448
  if (info.isSymbolicLink()) {
@@ -4137,7 +4456,7 @@ function inspectOptionalEntry(root, filename, agentsPath) {
4137
4456
  return /AGENTS\.md/.test(content) && content.length < 2e3 ? "thin-adapter" : "custom";
4138
4457
  }
4139
4458
  function inspectSkills(root, grokInspection) {
4140
- const lock = readJson3(join21(root, ".pro-gov/assets.lock.json"));
4459
+ const lock = readJson4(join24(root, ".pro-gov/assets.lock.json"));
4141
4460
  const managed = /* @__PURE__ */ new Set();
4142
4461
  const bundleIds = stringArray(isRecord3(lock) ? lock.bundleIds : void 0);
4143
4462
  if (isRecord3(lock) && Array.isArray(lock.assets)) {
@@ -4147,9 +4466,9 @@ function inspectSkills(root, grokInspection) {
4147
4466
  if (match) managed.add(match[1]);
4148
4467
  }
4149
4468
  }
4150
- const skillRoot = join21(root, ".agents/skills");
4469
+ const skillRoot = join24(root, ".agents/skills");
4151
4470
  const canonical = pathLexists(skillRoot) && safeIsDirectory(skillRoot) ? safeReadDir(skillRoot).filter((name) => !name.startsWith(".")).map((name) => {
4152
- const path = join21(skillRoot, name);
4471
+ const path = join24(skillRoot, name);
4153
4472
  const stat = lstatSync8(path);
4154
4473
  let kind = stat.isSymbolicLink() ? "symlink" : stat.isDirectory() ? "directory" : "file";
4155
4474
  if (stat.isSymbolicLink()) {
@@ -4175,20 +4494,20 @@ function inspectSkills(root, grokInspection) {
4175
4494
  },
4176
4495
  hosts: {
4177
4496
  codexProject: canonical.filter((item) => item.kind !== "dangling-symlink").length,
4178
- claudeCodeProject: inspectSkillRoot(join21(root, ".claude/skills")).names.length,
4179
- grokNativeProject: inspectSkillRoot(join21(root, ".grok/skills")).names.length,
4497
+ claudeCodeProject: inspectSkillRoot(join24(root, ".claude/skills")).names.length,
4498
+ grokNativeProject: inspectSkillRoot(join24(root, ".grok/skills")).names.length,
4180
4499
  grokEffective: grokInspection.skills
4181
4500
  }
4182
4501
  };
4183
4502
  }
4184
4503
  function inspectClaudeSkillRoot(root) {
4185
- const path = join21(root, ".claude/skills");
4504
+ const path = join24(root, ".claude/skills");
4186
4505
  if (!pathLexists(path)) return "missing";
4187
4506
  const stat = lstatSync8(path);
4188
4507
  if (stat.isSymbolicLink()) {
4189
4508
  try {
4190
4509
  const target = realpathSync4(path);
4191
- return target === realpathSync4(join21(root, ".agents/skills")) ? "shared-root" : "other";
4510
+ return target === realpathSync4(join24(root, ".agents/skills")) ? "shared-root" : "other";
4192
4511
  } catch {
4193
4512
  return "dangling-symlink";
4194
4513
  }
@@ -4196,7 +4515,7 @@ function inspectClaudeSkillRoot(root) {
4196
4515
  return stat.isDirectory() ? "duplicate-directory" : "other";
4197
4516
  }
4198
4517
  function inspectRepositorySecrets(root, id, secretsRoot, isRepository, environmentPolicy) {
4199
- const centralPath = join21(secretsRoot, id);
4518
+ const centralPath = join24(secretsRoot, id);
4200
4519
  const centralRealPath = safeRealpath(centralPath);
4201
4520
  const localOnlyReasons = new Map(
4202
4521
  (environmentPolicy?.localOnly ?? []).map((entry) => [entry.path, entry.reason])
@@ -4208,16 +4527,16 @@ function inspectRepositorySecrets(root, id, secretsRoot, isRepository, environme
4208
4527
  tracked: isRepository ? gitTracks(root, path) : false,
4209
4528
  template: isEnvironmentTemplate(path),
4210
4529
  fixture: isEnvironmentFixture(path),
4211
- symlink: lstatSync8(join21(root, path)).isSymbolicLink(),
4212
- centralized: pointsInside(join21(root, path), centralRealPath),
4530
+ symlink: lstatSync8(join24(root, path)).isSymbolicLink(),
4531
+ centralized: pointsInside(join24(root, path), centralRealPath),
4213
4532
  localOnly: localOnlyReason !== void 0,
4214
4533
  ...localOnlyReason !== void 0 ? { localOnlyReason } : {}
4215
4534
  };
4216
4535
  });
4217
4536
  return {
4218
- centralDirectory: existsSync20(centralPath) ? "present" : "absent",
4219
- centralMode: existsSync20(centralPath) ? modeString(statSync4(centralPath).mode) : void 0,
4220
- centralFiles: existsSync20(centralPath) ? collectCentralSecretFiles(centralPath) : [],
4537
+ centralDirectory: existsSync23(centralPath) ? "present" : "absent",
4538
+ centralMode: existsSync23(centralPath) ? modeString(statSync5(centralPath).mode) : void 0,
4539
+ centralFiles: existsSync23(centralPath) ? collectCentralSecretFiles(centralPath) : [],
4221
4540
  repositoryEnvFiles: envFiles
4222
4541
  };
4223
4542
  }
@@ -4226,11 +4545,11 @@ function collectEnvironmentFiles(root, current = root, depth = 0) {
4226
4545
  if (depth > 5) return [];
4227
4546
  const found = [];
4228
4547
  try {
4229
- for (const entry of readdirSync8(current, { withFileTypes: true })) {
4548
+ for (const entry of readdirSync9(current, { withFileTypes: true })) {
4230
4549
  if (entry.isDirectory()) {
4231
- if (!SKIP_ENV_DIRECTORIES.has(entry.name)) found.push(...collectEnvironmentFiles(root, join21(current, entry.name), depth + 1));
4550
+ if (!SKIP_ENV_DIRECTORIES.has(entry.name)) found.push(...collectEnvironmentFiles(root, join24(current, entry.name), depth + 1));
4232
4551
  } else if (isEnvironmentFilename(entry.name) && !isProviderGeneratedEnvironmentFile(entry.name)) {
4233
- found.push(relative8(root, join21(current, entry.name)));
4552
+ found.push(relative8(root, join24(current, entry.name)));
4234
4553
  }
4235
4554
  }
4236
4555
  } catch {
@@ -4242,8 +4561,8 @@ function collectCentralSecretFiles(root, current = root, depth = 0) {
4242
4561
  if (depth > 3) return [];
4243
4562
  const found = [];
4244
4563
  try {
4245
- for (const entry of readdirSync8(current, { withFileTypes: true })) {
4246
- const path = join21(current, entry.name);
4564
+ for (const entry of readdirSync9(current, { withFileTypes: true })) {
4565
+ const path = join24(current, entry.name);
4247
4566
  if (entry.isDirectory()) found.push(...collectCentralSecretFiles(root, path, depth + 1));
4248
4567
  else found.push({ path: relative8(root, path), mode: modeString(lstatSync8(path).mode) });
4249
4568
  }
@@ -4286,12 +4605,12 @@ function hasUnsafeCentralSecretPermissions(secrets) {
4286
4605
  return secrets.centralDirectory === "present" && (secrets.centralMode !== "700" || secrets.centralFiles.some((file) => file.mode !== "600"));
4287
4606
  }
4288
4607
  function inspectSecretsRoot(path) {
4289
- return existsSync20(path) ? { path, exists: true, mode: modeString(statSync4(path).mode) } : { path, exists: false };
4608
+ return existsSync23(path) ? { path, exists: true, mode: modeString(statSync5(path).mode) } : { path, exists: false };
4290
4609
  }
4291
4610
  function inspectHostEnvironment(homeDir, grokVersion, repositoryPaths, devspaceSettings) {
4292
- const codexConfig = join21(homeDir, ".codex/config.toml");
4293
- const claudeConfig = join21(homeDir, ".claude.json");
4294
- const grokConfig = join21(homeDir, ".grok/config.toml");
4611
+ const codexConfig = join24(homeDir, ".codex/config.toml");
4612
+ const claudeConfig = join24(homeDir, ".claude.json");
4613
+ const grokConfig = join24(homeDir, ".grok/config.toml");
4295
4614
  const hostEnvironment = {
4296
4615
  mcp: {
4297
4616
  codexUser: { path: codexConfig, names: tomlMcpNames(codexConfig) },
@@ -4299,11 +4618,11 @@ function inspectHostEnvironment(homeDir, grokVersion, repositoryPaths, devspaceS
4299
4618
  grokUser: { path: grokConfig, names: tomlMcpNames(grokConfig) }
4300
4619
  },
4301
4620
  skills: {
4302
- codexUser: inspectSkillRoot(join21(homeDir, ".agents/skills")),
4303
- claudeCodeUser: inspectSkillRoot(join21(homeDir, ".claude/skills")),
4304
- grokUser: inspectSkillRoot(join21(homeDir, ".grok/skills")),
4305
- grokAgentsCompatibility: inspectSkillRoot(join21(homeDir, ".agents/skills")),
4306
- grokClaudeCompatibility: inspectSkillRoot(join21(homeDir, ".claude/skills")),
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")),
4307
4626
  ssot: inspectUserSkillsSsot(homeDir)
4308
4627
  },
4309
4628
  grok: {
@@ -4323,9 +4642,9 @@ function inspectHostEnvironment(homeDir, grokVersion, repositoryPaths, devspaceS
4323
4642
  }
4324
4643
  function inspectDevSpaceHealth(options) {
4325
4644
  const run = options.run ?? runDevSpaceCommand;
4326
- const configDirectory = join21(options.homeDir, ".devspace");
4327
- const configPath = join21(configDirectory, "config.json");
4328
- const authPath = join21(configDirectory, "auth.json");
4645
+ const configDirectory = join24(options.homeDir, ".devspace");
4646
+ const configPath = join24(configDirectory, "config.json");
4647
+ const authPath = join24(configDirectory, "auth.json");
4329
4648
  const installedResult = run("devspace", ["--version"], 3e3);
4330
4649
  const installedVersion = installedResult.ok ? installedResult.stdout.match(/\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\b/)?.[0] : void 0;
4331
4650
  const latestResult = run("npm", ["view", "@waishnav/devspace", "version", "--registry=https://registry.npmjs.org/"], 5e3);
@@ -4335,14 +4654,14 @@ function inspectDevSpaceHealth(options) {
4335
4654
  const processEnvironment = pid ? run("ps", ["eww", "-p", pid, "-o", "command="], 3e3) : void 0;
4336
4655
  const processToolMode = processEnvironment?.ok ? processEnvironment.stdout.match(/(?:^|\s)DEVSPACE_TOOL_MODE=(minimal|full|codex)(?:\s|$)/)?.[1] : void 0;
4337
4656
  const doctor = !installedResult.ok ? "unavailable" : run("devspace", ["doctor"], 1e4).ok ? "ok" : "failed";
4338
- const configValue = readJson3(configPath);
4657
+ const configValue = readJson4(configPath);
4339
4658
  const configExists = isRecord3(configValue);
4340
4659
  const allowedRoots = configExists && Array.isArray(configValue.allowedRoots) ? configValue.allowedRoots.filter((value) => typeof value === "string") : [];
4341
4660
  const portfolioCoverage = !configExists || allowedRoots.length === 0 ? "unknown" : options.repositoryPaths.every((repositoryPath) => allowedRoots.some((root) => isPathInside(repositoryPath, root))) ? "complete" : "partial";
4342
4661
  const bind = configExists && typeof configValue.host === "string" ? isLoopbackHost(configValue.host) ? "loopback" : "non-loopback" : "unknown";
4343
- const directoryMode = existsSync20(configDirectory) ? modeString(statSync4(configDirectory).mode) : void 0;
4344
- const fileMode = existsSync20(configPath) ? modeString(statSync4(configPath).mode) : void 0;
4345
- const authMode = existsSync20(authPath) ? modeString(statSync4(authPath).mode) : void 0;
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;
4346
4665
  const update = installedVersion && latestVersion ? installedVersion === latestVersion ? "current" : "available" : "unknown";
4347
4666
  const recommendations = [];
4348
4667
  let status = "healthy";
@@ -4356,7 +4675,7 @@ function inspectDevSpaceHealth(options) {
4356
4675
  };
4357
4676
  if (!installedResult.ok) unhealthy("\u672C\u673A\u672A\u53D1\u73B0 DevSpace\uFF1B\u65E0\u6CD5\u4F7F\u7528\u5BBF\u4E3B\u5DE5\u4F5C\u533A\u670D\u52A1\u3002");
4358
4677
  if (!configExists) unhealthy("\u7F3A\u5C11 ~/.devspace/config.json\u3002");
4359
- if (!existsSync20(authPath)) unhealthy("\u7F3A\u5C11 ~/.devspace/auth.json\u3002");
4678
+ if (!existsSync23(authPath)) unhealthy("\u7F3A\u5C11 ~/.devspace/auth.json\u3002");
4360
4679
  if (directoryMode && directoryMode !== "700") unhealthy(`~/.devspace \u76EE\u5F55\u6743\u9650\u4E3A ${directoryMode}\uFF0C\u5E94\u6536\u7D27\u4E3A 700\u3002`);
4361
4680
  if (fileMode && fileMode !== "600") unhealthy(`DevSpace \u914D\u7F6E\u6587\u4EF6\u6743\u9650\u4E3A ${fileMode}\uFF0C\u5E94\u4E3A 600\u3002`);
4362
4681
  if (authMode && authMode !== "600") unhealthy(`DevSpace \u8BA4\u8BC1\u6587\u4EF6\u6743\u9650\u4E3A ${authMode}\uFF0C\u5E94\u4E3A 600\u3002`);
@@ -4382,7 +4701,7 @@ function inspectDevSpaceHealth(options) {
4382
4701
  exists: configExists,
4383
4702
  ...directoryMode ? { directoryMode } : {},
4384
4703
  ...fileMode ? { fileMode } : {},
4385
- authExists: existsSync20(authPath),
4704
+ authExists: existsSync23(authPath),
4386
4705
  ...authMode ? { authMode } : {},
4387
4706
  bind,
4388
4707
  portValid: configExists && typeof configValue.port === "number" && Number.isInteger(configValue.port) && configValue.port > 0 && configValue.port <= 65535,
@@ -4416,12 +4735,12 @@ function isPathInside(path, root) {
4416
4735
  }
4417
4736
  function inspectSkillRoot(path) {
4418
4737
  const exists = pathLexists(path) && safeIsDirectory(path);
4419
- const names = exists ? safeReadDir(path).filter((name) => !name.startsWith(".") && existsSync20(join21(path, name, "SKILL.md"))) : [];
4738
+ const names = exists ? safeReadDir(path).filter((name) => !name.startsWith(".") && existsSync23(join24(path, name, "SKILL.md"))) : [];
4420
4739
  return { path, exists, names };
4421
4740
  }
4422
4741
  function claudeProjectLocalMcpNames(homeDir, root) {
4423
4742
  if (!homeDir) return [];
4424
- const value = readJson3(join21(homeDir, ".claude.json"));
4743
+ const value = readJson4(join24(homeDir, ".claude.json"));
4425
4744
  if (!isRecord3(value) || !isRecord3(value.projects)) return [];
4426
4745
  const candidates = new Set([resolve5(root), safeRealpath(root)].filter((path) => Boolean(path)));
4427
4746
  const names = /* @__PURE__ */ new Set();
@@ -4457,13 +4776,13 @@ function inspectGrokProject(root, homeDir, grokVersion) {
4457
4776
  const value = JSON.parse(execFileSync("grok", ["inspect", "--json"], {
4458
4777
  cwd: root,
4459
4778
  encoding: "utf8",
4460
- env: { ...process.env, HOME: homeDir, GROK_HOME: join21(homeDir, ".grok") },
4779
+ env: { ...process.env, HOME: homeDir, GROK_HOME: join24(homeDir, ".grok") },
4461
4780
  maxBuffer: 10 * 1024 * 1024,
4462
4781
  stdio: ["ignore", "pipe", "ignore"],
4463
4782
  timeout: 8e3
4464
4783
  }));
4465
4784
  if (!isRecord3(value)) return empty("failed");
4466
- const userClaudeNames = new Set(jsonObjectKeys(join21(homeDir, ".claude.json"), "mcpServers"));
4785
+ const userClaudeNames = new Set(jsonObjectKeys(join24(homeDir, ".claude.json"), "mcpServers"));
4467
4786
  const localClaudeNames = new Set(claudeProjectLocalMcpNames(homeDir, root));
4468
4787
  const effectiveMcp = Array.isArray(value.mcpServers) ? value.mcpServers.flatMap((item) => {
4469
4788
  if (!isRecord3(item) || typeof item.name !== "string") return [];
@@ -4521,9 +4840,9 @@ function inferGrokMcpScope(name, sourceType, sourcePath, root, homeDir, userClau
4521
4840
  }
4522
4841
  const resolvedSource = safeRealpath(sourcePath) ?? resolve5(sourcePath);
4523
4842
  const resolvedRoot = safeRealpath(root) ?? resolve5(root);
4524
- if (resolvedSource === join21(resolvedRoot, ".mcp.json")) return "project-shared";
4843
+ if (resolvedSource === join24(resolvedRoot, ".mcp.json")) return "project-shared";
4525
4844
  if (resolvedSource.startsWith(resolvedRoot + sep)) return "project";
4526
- if (homeDir && resolvedSource === join21(resolve5(homeDir), ".claude.json")) {
4845
+ if (homeDir && resolvedSource === join24(resolve5(homeDir), ".claude.json")) {
4527
4846
  if (localClaudeNames.has(name)) return "project-local";
4528
4847
  if (userClaudeNames.has(name)) return "user";
4529
4848
  }
@@ -4558,7 +4877,7 @@ function inspectHooks(root) {
4558
4877
  { host: "codex", path: ".codex/hooks.json" }
4559
4878
  ];
4560
4879
  return configs.map((config) => {
4561
- const value = readJson3(join21(root, config.path));
4880
+ const value = readJson4(join24(root, config.path));
4562
4881
  const counts = /* @__PURE__ */ new Map();
4563
4882
  collectHookEvents(value, counts);
4564
4883
  return {
@@ -4579,18 +4898,18 @@ function collectHookEvents(value, counts) {
4579
4898
  }
4580
4899
  }
4581
4900
  function inspectDocs(root, expected) {
4582
- const packageJson = readJson3(join21(root, "package.json"));
4901
+ const packageJson = readJson4(join24(root, "package.json"));
4583
4902
  const dependencies = isRecord3(packageJson) ? { ...recordOrEmpty(packageJson.dependencies), ...recordOrEmpty(packageJson.devDependencies) } : {};
4584
4903
  const docGov = dependencyVersion(dependencies["@pieai/doc-gov"]);
4585
4904
  const proGov = dependencyVersion(dependencies["@pieai/pro-gov"]);
4586
- const routerMatch = safeRead(join21(root, "AGENTS.md")).match(/PGS-ROUTER:BEGIN\s+v([0-9.]+)/);
4905
+ const routerMatch = safeRead(join24(root, "AGENTS.md")).match(/PGS-ROUTER:BEGIN\s+v([0-9.]+)/);
4587
4906
  const declared = [docGov, proGov].filter((value) => Boolean(value));
4588
4907
  return {
4589
4908
  routerVersion: routerMatch?.[1],
4590
4909
  expectedRouterVersion: CURRENT_ROUTER_VERSION,
4591
4910
  routerAligned: routerMatch?.[1] === CURRENT_ROUTER_VERSION,
4592
- manifest: existsSync20(join21(root, "docs/governance/MANIFEST.yml")),
4593
- currentWork: existsSync20(join21(root, "docs/reference/execution/current-work.md")),
4911
+ manifest: existsSync23(join24(root, "docs/governance/MANIFEST.yml")),
4912
+ currentWork: existsSync23(join24(root, "docs/reference/execution/current-work.md")),
4594
4913
  packages: {
4595
4914
  expected,
4596
4915
  docGov,
@@ -4605,7 +4924,7 @@ function dependencyVersion(value) {
4605
4924
  return match?.[1];
4606
4925
  }
4607
4926
  function packageVersion(path) {
4608
- const value = readJson3(path);
4927
+ const value = readJson4(path);
4609
4928
  return isRecord3(value) && typeof value.version === "string" ? value.version : void 0;
4610
4929
  }
4611
4930
  function recordOrEmpty(value) {
@@ -4613,20 +4932,20 @@ function recordOrEmpty(value) {
4613
4932
  }
4614
4933
  function inspectSkillRegistry(executionEngineRoot) {
4615
4934
  if (!executionEngineRoot) return { source: 0, registered: 0, bundled: 0, bundles: 0 };
4616
- const agentAssetsRoot = join21(executionEngineRoot, "agent-assets");
4617
- const registry = readJson3(join21(agentAssetsRoot, "registry.json"));
4935
+ const agentAssetsRoot = join24(executionEngineRoot, "agent-assets");
4936
+ const registry = readJson4(join24(agentAssetsRoot, "registry.json"));
4618
4937
  const assets = isRecord3(registry) && Array.isArray(registry.assets) ? registry.assets : [];
4619
4938
  const registeredSkills = assets.filter((asset) => isRecord3(asset) && asset.kind === "skill");
4620
- const bundleRoot = join21(agentAssetsRoot, "bundles");
4939
+ const bundleRoot = join24(agentAssetsRoot, "bundles");
4621
4940
  const bundleFiles = safeReadDir(bundleRoot).filter((file) => file.endsWith(".json"));
4622
4941
  const bundledIds = /* @__PURE__ */ new Set();
4623
4942
  for (const file of bundleFiles) {
4624
- const bundle = readJson3(join21(bundleRoot, file));
4943
+ const bundle = readJson4(join24(bundleRoot, file));
4625
4944
  if (!isRecord3(bundle) || !Array.isArray(bundle.assets)) continue;
4626
4945
  for (const id of bundle.assets) if (typeof id === "string") bundledIds.add(id);
4627
4946
  }
4628
- const sourceRoots = [join21(agentAssetsRoot, "skills/pie-skills"), join21(agentAssetsRoot, "skills/npx-skills/.agents/skills")];
4629
- const source = sourceRoots.reduce((count, root) => count + safeReadDir(root).filter((name) => existsSync20(join21(root, name, "SKILL.md"))).length, 0);
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);
4630
4949
  return {
4631
4950
  source,
4632
4951
  registered: registeredSkills.length,
@@ -4635,12 +4954,12 @@ function inspectSkillRegistry(executionEngineRoot) {
4635
4954
  };
4636
4955
  }
4637
4956
  function jsonObjectKeys(path, key) {
4638
- const value = readJson3(path);
4957
+ const value = readJson4(path);
4639
4958
  if (!isRecord3(value) || !isRecord3(value[key])) return [];
4640
4959
  return Object.keys(value[key]).sort();
4641
4960
  }
4642
4961
  function tomlMcpNames(path) {
4643
- if (!existsSync20(path)) return [];
4962
+ if (!existsSync23(path)) return [];
4644
4963
  const names = /* @__PURE__ */ new Set();
4645
4964
  for (const line of safeRead(path).split(/\r?\n/)) {
4646
4965
  const match = line.match(/^\s*\[mcp_servers\.(?:"([^"]+)"|([^\.\]]+))\]\s*$/);
@@ -4649,30 +4968,30 @@ function tomlMcpNames(path) {
4649
4968
  }
4650
4969
  return [...names].sort();
4651
4970
  }
4652
- function readJson3(path) {
4971
+ function readJson4(path) {
4653
4972
  try {
4654
- return JSON.parse(readFileSync15(path, "utf8"));
4973
+ return JSON.parse(readFileSync17(path, "utf8"));
4655
4974
  } catch {
4656
4975
  return void 0;
4657
4976
  }
4658
4977
  }
4659
4978
  function safeRead(path) {
4660
4979
  try {
4661
- return readFileSync15(path, "utf8");
4980
+ return readFileSync17(path, "utf8");
4662
4981
  } catch {
4663
4982
  return "";
4664
4983
  }
4665
4984
  }
4666
4985
  function safeReadDir(path) {
4667
4986
  try {
4668
- return readdirSync8(path).sort();
4987
+ return readdirSync9(path).sort();
4669
4988
  } catch {
4670
4989
  return [];
4671
4990
  }
4672
4991
  }
4673
4992
  function safeIsDirectory(path) {
4674
4993
  try {
4675
- return statSync4(path).isDirectory();
4994
+ return statSync5(path).isDirectory();
4676
4995
  } catch {
4677
4996
  return false;
4678
4997
  }
@@ -4709,14 +5028,14 @@ function findDashboardAssets() {
4709
5028
  const packageRoot2 = dirname13(dirname13(fileURLToPath4(import.meta.url)));
4710
5029
  const candidates = [
4711
5030
  process.env.PGS_DASHBOARD_ASSETS_DIR,
4712
- join21(packageRoot2, ".dashboard-build"),
4713
- join21(packageRoot2, "assets/portfolio-dashboard"),
4714
- join21(process.cwd(), ".dashboard-build"),
4715
- join21(process.cwd(), "assets/portfolio-dashboard"),
4716
- join21(process.cwd(), "packages/pro-gov/.dashboard-build"),
4717
- join21(process.cwd(), "packages/pro-gov/assets/portfolio-dashboard")
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")
4718
5037
  ].filter((value) => Boolean(value));
4719
- const match = candidates.find((path) => existsSync20(join21(path, "index.html")));
5038
+ const match = candidates.find((path) => existsSync23(join24(path, "index.html")));
4720
5039
  if (!match) throw new Error("Portfolio dashboard assets were not built. Run pnpm --filter @pieai/pro-gov build.");
4721
5040
  return match;
4722
5041
  }
@@ -4752,10 +5071,23 @@ function runPortfolioAiHealth(args) {
4752
5071
  for (const issue of loaded.issues) console.error(`${issue.type}: ${issue.message}`);
4753
5072
  return 1;
4754
5073
  }
4755
- const report = inspectPortfolioAiHealth({
5074
+ const targetId = options.value.targetId && options.value.targetId !== "all" ? options.value.targetId : void 0;
5075
+ if (targetId && !loaded.manifest.targets.some((target) => target.id === targetId)) {
5076
+ console.error(`Unknown portfolio target: ${targetId}`);
5077
+ return 1;
5078
+ }
5079
+ const latest = inspectPortfolioAiHealth({
4756
5080
  manifest: loaded.manifest,
4757
- secretsRoot: options.value.secretsRoot
5081
+ secretsRoot: options.value.secretsRoot,
5082
+ targetId
4758
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;
4759
5091
  const written = writePortfolioAiHealthReport(report, options.value.outDir);
4760
5092
  if (options.value.json) {
4761
5093
  console.log(JSON.stringify({ ok: true, ...written, summary: report.summary }, null, 2));
@@ -4814,6 +5146,12 @@ function runPortfolioDoctor(args) {
4814
5146
  for (const warning of target.hostSsot.issues) {
4815
5147
  console.log(`${target.id} host-ssot-warning: ${warning}`);
4816
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
+ }
4817
5155
  }
4818
5156
  if (result.ok) {
4819
5157
  console.log(`portfolio doctor passed (${targets.length} targets)`);
@@ -5078,8 +5416,8 @@ function isHost2(value) {
5078
5416
  return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
5079
5417
  }
5080
5418
  function findPortfolioAgentAssetsDir(manifest) {
5081
- const agentAssetsDir = manifest?.executionEngine?.path ? join22(manifest.executionEngine.path, "agent-assets") : void 0;
5082
- return agentAssetsDir && existsSync21(join22(agentAssetsDir, "registry.json")) ? agentAssetsDir : void 0;
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;
5083
5421
  }
5084
5422
  function printUsage4() {
5085
5423
  console.error("Usage:");
@@ -5087,12 +5425,24 @@ function printUsage4() {
5087
5425
  console.error(" pro-gov portfolio plan --config <path> [--target <id|all>] [--host codex|claude-code|gemini-cli|antigravity] [--json]");
5088
5426
  console.error(" pro-gov portfolio assets-check --config <path> [--target <id|all>] [--json]");
5089
5427
  console.error(" pro-gov portfolio doctor --config <path> [--target <id|all>] [--json]");
5090
- 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
+ }
5091
5441
  }
5092
5442
 
5093
5443
  // src/commands/sync.ts
5094
- import { existsSync as existsSync22, lstatSync as lstatSync9, readFileSync as readFileSync16, readlinkSync as readlinkSync4 } from "node:fs";
5095
- import { join as join23 } from "node:path";
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";
5096
5446
  function runSync(args) {
5097
5447
  const check = args.includes("--check");
5098
5448
  if (!check) {
@@ -5120,7 +5470,7 @@ function runSync(args) {
5120
5470
  console.log("pro-gov sync check");
5121
5471
  console.log(`profile: ${profile}`);
5122
5472
  for (const file of planStarterFiles(profile)) {
5123
- const targetPath = join23(process.cwd(), file.targetPath);
5473
+ const targetPath = join26(process.cwd(), file.targetPath);
5124
5474
  const stat = safeLstat3(targetPath);
5125
5475
  if (!stat) {
5126
5476
  if (file.ownership === "optional-guardrail") continue;
@@ -5144,8 +5494,8 @@ function runSync(args) {
5144
5494
  }
5145
5495
  continue;
5146
5496
  }
5147
- const source = readFileSync16(file.absoluteSourcePath, "utf8");
5148
- const target = readFileSync16(targetPath, "utf8");
5497
+ const source = readFileSync19(file.absoluteSourcePath, "utf8");
5498
+ const target = readFileSync19(targetPath, "utf8");
5149
5499
  if (!matchesExpectedContent(file.targetPath, source, target)) {
5150
5500
  console.log(`different: ${file.targetPath}`);
5151
5501
  differences += 1;
@@ -5179,7 +5529,7 @@ function normalizeMarkdownTableCell(cell) {
5179
5529
  }
5180
5530
  function inferInstalledProfile(root) {
5181
5531
  const installed = ["engineering-runtime", "doc-only"].filter(
5182
- (profile) => existsSync22(join23(root, `docs/governance/agents-routing/${profile}-v1.1.md`))
5532
+ (profile) => existsSync25(join26(root, `docs/governance/agents-routing/${profile}-v1.1.md`))
5183
5533
  );
5184
5534
  return installed.length === 1 ? installed[0] : void 0;
5185
5535
  }