@pieai/pro-gov 0.6.0 → 0.7.1

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,102 @@ 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", "runtimes", "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
+ validateRuntimeRequirements(value.runtimes, projectTypes, issues);
3481
+ if (!Array.isArray(value.packages)) {
3482
+ issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.packages", message: "Technology versionPolicy packages must be an array." });
3483
+ return;
3484
+ }
3485
+ const seen = /* @__PURE__ */ new Set();
3486
+ for (const entry of value.packages) {
3487
+ if (!isRecord(entry)) {
3488
+ issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.packages", message: "Version policy package entries must be objects." });
3489
+ continue;
3490
+ }
3491
+ validateAllowedFields(entry, "versionPolicy.package", ["name", "version", "appliesTo"], issues);
3492
+ const name = typeof entry.name === "string" ? entry.name : "";
3493
+ if (!name || seen.has(name)) {
3494
+ issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.packages.name", message: `Version policy package name must be non-empty and unique: ${String(entry.name)}` });
3495
+ } else seen.add(name);
3496
+ if (typeof entry.version !== "string" || !isExactVersion(entry.version)) {
3497
+ issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.packages.version", message: `Version policy package version must be exact semver: ${String(entry.version)}` });
3498
+ }
3499
+ if (entry.appliesTo !== void 0) {
3500
+ validateOptionalStringArray(entry.appliesTo, name, "technologyGovernance.versionPolicy.packages.appliesTo", issues);
3501
+ if (Array.isArray(entry.appliesTo)) {
3502
+ for (const projectType of entry.appliesTo) {
3503
+ if (typeof projectType === "string" && !projectTypes.has(projectType)) {
3504
+ issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.packages.appliesTo", message: `Version policy references unknown project type: ${projectType}` });
3505
+ }
3506
+ }
3507
+ }
3508
+ }
3509
+ }
3510
+ }
3511
+ function validateRuntimeRequirements(value, projectTypes, issues) {
3512
+ if (value === void 0) return;
3513
+ const field = "technologyGovernance.versionPolicy.runtimes";
3514
+ if (!Array.isArray(value)) {
3515
+ issues.push({ type: "invalid-field", field, message: "Technology versionPolicy runtimes must be an array." });
3516
+ return;
3517
+ }
3518
+ const seen = /* @__PURE__ */ new Set();
3519
+ for (const entry of value) {
3520
+ if (!isRecord(entry)) {
3521
+ issues.push({ type: "invalid-field", field, message: "Version policy runtime entries must be objects." });
3522
+ continue;
3523
+ }
3524
+ validateAllowedFields(entry, "versionPolicy.runtimeEntry", ["name", "version", "appliesTo"], issues);
3525
+ const name = typeof entry.name === "string" ? entry.name : "";
3526
+ if (!name || seen.has(name)) {
3527
+ issues.push({ type: "invalid-field", field: `${field}.name`, message: `Version policy runtime name must be non-empty and unique: ${String(entry.name)}` });
3528
+ } else seen.add(name);
3529
+ if (typeof entry.version !== "string" || !isExactVersion(entry.version)) {
3530
+ issues.push({ type: "invalid-field", field: `${field}.version`, message: `Version policy runtime version must be exact semver: ${String(entry.version)}` });
3531
+ }
3532
+ if (entry.appliesTo !== void 0) {
3533
+ validateOptionalStringArray(entry.appliesTo, name, `${field}.appliesTo`, issues);
3534
+ if (Array.isArray(entry.appliesTo)) {
3535
+ for (const projectType of entry.appliesTo) {
3536
+ if (typeof projectType === "string" && !projectTypes.has(projectType)) {
3537
+ issues.push({ type: "invalid-field", field: `${field}.appliesTo`, message: `Version policy references unknown project type: ${projectType}` });
3538
+ }
3539
+ }
3540
+ }
3541
+ }
3542
+ }
3543
+ }
3544
+ function validateVersionRequirement(value, field, issues) {
3545
+ if (value === void 0) return;
3546
+ if (!isRecord(value)) {
3547
+ issues.push({ type: "invalid-field", field, message: "Version requirement must be an object." });
3548
+ return;
3549
+ }
3550
+ validateAllowedFields(value, field, ["name", "version"], issues);
3551
+ if (typeof value.name !== "string" || value.name.length === 0) {
3552
+ issues.push({ type: "invalid-field", field: `${field}.name`, message: "Version requirement name must be non-empty." });
3553
+ }
3554
+ if (typeof value.version !== "string" || !isExactVersion(value.version)) {
3555
+ issues.push({ type: "invalid-field", field: `${field}.version`, message: `Version requirement version must be exact semver: ${String(value.version)}` });
3556
+ }
3557
+ }
3558
+ function isExactVersion(value) {
3559
+ return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value);
3560
+ }
3332
3561
  function isRepositoryRelativePath(value) {
3333
3562
  if (value.length === 0 || isAbsolute4(value)) return false;
3334
3563
  const segments = value.replaceAll("\\", "/").split("/");
@@ -3502,11 +3731,11 @@ function isRecord(value) {
3502
3731
  }
3503
3732
 
3504
3733
  // src/portfolio/doctor.ts
3505
- import { spawnSync as spawnSync5 } from "node:child_process";
3506
- import { existsSync as existsSync19, readFileSync as readFileSync14 } from "node:fs";
3734
+ import { spawnSync as spawnSync6 } from "node:child_process";
3735
+ import { existsSync as existsSync22, readFileSync as readFileSync16 } from "node:fs";
3507
3736
  import { createRequire as createRequire2 } from "node:module";
3508
- import { homedir } from "node:os";
3509
- import { dirname as dirname12, join as join20 } from "node:path";
3737
+ import { homedir as homedir3 } from "node:os";
3738
+ import { dirname as dirname12, join as join23 } from "node:path";
3510
3739
  import { fileURLToPath as fileURLToPath3 } from "node:url";
3511
3740
 
3512
3741
  // src/host-tooling/inventory.ts
@@ -3597,13 +3826,13 @@ function isRecord2(value) {
3597
3826
  }
3598
3827
 
3599
3828
  // 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";
3829
+ import { existsSync as existsSync20, lstatSync as lstatSync7, readFileSync as readFileSync14 } from "node:fs";
3830
+ import { join as join21 } from "node:path";
3602
3831
  function comparePortfolioAssetState(options) {
3603
3832
  const expectedManifest = readPlanDocument(options.expectedPlan, ".pro-gov/assets.json");
3604
3833
  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"));
3834
+ const currentManifest = readJsonFile(join21(options.targetDir, ".pro-gov/assets.json"));
3835
+ const currentLock = readJsonFile(join21(options.targetDir, ".pro-gov/assets.lock.json"));
3607
3836
  const issues = [];
3608
3837
  if (!sameStrings(currentManifest?.bundleIds, expectedManifest?.bundleIds)) {
3609
3838
  issues.push({
@@ -3627,7 +3856,7 @@ function comparePortfolioAssetState(options) {
3627
3856
  for (const entry of currentLock?.assets ?? []) {
3628
3857
  if (expectedTargets.has(entry.targetPath)) continue;
3629
3858
  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);
3859
+ const targetAbsolutePath = join21(options.targetDir, entry.targetPath);
3631
3860
  if (!pathIsSymlink(targetAbsolutePath)) continue;
3632
3861
  issues.push({
3633
3862
  type: "orphaned-managed-symlink",
@@ -3649,9 +3878,9 @@ function readPlanDocument(plan, targetPath) {
3649
3878
  }
3650
3879
  }
3651
3880
  function readJsonFile(path) {
3652
- if (!existsSync18(path)) return void 0;
3881
+ if (!existsSync20(path)) return void 0;
3653
3882
  try {
3654
- return JSON.parse(readFileSync13(path, "utf8"));
3883
+ return JSON.parse(readFileSync14(path, "utf8"));
3655
3884
  } catch {
3656
3885
  return void 0;
3657
3886
  }
@@ -3678,6 +3907,95 @@ function pathIsSymlink(path) {
3678
3907
  }
3679
3908
  }
3680
3909
 
3910
+ // src/portfolio/version-policy.ts
3911
+ import { spawnSync as spawnSync5 } from "node:child_process";
3912
+ import { existsSync as existsSync21, readFileSync as readFileSync15 } from "node:fs";
3913
+ import { join as join22 } from "node:path";
3914
+ function inspectVersionPolicy(root, policy, projectType) {
3915
+ if (!policy) return { status: "compliant", packages: [], runtimes: [], attentionCount: 0 };
3916
+ const packageJson = readJson2(join22(root, "package.json"));
3917
+ const packageManager = policy.packageManager ? inspectPackageManager(packageJson, policy.packageManager.name, policy.packageManager.version) : void 0;
3918
+ const runtime = policy.runtime ? inspectRuntime(policy.runtime.name, policy.runtime.version) : void 0;
3919
+ const runtimes = (policy.runtimes ?? []).map((requirement) => {
3920
+ if (requirement.appliesTo && (!projectType || !requirement.appliesTo.includes(projectType))) {
3921
+ return { name: requirement.name, expected: requirement.version, status: "not-applicable" };
3922
+ }
3923
+ return inspectRuntime(requirement.name, requirement.version);
3924
+ });
3925
+ const packages = policy.packages.map((requirement) => {
3926
+ if (requirement.appliesTo && (!projectType || !requirement.appliesTo.includes(projectType))) {
3927
+ return { name: requirement.name, expected: requirement.version, status: "not-applicable" };
3928
+ }
3929
+ const declared = findDeclaredVersion(packageJson, requirement.name);
3930
+ const installed = readInstalledVersion(root, requirement.name);
3931
+ const status = declared === void 0 ? requirement.appliesTo ? "missing" : "not-applicable" : declared !== requirement.version ? "drift" : installed !== requirement.version ? installed === void 0 ? "missing" : "drift" : "compliant";
3932
+ return {
3933
+ name: requirement.name,
3934
+ expected: requirement.version,
3935
+ declared,
3936
+ installed,
3937
+ actual: installed,
3938
+ status
3939
+ };
3940
+ });
3941
+ const all = [packageManager, runtime, ...runtimes, ...packages].filter((item) => item !== void 0);
3942
+ const attentionCount = all.filter((item) => item.status !== "compliant" && item.status !== "not-applicable").length;
3943
+ return {
3944
+ status: attentionCount === 0 ? "compliant" : "attention",
3945
+ packageManager,
3946
+ runtime,
3947
+ runtimes,
3948
+ packages,
3949
+ attentionCount
3950
+ };
3951
+ }
3952
+ function inspectPackageManager(packageJson, expectedName, expectedVersion) {
3953
+ const declared = typeof packageJson?.packageManager === "string" ? packageJson.packageManager : void 0;
3954
+ const expected = `${expectedName}@${expectedVersion}`;
3955
+ return {
3956
+ name: expectedName,
3957
+ expected: expectedVersion,
3958
+ actual: declared,
3959
+ declared,
3960
+ status: declared === void 0 ? "missing" : declared === expected ? "compliant" : "drift"
3961
+ };
3962
+ }
3963
+ function inspectRuntime(expectedName, expectedVersion) {
3964
+ const actual = readRuntimeVersion(expectedName);
3965
+ return {
3966
+ name: expectedName,
3967
+ expected: expectedVersion,
3968
+ actual,
3969
+ status: actual === void 0 ? "missing" : actual === expectedVersion ? "compliant" : "drift"
3970
+ };
3971
+ }
3972
+ function readRuntimeVersion(name) {
3973
+ if (name === "node") return process.versions.node;
3974
+ if (name !== "deno") return void 0;
3975
+ const result = spawnSync5("deno", ["--version"], { encoding: "utf8" });
3976
+ if (result.status !== 0) return void 0;
3977
+ return /^deno\s+(\d+\.\d+\.\d+)/m.exec(result.stdout)?.[1];
3978
+ }
3979
+ function findDeclaredVersion(packageJson, name) {
3980
+ for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
3981
+ const value = packageJson?.[section]?.[name];
3982
+ if (typeof value === "string") return value;
3983
+ }
3984
+ return void 0;
3985
+ }
3986
+ function readInstalledVersion(root, name) {
3987
+ const packageJson = readJson2(join22(root, "node_modules", name, "package.json"));
3988
+ return typeof packageJson?.version === "string" ? packageJson.version : void 0;
3989
+ }
3990
+ function readJson2(path) {
3991
+ if (!existsSync21(path)) return void 0;
3992
+ try {
3993
+ return JSON.parse(readFileSync15(path, "utf8"));
3994
+ } catch {
3995
+ return void 0;
3996
+ }
3997
+ }
3998
+
3681
3999
  // src/portfolio/doctor.ts
3682
4000
  function inspectPortfolio(options) {
3683
4001
  const expectedPackageVersions = getExpectedPackageVersions();
@@ -3687,14 +4005,15 @@ function inspectPortfolio(options) {
3687
4005
  agentAssetsDir: options.agentAssetsDir,
3688
4006
  registry: options.registry,
3689
4007
  bundles: options.bundles,
3690
- expectedPackageVersions
4008
+ expectedPackageVersions,
4009
+ versionPolicy: options.manifest.technologyGovernance?.versionPolicy
3691
4010
  }));
3692
4011
  return {
3693
4012
  ok: hostTooling.issues.length === 0 && targets.every((target) => target.issues.length === 0),
3694
4013
  portfolioId: options.manifest.portfolioId,
3695
4014
  expectedPackageVersions,
3696
4015
  hostTooling,
3697
- hostSsot: { userSkills: inspectUserSkillsSsot(options.homeDir ?? homedir()) },
4016
+ hostSsot: { userSkills: inspectUserSkillsSsot(options.homeDir ?? homedir3()) },
3698
4017
  targets
3699
4018
  };
3700
4019
  }
@@ -3702,11 +4021,11 @@ function inspectTarget(options) {
3702
4021
  const { target } = options;
3703
4022
  const hostSsot = inspectProjectHostSsot(target.path);
3704
4023
  const issues = [];
3705
- const packageJson = readJson2(join20(target.path, "package.json"));
4024
+ const packageJson = readJson3(join23(target.path, "package.json"));
3706
4025
  const packages = {};
3707
4026
  for (const packageName of ["@pieai/pro-gov", "@pieai/doc-gov"]) {
3708
4027
  const declared = packageJson?.devDependencies?.[packageName] ?? packageJson?.dependencies?.[packageName];
3709
- const installedPackage = readJson2(join20(target.path, "node_modules", packageName, "package.json"));
4028
+ const installedPackage = readJson3(join23(target.path, "node_modules", packageName, "package.json"));
3710
4029
  const installed = installedPackage?.version;
3711
4030
  const expected = options.expectedPackageVersions[packageName];
3712
4031
  packages[packageName] = { declared, installed, expected };
@@ -3726,6 +4045,8 @@ function inspectTarget(options) {
3726
4045
  }
3727
4046
  }
3728
4047
  const checks = runTargetChecks(target);
4048
+ const versions = inspectVersionPolicy(target.path, options.versionPolicy, target.projectType);
4049
+ const verification = inspectProjectVerification(target.path);
3729
4050
  for (const check of checks) {
3730
4051
  if (check.status === 0) continue;
3731
4052
  issues.push({
@@ -3760,7 +4081,7 @@ function inspectTarget(options) {
3760
4081
  type: "asset-lock-drift",
3761
4082
  message: error instanceof Error ? error.message : String(error)
3762
4083
  });
3763
- if (!existsSync19(join20(target.path, ".pro-gov/assets.json"))) {
4084
+ if (!existsSync22(join23(target.path, ".pro-gov/assets.json"))) {
3764
4085
  issues.push({ type: "bundle-drift", message: "Target asset manifest is missing." });
3765
4086
  }
3766
4087
  }
@@ -3772,19 +4093,21 @@ function inspectTarget(options) {
3772
4093
  git: inspectGit(target.path),
3773
4094
  checks,
3774
4095
  hostSsot,
4096
+ versions,
4097
+ verification,
3775
4098
  issues: deduplicateIssues(issues)
3776
4099
  };
3777
4100
  }
3778
4101
  function readTargetAssetHost(targetDir) {
3779
- const lockfile = readJson2(join20(targetDir, ".pro-gov/assets.lock.json"));
4102
+ const lockfile = readJson3(join23(targetDir, ".pro-gov/assets.lock.json"));
3780
4103
  return isAssetRegistryHost(lockfile?.host) ? lockfile.host : void 0;
3781
4104
  }
3782
4105
  function isAssetRegistryHost(value) {
3783
4106
  return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
3784
4107
  }
3785
4108
  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");
4109
+ const proGovCli = join23(target.path, "node_modules/@pieai/pro-gov/dist/cli.js");
4110
+ const docGovCli = join23(target.path, "node_modules/@pieai/doc-gov/dist/cli.js");
3788
4111
  const commands = [
3789
4112
  {
3790
4113
  name: "pro-gov doctor",
@@ -3795,8 +4118,8 @@ function runTargetChecks(target) {
3795
4118
  { name: "doc-gov scan --check", cli: docGovCli, args: ["scan", "--check"] }
3796
4119
  ];
3797
4120
  return commands.map((command2) => {
3798
- if (!existsSync19(command2.cli)) return { name: command2.name, status: null };
3799
- const result = spawnSync5(process.execPath, [command2.cli, ...command2.args], {
4121
+ if (!existsSync22(command2.cli)) return { name: command2.name, status: null };
4122
+ const result = spawnSync6(process.execPath, [command2.cli, ...command2.args], {
3800
4123
  cwd: target.path,
3801
4124
  encoding: "utf8",
3802
4125
  timeout: 3e4
@@ -3805,13 +4128,13 @@ function runTargetChecks(target) {
3805
4128
  });
3806
4129
  }
3807
4130
  function inspectGit(path) {
3808
- const inside = spawnSync5("git", ["rev-parse", "--is-inside-work-tree"], {
4131
+ const inside = spawnSync6("git", ["rev-parse", "--is-inside-work-tree"], {
3809
4132
  cwd: path,
3810
4133
  encoding: "utf8"
3811
4134
  });
3812
4135
  if (inside.status !== 0) return { isRepository: false, dirty: false };
3813
- const status = spawnSync5("git", ["status", "--porcelain"], { cwd: path, encoding: "utf8" });
3814
- const branch = spawnSync5("git", ["branch", "--show-current"], { cwd: path, encoding: "utf8" });
4136
+ const status = spawnSync6("git", ["status", "--porcelain"], { cwd: path, encoding: "utf8" });
4137
+ const branch = spawnSync6("git", ["branch", "--show-current"], { cwd: path, encoding: "utf8" });
3815
4138
  return {
3816
4139
  isRepository: true,
3817
4140
  dirty: status.stdout.trim().length > 0,
@@ -3819,11 +4142,11 @@ function inspectGit(path) {
3819
4142
  };
3820
4143
  }
3821
4144
  function getExpectedPackageVersions() {
3822
- const proGovPackage = readJson2(findOwnPackageJson());
4145
+ const proGovPackage = readJson3(findOwnPackageJson());
3823
4146
  let docGovVersion;
3824
4147
  try {
3825
4148
  const require2 = createRequire2(import.meta.url);
3826
- const docGovPackage = readJson2(require2.resolve("@pieai/doc-gov/package.json"));
4149
+ const docGovPackage = readJson3(require2.resolve("@pieai/doc-gov/package.json"));
3827
4150
  docGovVersion = docGovPackage?.version;
3828
4151
  } catch {
3829
4152
  docGovVersion = void 0;
@@ -3836,16 +4159,16 @@ function getExpectedPackageVersions() {
3836
4159
  function findOwnPackageJson() {
3837
4160
  let current = dirname12(fileURLToPath3(import.meta.url));
3838
4161
  for (let depth = 0; depth < 5; depth += 1) {
3839
- const candidate = join20(current, "package.json");
3840
- if (existsSync19(candidate)) return candidate;
4162
+ const candidate = join23(current, "package.json");
4163
+ if (existsSync22(candidate)) return candidate;
3841
4164
  current = dirname12(current);
3842
4165
  }
3843
4166
  return "";
3844
4167
  }
3845
- function readJson2(path) {
3846
- if (!path || !existsSync19(path)) return void 0;
4168
+ function readJson3(path) {
4169
+ if (!path || !existsSync22(path)) return void 0;
3847
4170
  try {
3848
- return JSON.parse(readFileSync14(path, "utf8"));
4171
+ return JSON.parse(readFileSync16(path, "utf8"));
3849
4172
  } catch {
3850
4173
  return void 0;
3851
4174
  }
@@ -3864,27 +4187,31 @@ function deduplicateIssues(issues) {
3864
4187
  import { execFileSync } from "node:child_process";
3865
4188
  import {
3866
4189
  cpSync as cpSync2,
3867
- existsSync as existsSync20,
4190
+ existsSync as existsSync23,
3868
4191
  lstatSync as lstatSync8,
3869
4192
  mkdirSync as mkdirSync8,
3870
- readFileSync as readFileSync15,
3871
- readdirSync as readdirSync8,
4193
+ readFileSync as readFileSync17,
4194
+ readdirSync as readdirSync9,
3872
4195
  realpathSync as realpathSync4,
3873
- statSync as statSync4,
4196
+ statSync as statSync5,
3874
4197
  writeFileSync as writeFileSync7
3875
4198
  } 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";
4199
+ import { homedir as homedir4 } from "node:os";
4200
+ import { dirname as dirname13, join as join24, relative as relative8, resolve as resolve5, sep } from "node:path";
3878
4201
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3879
4202
  var CURRENT_ROUTER_VERSION = "1.1";
3880
4203
  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();
4204
+ const allEndpoints = collectEndpoints(options.manifest);
4205
+ const endpoints = options.targetId && options.targetId !== "all" ? allEndpoints.filter(({ endpoint }) => endpoint.id === options.targetId) : allEndpoints;
4206
+ if (options.targetId && options.targetId !== "all" && endpoints.length === 0) {
4207
+ throw new Error(`Unknown portfolio target: ${options.targetId}`);
4208
+ }
4209
+ const secretsRoot = options.secretsRoot ?? join24(dirname13(options.manifest.controlPlane?.path ?? allEndpoints[0]?.endpoint.path ?? process.cwd()), ".secrets");
4210
+ const homeDir = options.homeDir ?? process.env.HOME ?? homedir4();
3884
4211
  const grokVersion = commandVersion("grok");
3885
4212
  const executionEngineRoot = options.manifest.executionEngine?.path;
3886
4213
  const skillRegistry = inspectSkillRegistry(executionEngineRoot);
3887
- const expectedPackageVersion = packageVersion(join21(executionEngineRoot ?? "", "packages/pro-gov/package.json"));
4214
+ const expectedPackageVersion = packageVersion(join24(executionEngineRoot ?? "", "packages/pro-gov/package.json"));
3888
4215
  const repositories = endpoints.map(({ endpoint, role }) => inspectRepository(
3889
4216
  endpoint,
3890
4217
  role,
@@ -3897,20 +4224,27 @@ function inspectPortfolioAiHealth(options) {
3897
4224
  const summary = { healthy: 0, attention: 0, unhealthy: 0 };
3898
4225
  for (const repository of repositories) summary[repository.status] += 1;
3899
4226
  return {
3900
- schemaVersion: 4,
4227
+ schemaVersion: 5,
3901
4228
  portfolioId: options.manifest.portfolioId,
3902
4229
  generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4230
+ coverage: {
4231
+ mode: options.targetId && options.targetId !== "all" ? "single" : "all",
4232
+ targetId: options.targetId && options.targetId !== "all" ? options.targetId : void 0,
4233
+ coveredRepositoryIds: repositories.map((repository) => repository.id),
4234
+ totalRepositoryCount: allEndpoints.length
4235
+ },
3903
4236
  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
4237
  secretsRoot: inspectSecretsRoot(secretsRoot),
3905
4238
  hostEnvironment: inspectHostEnvironment(
3906
4239
  homeDir,
3907
4240
  grokVersion,
3908
- endpoints.map(({ endpoint }) => endpoint.path),
4241
+ allEndpoints.map(({ endpoint }) => endpoint.path),
3909
4242
  options.manifest.specialistChecks?.devspace
3910
4243
  ),
3911
4244
  skillRegistry,
3912
4245
  technologyGovernance: {
3913
4246
  strategySource: options.manifest.technologyGovernance?.strategySource,
4247
+ versionPolicy: options.manifest.technologyGovernance?.versionPolicy,
3914
4248
  projectTypes: options.manifest.technologyGovernance?.projectTypes.length ?? 0,
3915
4249
  technologies: options.manifest.technologyGovernance?.technologies.length ?? 0
3916
4250
  },
@@ -3918,19 +4252,44 @@ function inspectPortfolioAiHealth(options) {
3918
4252
  repositories
3919
4253
  };
3920
4254
  }
4255
+ function mergePortfolioAiHealthReport(existing, latest, allRepositoryIds) {
4256
+ const repositoriesById = /* @__PURE__ */ new Map();
4257
+ for (const repository of existing?.repositories ?? []) repositoriesById.set(repository.id, repository);
4258
+ for (const repository of latest.repositories) repositoriesById.set(repository.id, repository);
4259
+ const repositories = allRepositoryIds.map((id) => repositoriesById.get(id)).filter((repository) => repository !== void 0);
4260
+ const coveredIds = /* @__PURE__ */ new Set([
4261
+ ...existing?.coverage?.coveredRepositoryIds ?? [],
4262
+ ...latest.coverage.coveredRepositoryIds
4263
+ ]);
4264
+ const coveredRepositoryIds = allRepositoryIds.filter((id) => coveredIds.has(id));
4265
+ const summary = { healthy: 0, attention: 0, unhealthy: 0 };
4266
+ for (const repository of repositories) summary[repository.status] += 1;
4267
+ const complete = coveredRepositoryIds.length === allRepositoryIds.length;
4268
+ return {
4269
+ ...latest,
4270
+ repositories,
4271
+ summary,
4272
+ coverage: {
4273
+ mode: complete ? "all" : "single",
4274
+ targetId: complete ? void 0 : latest.coverage.targetId,
4275
+ coveredRepositoryIds,
4276
+ totalRepositoryCount: allRepositoryIds.length
4277
+ }
4278
+ };
4279
+ }
3921
4280
  function writePortfolioAiHealthReport(report, outDir) {
3922
4281
  mkdirSync8(outDir, { recursive: true });
3923
4282
  const dashboardAssets = findDashboardAssets();
3924
4283
  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));
4284
+ const source = join24(dashboardAssets, file);
4285
+ if (!existsSync23(source)) throw new Error(`Portfolio dashboard asset is missing: ${source}`);
4286
+ cpSync2(source, join24(outDir, file));
3928
4287
  }
3929
- const jsonPath = join21(outDir, "portfolio-ai-health.json");
3930
- const htmlPath = join21(outDir, "index.html");
4288
+ const jsonPath = join24(outDir, "portfolio-ai-health.json");
4289
+ const htmlPath = join24(outDir, "index.html");
3931
4290
  writeFileSync7(jsonPath, `${JSON.stringify(report, null, 2)}
3932
4291
  `);
3933
- writeFileSync7(join21(outDir, "data.js"), `window.__PORTFOLIO_AI_HEALTH__ = ${safeJavaScriptJson(report)};
4292
+ writeFileSync7(join24(outDir, "data.js"), `window.__PORTFOLIO_AI_HEALTH__ = ${safeJavaScriptJson(report)};
3934
4293
  `);
3935
4294
  return { jsonPath, htmlPath };
3936
4295
  }
@@ -3957,15 +4316,18 @@ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackage
3957
4316
  const hooks = inspectHooks(root);
3958
4317
  const docs = inspectDocs(root, role === "execution-engine" ? void 0 : expectedPackageVersion);
3959
4318
  const mcp = {
3960
- codexProject: tomlMcpNames(join21(root, ".codex/config.toml")),
3961
- claudeCodeProjectShared: jsonObjectKeys(join21(root, ".mcp.json"), "mcpServers"),
4319
+ codexProject: tomlMcpNames(join24(root, ".codex/config.toml")),
4320
+ claudeCodeProjectShared: jsonObjectKeys(join24(root, ".mcp.json"), "mcpServers"),
3962
4321
  claudeCodeProjectLocal: claudeProjectLocalMcpNames(homeDir, root),
3963
- grokProject: tomlMcpNames(join21(root, ".grok/config.toml")),
4322
+ grokProject: tomlMcpNames(join24(root, ".grok/config.toml")),
3964
4323
  grokEffective: grokInspection.effectiveMcp,
3965
4324
  grokInspection: grokInspection.inspection
3966
4325
  };
3967
4326
  const secrets = inspectRepositorySecrets(root, endpoint.id, secretsRoot, git.isRepository, endpoint.environmentPolicy);
3968
4327
  const projectModel = inspectProjectModel(root, endpoint, technologyGovernance);
4328
+ const versions = inspectVersionPolicy(root, technologyGovernance?.versionPolicy, endpoint.projectType);
4329
+ const verification = inspectProjectVerification(root);
4330
+ const redundancy = inspectProjectRedundancy(root, { homeDir });
3969
4331
  const recommendations = [];
3970
4332
  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
4333
  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 +4364,15 @@ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackage
4002
4364
  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
4365
  const missingSelected = projectModel.optionalCapabilities.filter((technology) => technology.selected && !technology.detected);
4004
4366
  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`);
4367
+ 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`);
4368
+ 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`);
4369
+ 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
4370
  return {
4006
4371
  id: endpoint.id,
4007
4372
  role,
4008
4373
  path: root,
4009
4374
  profile: "profile" in endpoint ? endpoint.profile : void 0,
4010
- status: deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, Boolean(technologyGovernance)),
4375
+ status: deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, Boolean(technologyGovernance), versions, verification, redundancy),
4011
4376
  recommendations,
4012
4377
  git,
4013
4378
  entries,
@@ -4017,10 +4382,13 @@ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackage
4017
4382
  hostSsot,
4018
4383
  secrets,
4019
4384
  docs,
4020
- projectModel
4385
+ projectModel,
4386
+ versions,
4387
+ verification,
4388
+ redundancy
4021
4389
  };
4022
4390
  }
4023
- function deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, technologyGovernanceConfigured) {
4391
+ function deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, technologyGovernanceConfigured, versions, verification, redundancy) {
4024
4392
  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
4393
  const missingBaseline = projectModel.baseline.some((technology) => !technology.detected && !hasBaselineException(projectModel, technology.id));
4026
4394
  const missingSelected = projectModel.optionalCapabilities.some((technology) => technology.selected && !technology.detected);
@@ -4029,7 +4397,7 @@ function deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs
4029
4397
  const packageVersionNeedsReview = docs.packages.expected !== void 0 && !docs.packages.aligned;
4030
4398
  const routerVersionNeedsReview = !docs.routerAligned;
4031
4399
  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";
4400
+ 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
4401
  return "healthy";
4034
4402
  }
4035
4403
  function inspectProjectModel(root, endpoint, governance) {
@@ -4039,7 +4407,7 @@ function inspectProjectModel(root, endpoint, governance) {
4039
4407
  const detection = (id) => {
4040
4408
  const technology = technologyById.get(id);
4041
4409
  const packageMatch = technology?.packages?.some((name) => packages.has(name)) ?? false;
4042
- const fileMatch = technology?.files?.some((path) => existsSync20(join21(root, path))) ?? false;
4410
+ const fileMatch = technology?.files?.some((path) => existsSync23(join24(root, path))) ?? false;
4043
4411
  return { id, label: technology?.label ?? id, detected: packageMatch || fileMatch };
4044
4412
  };
4045
4413
  const selected = new Set(endpoint.capabilities ?? []);
@@ -4062,10 +4430,10 @@ function collectPackageNames(root) {
4062
4430
  try {
4063
4431
  files = splitLines(execFileSync("git", ["ls-files", "*package.json"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }));
4064
4432
  } catch {
4065
- if (existsSync20(join21(root, "package.json"))) files = ["package.json"];
4433
+ if (existsSync23(join24(root, "package.json"))) files = ["package.json"];
4066
4434
  }
4067
4435
  for (const file of files) {
4068
- const packageJson = readJson3(join21(root, file));
4436
+ const packageJson = readJson4(join24(root, file));
4069
4437
  if (!isRecord3(packageJson)) continue;
4070
4438
  for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
4071
4439
  const dependencies = packageJson[section];
@@ -4103,9 +4471,9 @@ function inspectGit2(root) {
4103
4471
  };
4104
4472
  }
4105
4473
  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");
4474
+ const agentsPath = join24(root, "AGENTS.md");
4475
+ const agents = !existsSync23(agentsPath) ? "missing" : safeRead(agentsPath).includes("PGS-ROUTER:BEGIN") ? "pgs-router" : "custom";
4476
+ const claudePath = join24(root, "CLAUDE.md");
4109
4477
  let claude = "missing";
4110
4478
  if (pathLexists(claudePath)) {
4111
4479
  const info = lstatSync8(claudePath);
@@ -4123,7 +4491,7 @@ function inspectEntries(root) {
4123
4491
  return { agents, claude, gemini: inspectOptionalEntry(root, "GEMINI.md", agentsPath) };
4124
4492
  }
4125
4493
  function inspectOptionalEntry(root, filename, agentsPath) {
4126
- const path = join21(root, filename);
4494
+ const path = join24(root, filename);
4127
4495
  if (!pathLexists(path)) return "missing";
4128
4496
  const info = lstatSync8(path);
4129
4497
  if (info.isSymbolicLink()) {
@@ -4137,7 +4505,7 @@ function inspectOptionalEntry(root, filename, agentsPath) {
4137
4505
  return /AGENTS\.md/.test(content) && content.length < 2e3 ? "thin-adapter" : "custom";
4138
4506
  }
4139
4507
  function inspectSkills(root, grokInspection) {
4140
- const lock = readJson3(join21(root, ".pro-gov/assets.lock.json"));
4508
+ const lock = readJson4(join24(root, ".pro-gov/assets.lock.json"));
4141
4509
  const managed = /* @__PURE__ */ new Set();
4142
4510
  const bundleIds = stringArray(isRecord3(lock) ? lock.bundleIds : void 0);
4143
4511
  if (isRecord3(lock) && Array.isArray(lock.assets)) {
@@ -4147,9 +4515,9 @@ function inspectSkills(root, grokInspection) {
4147
4515
  if (match) managed.add(match[1]);
4148
4516
  }
4149
4517
  }
4150
- const skillRoot = join21(root, ".agents/skills");
4518
+ const skillRoot = join24(root, ".agents/skills");
4151
4519
  const canonical = pathLexists(skillRoot) && safeIsDirectory(skillRoot) ? safeReadDir(skillRoot).filter((name) => !name.startsWith(".")).map((name) => {
4152
- const path = join21(skillRoot, name);
4520
+ const path = join24(skillRoot, name);
4153
4521
  const stat = lstatSync8(path);
4154
4522
  let kind = stat.isSymbolicLink() ? "symlink" : stat.isDirectory() ? "directory" : "file";
4155
4523
  if (stat.isSymbolicLink()) {
@@ -4175,20 +4543,20 @@ function inspectSkills(root, grokInspection) {
4175
4543
  },
4176
4544
  hosts: {
4177
4545
  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,
4546
+ claudeCodeProject: inspectSkillRoot(join24(root, ".claude/skills")).names.length,
4547
+ grokNativeProject: inspectSkillRoot(join24(root, ".grok/skills")).names.length,
4180
4548
  grokEffective: grokInspection.skills
4181
4549
  }
4182
4550
  };
4183
4551
  }
4184
4552
  function inspectClaudeSkillRoot(root) {
4185
- const path = join21(root, ".claude/skills");
4553
+ const path = join24(root, ".claude/skills");
4186
4554
  if (!pathLexists(path)) return "missing";
4187
4555
  const stat = lstatSync8(path);
4188
4556
  if (stat.isSymbolicLink()) {
4189
4557
  try {
4190
4558
  const target = realpathSync4(path);
4191
- return target === realpathSync4(join21(root, ".agents/skills")) ? "shared-root" : "other";
4559
+ return target === realpathSync4(join24(root, ".agents/skills")) ? "shared-root" : "other";
4192
4560
  } catch {
4193
4561
  return "dangling-symlink";
4194
4562
  }
@@ -4196,7 +4564,7 @@ function inspectClaudeSkillRoot(root) {
4196
4564
  return stat.isDirectory() ? "duplicate-directory" : "other";
4197
4565
  }
4198
4566
  function inspectRepositorySecrets(root, id, secretsRoot, isRepository, environmentPolicy) {
4199
- const centralPath = join21(secretsRoot, id);
4567
+ const centralPath = join24(secretsRoot, id);
4200
4568
  const centralRealPath = safeRealpath(centralPath);
4201
4569
  const localOnlyReasons = new Map(
4202
4570
  (environmentPolicy?.localOnly ?? []).map((entry) => [entry.path, entry.reason])
@@ -4208,16 +4576,16 @@ function inspectRepositorySecrets(root, id, secretsRoot, isRepository, environme
4208
4576
  tracked: isRepository ? gitTracks(root, path) : false,
4209
4577
  template: isEnvironmentTemplate(path),
4210
4578
  fixture: isEnvironmentFixture(path),
4211
- symlink: lstatSync8(join21(root, path)).isSymbolicLink(),
4212
- centralized: pointsInside(join21(root, path), centralRealPath),
4579
+ symlink: lstatSync8(join24(root, path)).isSymbolicLink(),
4580
+ centralized: pointsInside(join24(root, path), centralRealPath),
4213
4581
  localOnly: localOnlyReason !== void 0,
4214
4582
  ...localOnlyReason !== void 0 ? { localOnlyReason } : {}
4215
4583
  };
4216
4584
  });
4217
4585
  return {
4218
- centralDirectory: existsSync20(centralPath) ? "present" : "absent",
4219
- centralMode: existsSync20(centralPath) ? modeString(statSync4(centralPath).mode) : void 0,
4220
- centralFiles: existsSync20(centralPath) ? collectCentralSecretFiles(centralPath) : [],
4586
+ centralDirectory: existsSync23(centralPath) ? "present" : "absent",
4587
+ centralMode: existsSync23(centralPath) ? modeString(statSync5(centralPath).mode) : void 0,
4588
+ centralFiles: existsSync23(centralPath) ? collectCentralSecretFiles(centralPath) : [],
4221
4589
  repositoryEnvFiles: envFiles
4222
4590
  };
4223
4591
  }
@@ -4226,11 +4594,11 @@ function collectEnvironmentFiles(root, current = root, depth = 0) {
4226
4594
  if (depth > 5) return [];
4227
4595
  const found = [];
4228
4596
  try {
4229
- for (const entry of readdirSync8(current, { withFileTypes: true })) {
4597
+ for (const entry of readdirSync9(current, { withFileTypes: true })) {
4230
4598
  if (entry.isDirectory()) {
4231
- if (!SKIP_ENV_DIRECTORIES.has(entry.name)) found.push(...collectEnvironmentFiles(root, join21(current, entry.name), depth + 1));
4599
+ if (!SKIP_ENV_DIRECTORIES.has(entry.name)) found.push(...collectEnvironmentFiles(root, join24(current, entry.name), depth + 1));
4232
4600
  } else if (isEnvironmentFilename(entry.name) && !isProviderGeneratedEnvironmentFile(entry.name)) {
4233
- found.push(relative8(root, join21(current, entry.name)));
4601
+ found.push(relative8(root, join24(current, entry.name)));
4234
4602
  }
4235
4603
  }
4236
4604
  } catch {
@@ -4242,8 +4610,8 @@ function collectCentralSecretFiles(root, current = root, depth = 0) {
4242
4610
  if (depth > 3) return [];
4243
4611
  const found = [];
4244
4612
  try {
4245
- for (const entry of readdirSync8(current, { withFileTypes: true })) {
4246
- const path = join21(current, entry.name);
4613
+ for (const entry of readdirSync9(current, { withFileTypes: true })) {
4614
+ const path = join24(current, entry.name);
4247
4615
  if (entry.isDirectory()) found.push(...collectCentralSecretFiles(root, path, depth + 1));
4248
4616
  else found.push({ path: relative8(root, path), mode: modeString(lstatSync8(path).mode) });
4249
4617
  }
@@ -4286,12 +4654,12 @@ function hasUnsafeCentralSecretPermissions(secrets) {
4286
4654
  return secrets.centralDirectory === "present" && (secrets.centralMode !== "700" || secrets.centralFiles.some((file) => file.mode !== "600"));
4287
4655
  }
4288
4656
  function inspectSecretsRoot(path) {
4289
- return existsSync20(path) ? { path, exists: true, mode: modeString(statSync4(path).mode) } : { path, exists: false };
4657
+ return existsSync23(path) ? { path, exists: true, mode: modeString(statSync5(path).mode) } : { path, exists: false };
4290
4658
  }
4291
4659
  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");
4660
+ const codexConfig = join24(homeDir, ".codex/config.toml");
4661
+ const claudeConfig = join24(homeDir, ".claude.json");
4662
+ const grokConfig = join24(homeDir, ".grok/config.toml");
4295
4663
  const hostEnvironment = {
4296
4664
  mcp: {
4297
4665
  codexUser: { path: codexConfig, names: tomlMcpNames(codexConfig) },
@@ -4299,11 +4667,11 @@ function inspectHostEnvironment(homeDir, grokVersion, repositoryPaths, devspaceS
4299
4667
  grokUser: { path: grokConfig, names: tomlMcpNames(grokConfig) }
4300
4668
  },
4301
4669
  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")),
4670
+ codexUser: inspectSkillRoot(join24(homeDir, ".agents/skills")),
4671
+ claudeCodeUser: inspectSkillRoot(join24(homeDir, ".claude/skills")),
4672
+ grokUser: inspectSkillRoot(join24(homeDir, ".grok/skills")),
4673
+ grokAgentsCompatibility: inspectSkillRoot(join24(homeDir, ".agents/skills")),
4674
+ grokClaudeCompatibility: inspectSkillRoot(join24(homeDir, ".claude/skills")),
4307
4675
  ssot: inspectUserSkillsSsot(homeDir)
4308
4676
  },
4309
4677
  grok: {
@@ -4323,9 +4691,9 @@ function inspectHostEnvironment(homeDir, grokVersion, repositoryPaths, devspaceS
4323
4691
  }
4324
4692
  function inspectDevSpaceHealth(options) {
4325
4693
  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");
4694
+ const configDirectory = join24(options.homeDir, ".devspace");
4695
+ const configPath = join24(configDirectory, "config.json");
4696
+ const authPath = join24(configDirectory, "auth.json");
4329
4697
  const installedResult = run("devspace", ["--version"], 3e3);
4330
4698
  const installedVersion = installedResult.ok ? installedResult.stdout.match(/\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\b/)?.[0] : void 0;
4331
4699
  const latestResult = run("npm", ["view", "@waishnav/devspace", "version", "--registry=https://registry.npmjs.org/"], 5e3);
@@ -4335,14 +4703,14 @@ function inspectDevSpaceHealth(options) {
4335
4703
  const processEnvironment = pid ? run("ps", ["eww", "-p", pid, "-o", "command="], 3e3) : void 0;
4336
4704
  const processToolMode = processEnvironment?.ok ? processEnvironment.stdout.match(/(?:^|\s)DEVSPACE_TOOL_MODE=(minimal|full|codex)(?:\s|$)/)?.[1] : void 0;
4337
4705
  const doctor = !installedResult.ok ? "unavailable" : run("devspace", ["doctor"], 1e4).ok ? "ok" : "failed";
4338
- const configValue = readJson3(configPath);
4706
+ const configValue = readJson4(configPath);
4339
4707
  const configExists = isRecord3(configValue);
4340
4708
  const allowedRoots = configExists && Array.isArray(configValue.allowedRoots) ? configValue.allowedRoots.filter((value) => typeof value === "string") : [];
4341
4709
  const portfolioCoverage = !configExists || allowedRoots.length === 0 ? "unknown" : options.repositoryPaths.every((repositoryPath) => allowedRoots.some((root) => isPathInside(repositoryPath, root))) ? "complete" : "partial";
4342
4710
  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;
4711
+ const directoryMode = existsSync23(configDirectory) ? modeString(statSync5(configDirectory).mode) : void 0;
4712
+ const fileMode = existsSync23(configPath) ? modeString(statSync5(configPath).mode) : void 0;
4713
+ const authMode = existsSync23(authPath) ? modeString(statSync5(authPath).mode) : void 0;
4346
4714
  const update = installedVersion && latestVersion ? installedVersion === latestVersion ? "current" : "available" : "unknown";
4347
4715
  const recommendations = [];
4348
4716
  let status = "healthy";
@@ -4356,7 +4724,7 @@ function inspectDevSpaceHealth(options) {
4356
4724
  };
4357
4725
  if (!installedResult.ok) unhealthy("\u672C\u673A\u672A\u53D1\u73B0 DevSpace\uFF1B\u65E0\u6CD5\u4F7F\u7528\u5BBF\u4E3B\u5DE5\u4F5C\u533A\u670D\u52A1\u3002");
4358
4726
  if (!configExists) unhealthy("\u7F3A\u5C11 ~/.devspace/config.json\u3002");
4359
- if (!existsSync20(authPath)) unhealthy("\u7F3A\u5C11 ~/.devspace/auth.json\u3002");
4727
+ if (!existsSync23(authPath)) unhealthy("\u7F3A\u5C11 ~/.devspace/auth.json\u3002");
4360
4728
  if (directoryMode && directoryMode !== "700") unhealthy(`~/.devspace \u76EE\u5F55\u6743\u9650\u4E3A ${directoryMode}\uFF0C\u5E94\u6536\u7D27\u4E3A 700\u3002`);
4361
4729
  if (fileMode && fileMode !== "600") unhealthy(`DevSpace \u914D\u7F6E\u6587\u4EF6\u6743\u9650\u4E3A ${fileMode}\uFF0C\u5E94\u4E3A 600\u3002`);
4362
4730
  if (authMode && authMode !== "600") unhealthy(`DevSpace \u8BA4\u8BC1\u6587\u4EF6\u6743\u9650\u4E3A ${authMode}\uFF0C\u5E94\u4E3A 600\u3002`);
@@ -4382,7 +4750,7 @@ function inspectDevSpaceHealth(options) {
4382
4750
  exists: configExists,
4383
4751
  ...directoryMode ? { directoryMode } : {},
4384
4752
  ...fileMode ? { fileMode } : {},
4385
- authExists: existsSync20(authPath),
4753
+ authExists: existsSync23(authPath),
4386
4754
  ...authMode ? { authMode } : {},
4387
4755
  bind,
4388
4756
  portValid: configExists && typeof configValue.port === "number" && Number.isInteger(configValue.port) && configValue.port > 0 && configValue.port <= 65535,
@@ -4416,12 +4784,12 @@ function isPathInside(path, root) {
4416
4784
  }
4417
4785
  function inspectSkillRoot(path) {
4418
4786
  const exists = pathLexists(path) && safeIsDirectory(path);
4419
- const names = exists ? safeReadDir(path).filter((name) => !name.startsWith(".") && existsSync20(join21(path, name, "SKILL.md"))) : [];
4787
+ const names = exists ? safeReadDir(path).filter((name) => !name.startsWith(".") && existsSync23(join24(path, name, "SKILL.md"))) : [];
4420
4788
  return { path, exists, names };
4421
4789
  }
4422
4790
  function claudeProjectLocalMcpNames(homeDir, root) {
4423
4791
  if (!homeDir) return [];
4424
- const value = readJson3(join21(homeDir, ".claude.json"));
4792
+ const value = readJson4(join24(homeDir, ".claude.json"));
4425
4793
  if (!isRecord3(value) || !isRecord3(value.projects)) return [];
4426
4794
  const candidates = new Set([resolve5(root), safeRealpath(root)].filter((path) => Boolean(path)));
4427
4795
  const names = /* @__PURE__ */ new Set();
@@ -4457,13 +4825,13 @@ function inspectGrokProject(root, homeDir, grokVersion) {
4457
4825
  const value = JSON.parse(execFileSync("grok", ["inspect", "--json"], {
4458
4826
  cwd: root,
4459
4827
  encoding: "utf8",
4460
- env: { ...process.env, HOME: homeDir, GROK_HOME: join21(homeDir, ".grok") },
4828
+ env: { ...process.env, HOME: homeDir, GROK_HOME: join24(homeDir, ".grok") },
4461
4829
  maxBuffer: 10 * 1024 * 1024,
4462
4830
  stdio: ["ignore", "pipe", "ignore"],
4463
4831
  timeout: 8e3
4464
4832
  }));
4465
4833
  if (!isRecord3(value)) return empty("failed");
4466
- const userClaudeNames = new Set(jsonObjectKeys(join21(homeDir, ".claude.json"), "mcpServers"));
4834
+ const userClaudeNames = new Set(jsonObjectKeys(join24(homeDir, ".claude.json"), "mcpServers"));
4467
4835
  const localClaudeNames = new Set(claudeProjectLocalMcpNames(homeDir, root));
4468
4836
  const effectiveMcp = Array.isArray(value.mcpServers) ? value.mcpServers.flatMap((item) => {
4469
4837
  if (!isRecord3(item) || typeof item.name !== "string") return [];
@@ -4521,9 +4889,9 @@ function inferGrokMcpScope(name, sourceType, sourcePath, root, homeDir, userClau
4521
4889
  }
4522
4890
  const resolvedSource = safeRealpath(sourcePath) ?? resolve5(sourcePath);
4523
4891
  const resolvedRoot = safeRealpath(root) ?? resolve5(root);
4524
- if (resolvedSource === join21(resolvedRoot, ".mcp.json")) return "project-shared";
4892
+ if (resolvedSource === join24(resolvedRoot, ".mcp.json")) return "project-shared";
4525
4893
  if (resolvedSource.startsWith(resolvedRoot + sep)) return "project";
4526
- if (homeDir && resolvedSource === join21(resolve5(homeDir), ".claude.json")) {
4894
+ if (homeDir && resolvedSource === join24(resolve5(homeDir), ".claude.json")) {
4527
4895
  if (localClaudeNames.has(name)) return "project-local";
4528
4896
  if (userClaudeNames.has(name)) return "user";
4529
4897
  }
@@ -4558,7 +4926,7 @@ function inspectHooks(root) {
4558
4926
  { host: "codex", path: ".codex/hooks.json" }
4559
4927
  ];
4560
4928
  return configs.map((config) => {
4561
- const value = readJson3(join21(root, config.path));
4929
+ const value = readJson4(join24(root, config.path));
4562
4930
  const counts = /* @__PURE__ */ new Map();
4563
4931
  collectHookEvents(value, counts);
4564
4932
  return {
@@ -4579,18 +4947,18 @@ function collectHookEvents(value, counts) {
4579
4947
  }
4580
4948
  }
4581
4949
  function inspectDocs(root, expected) {
4582
- const packageJson = readJson3(join21(root, "package.json"));
4950
+ const packageJson = readJson4(join24(root, "package.json"));
4583
4951
  const dependencies = isRecord3(packageJson) ? { ...recordOrEmpty(packageJson.dependencies), ...recordOrEmpty(packageJson.devDependencies) } : {};
4584
4952
  const docGov = dependencyVersion(dependencies["@pieai/doc-gov"]);
4585
4953
  const proGov = dependencyVersion(dependencies["@pieai/pro-gov"]);
4586
- const routerMatch = safeRead(join21(root, "AGENTS.md")).match(/PGS-ROUTER:BEGIN\s+v([0-9.]+)/);
4954
+ const routerMatch = safeRead(join24(root, "AGENTS.md")).match(/PGS-ROUTER:BEGIN\s+v([0-9.]+)/);
4587
4955
  const declared = [docGov, proGov].filter((value) => Boolean(value));
4588
4956
  return {
4589
4957
  routerVersion: routerMatch?.[1],
4590
4958
  expectedRouterVersion: CURRENT_ROUTER_VERSION,
4591
4959
  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")),
4960
+ manifest: existsSync23(join24(root, "docs/governance/MANIFEST.yml")),
4961
+ currentWork: existsSync23(join24(root, "docs/reference/execution/current-work.md")),
4594
4962
  packages: {
4595
4963
  expected,
4596
4964
  docGov,
@@ -4605,7 +4973,7 @@ function dependencyVersion(value) {
4605
4973
  return match?.[1];
4606
4974
  }
4607
4975
  function packageVersion(path) {
4608
- const value = readJson3(path);
4976
+ const value = readJson4(path);
4609
4977
  return isRecord3(value) && typeof value.version === "string" ? value.version : void 0;
4610
4978
  }
4611
4979
  function recordOrEmpty(value) {
@@ -4613,20 +4981,20 @@ function recordOrEmpty(value) {
4613
4981
  }
4614
4982
  function inspectSkillRegistry(executionEngineRoot) {
4615
4983
  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"));
4984
+ const agentAssetsRoot = join24(executionEngineRoot, "agent-assets");
4985
+ const registry = readJson4(join24(agentAssetsRoot, "registry.json"));
4618
4986
  const assets = isRecord3(registry) && Array.isArray(registry.assets) ? registry.assets : [];
4619
4987
  const registeredSkills = assets.filter((asset) => isRecord3(asset) && asset.kind === "skill");
4620
- const bundleRoot = join21(agentAssetsRoot, "bundles");
4988
+ const bundleRoot = join24(agentAssetsRoot, "bundles");
4621
4989
  const bundleFiles = safeReadDir(bundleRoot).filter((file) => file.endsWith(".json"));
4622
4990
  const bundledIds = /* @__PURE__ */ new Set();
4623
4991
  for (const file of bundleFiles) {
4624
- const bundle = readJson3(join21(bundleRoot, file));
4992
+ const bundle = readJson4(join24(bundleRoot, file));
4625
4993
  if (!isRecord3(bundle) || !Array.isArray(bundle.assets)) continue;
4626
4994
  for (const id of bundle.assets) if (typeof id === "string") bundledIds.add(id);
4627
4995
  }
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);
4996
+ const sourceRoots = [join24(agentAssetsRoot, "skills/pie-skills"), join24(agentAssetsRoot, "skills/npx-skills/.agents/skills")];
4997
+ const source = sourceRoots.reduce((count, root) => count + safeReadDir(root).filter((name) => existsSync23(join24(root, name, "SKILL.md"))).length, 0);
4630
4998
  return {
4631
4999
  source,
4632
5000
  registered: registeredSkills.length,
@@ -4635,12 +5003,12 @@ function inspectSkillRegistry(executionEngineRoot) {
4635
5003
  };
4636
5004
  }
4637
5005
  function jsonObjectKeys(path, key) {
4638
- const value = readJson3(path);
5006
+ const value = readJson4(path);
4639
5007
  if (!isRecord3(value) || !isRecord3(value[key])) return [];
4640
5008
  return Object.keys(value[key]).sort();
4641
5009
  }
4642
5010
  function tomlMcpNames(path) {
4643
- if (!existsSync20(path)) return [];
5011
+ if (!existsSync23(path)) return [];
4644
5012
  const names = /* @__PURE__ */ new Set();
4645
5013
  for (const line of safeRead(path).split(/\r?\n/)) {
4646
5014
  const match = line.match(/^\s*\[mcp_servers\.(?:"([^"]+)"|([^\.\]]+))\]\s*$/);
@@ -4649,30 +5017,30 @@ function tomlMcpNames(path) {
4649
5017
  }
4650
5018
  return [...names].sort();
4651
5019
  }
4652
- function readJson3(path) {
5020
+ function readJson4(path) {
4653
5021
  try {
4654
- return JSON.parse(readFileSync15(path, "utf8"));
5022
+ return JSON.parse(readFileSync17(path, "utf8"));
4655
5023
  } catch {
4656
5024
  return void 0;
4657
5025
  }
4658
5026
  }
4659
5027
  function safeRead(path) {
4660
5028
  try {
4661
- return readFileSync15(path, "utf8");
5029
+ return readFileSync17(path, "utf8");
4662
5030
  } catch {
4663
5031
  return "";
4664
5032
  }
4665
5033
  }
4666
5034
  function safeReadDir(path) {
4667
5035
  try {
4668
- return readdirSync8(path).sort();
5036
+ return readdirSync9(path).sort();
4669
5037
  } catch {
4670
5038
  return [];
4671
5039
  }
4672
5040
  }
4673
5041
  function safeIsDirectory(path) {
4674
5042
  try {
4675
- return statSync4(path).isDirectory();
5043
+ return statSync5(path).isDirectory();
4676
5044
  } catch {
4677
5045
  return false;
4678
5046
  }
@@ -4709,14 +5077,14 @@ function findDashboardAssets() {
4709
5077
  const packageRoot2 = dirname13(dirname13(fileURLToPath4(import.meta.url)));
4710
5078
  const candidates = [
4711
5079
  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")
5080
+ join24(packageRoot2, ".dashboard-build"),
5081
+ join24(packageRoot2, "assets/portfolio-dashboard"),
5082
+ join24(process.cwd(), ".dashboard-build"),
5083
+ join24(process.cwd(), "assets/portfolio-dashboard"),
5084
+ join24(process.cwd(), "packages/pro-gov/.dashboard-build"),
5085
+ join24(process.cwd(), "packages/pro-gov/assets/portfolio-dashboard")
4718
5086
  ].filter((value) => Boolean(value));
4719
- const match = candidates.find((path) => existsSync20(join21(path, "index.html")));
5087
+ const match = candidates.find((path) => existsSync23(join24(path, "index.html")));
4720
5088
  if (!match) throw new Error("Portfolio dashboard assets were not built. Run pnpm --filter @pieai/pro-gov build.");
4721
5089
  return match;
4722
5090
  }
@@ -4752,10 +5120,23 @@ function runPortfolioAiHealth(args) {
4752
5120
  for (const issue of loaded.issues) console.error(`${issue.type}: ${issue.message}`);
4753
5121
  return 1;
4754
5122
  }
4755
- const report = inspectPortfolioAiHealth({
5123
+ const targetId = options.value.targetId && options.value.targetId !== "all" ? options.value.targetId : void 0;
5124
+ if (targetId && !loaded.manifest.targets.some((target) => target.id === targetId)) {
5125
+ console.error(`Unknown portfolio target: ${targetId}`);
5126
+ return 1;
5127
+ }
5128
+ const latest = inspectPortfolioAiHealth({
4756
5129
  manifest: loaded.manifest,
4757
- secretsRoot: options.value.secretsRoot
5130
+ secretsRoot: options.value.secretsRoot,
5131
+ targetId
4758
5132
  });
5133
+ const allRepositoryIds = [
5134
+ loaded.manifest.controlPlane?.id,
5135
+ loaded.manifest.executionEngine?.id,
5136
+ ...loaded.manifest.targets.map((target) => target.id)
5137
+ ].filter((id) => Boolean(id));
5138
+ const existing = targetId ? readExistingAiHealthReport(options.value.outDir, loaded.manifest.portfolioId) : void 0;
5139
+ const report = targetId ? mergePortfolioAiHealthReport(existing, latest, allRepositoryIds) : latest;
4759
5140
  const written = writePortfolioAiHealthReport(report, options.value.outDir);
4760
5141
  if (options.value.json) {
4761
5142
  console.log(JSON.stringify({ ok: true, ...written, summary: report.summary }, null, 2));
@@ -4814,6 +5195,12 @@ function runPortfolioDoctor(args) {
4814
5195
  for (const warning of target.hostSsot.issues) {
4815
5196
  console.log(`${target.id} host-ssot-warning: ${warning}`);
4816
5197
  }
5198
+ if (target.verification.status === "attention") {
5199
+ console.log(`${target.id} verification-warning: missing ${target.verification.missing.join(", ")}`);
5200
+ }
5201
+ if (target.versions.status === "attention") {
5202
+ console.log(`${target.id} version-policy-warning: ${target.versions.attentionCount} drift(s)`);
5203
+ }
4817
5204
  }
4818
5205
  if (result.ok) {
4819
5206
  console.log(`portfolio doctor passed (${targets.length} targets)`);
@@ -5078,8 +5465,8 @@ function isHost2(value) {
5078
5465
  return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
5079
5466
  }
5080
5467
  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;
5468
+ const agentAssetsDir = manifest?.executionEngine?.path ? join25(manifest.executionEngine.path, "agent-assets") : void 0;
5469
+ return agentAssetsDir && existsSync24(join25(agentAssetsDir, "registry.json")) ? agentAssetsDir : void 0;
5083
5470
  }
5084
5471
  function printUsage4() {
5085
5472
  console.error("Usage:");
@@ -5087,12 +5474,24 @@ function printUsage4() {
5087
5474
  console.error(" pro-gov portfolio plan --config <path> [--target <id|all>] [--host codex|claude-code|gemini-cli|antigravity] [--json]");
5088
5475
  console.error(" pro-gov portfolio assets-check --config <path> [--target <id|all>] [--json]");
5089
5476
  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]");
5477
+ console.error(" pro-gov portfolio ai-health --config <path> --out <directory> [--target <id|all>] [--secrets-root <directory>] [--json]");
5478
+ }
5479
+ function readExistingAiHealthReport(outDir, portfolioId) {
5480
+ if (!outDir) return void 0;
5481
+ const path = join25(outDir, "portfolio-ai-health.json");
5482
+ if (!existsSync24(path)) return void 0;
5483
+ try {
5484
+ const value = JSON.parse(readFileSync18(path, "utf8"));
5485
+ if (!value || typeof value !== "object" || value.portfolioId !== portfolioId || !Array.isArray(value.repositories)) return void 0;
5486
+ return value;
5487
+ } catch {
5488
+ return void 0;
5489
+ }
5091
5490
  }
5092
5491
 
5093
5492
  // 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";
5493
+ import { existsSync as existsSync25, lstatSync as lstatSync9, readFileSync as readFileSync19, readlinkSync as readlinkSync4 } from "node:fs";
5494
+ import { join as join26 } from "node:path";
5096
5495
  function runSync(args) {
5097
5496
  const check = args.includes("--check");
5098
5497
  if (!check) {
@@ -5120,7 +5519,7 @@ function runSync(args) {
5120
5519
  console.log("pro-gov sync check");
5121
5520
  console.log(`profile: ${profile}`);
5122
5521
  for (const file of planStarterFiles(profile)) {
5123
- const targetPath = join23(process.cwd(), file.targetPath);
5522
+ const targetPath = join26(process.cwd(), file.targetPath);
5124
5523
  const stat = safeLstat3(targetPath);
5125
5524
  if (!stat) {
5126
5525
  if (file.ownership === "optional-guardrail") continue;
@@ -5144,8 +5543,8 @@ function runSync(args) {
5144
5543
  }
5145
5544
  continue;
5146
5545
  }
5147
- const source = readFileSync16(file.absoluteSourcePath, "utf8");
5148
- const target = readFileSync16(targetPath, "utf8");
5546
+ const source = readFileSync19(file.absoluteSourcePath, "utf8");
5547
+ const target = readFileSync19(targetPath, "utf8");
5149
5548
  if (!matchesExpectedContent(file.targetPath, source, target)) {
5150
5549
  console.log(`different: ${file.targetPath}`);
5151
5550
  differences += 1;
@@ -5179,7 +5578,7 @@ function normalizeMarkdownTableCell(cell) {
5179
5578
  }
5180
5579
  function inferInstalledProfile(root) {
5181
5580
  const installed = ["engineering-runtime", "doc-only"].filter(
5182
- (profile) => existsSync22(join23(root, `docs/governance/agents-routing/${profile}-v1.1.md`))
5581
+ (profile) => existsSync25(join26(root, `docs/governance/agents-routing/${profile}-v1.1.md`))
5183
5582
  );
5184
5583
  return installed.length === 1 ? installed[0] : void 0;
5185
5584
  }