@pieai/pro-gov 0.9.0 → 0.9.2

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.
@@ -25,11 +25,11 @@
25
25
  "notes": "Public rule for choosing evidence lanes and requiring fresh verification before completion.",
26
26
  "promotion": {
27
27
  "privateSourcePath": "rules/pie-rules/ai-in-the-loop.md",
28
- "privateSourceHash": "sha256:71f487152123491a2c110ad839a90780b08e04ae3a133d59bbe977bf8b2f64e4",
28
+ "privateSourceHash": "sha256:344eba8c3b1f3130ed33e4b6a4511a332e2ca477e6a40ad7d4c73006197f5445",
29
29
  "publicHash": "sha256:911702baf6ea1d724e41ea1876d23d80f4de8dfaaadbf1399a40c982c810224c",
30
30
  "sanitized": true,
31
- "lastReviewed": "2026-08-17",
32
- "reviewNotes": "Reviewed after the private source gained governed front matter and normalized formatting. The public copy intentionally keeps the portable evidence-loop method without the private Grok compatibility lane or project governance metadata."
31
+ "lastReviewed": "2026-08-20",
32
+ "reviewNotes": "Reviewed after the private source gained governed front matter and normalized formatting. The public copy intentionally keeps the portable evidence-loop method without the private Grok compatibility lane or project governance metadata. Re-reviewed 2026-08-20: the private source moved by Markdown table padding only, with no wording change, so the sanitized public copy is unaffected and only the private hash is re-pinned."
33
33
  }
34
34
  },
35
35
  {
@@ -43,7 +43,7 @@ jobs:
43
43
 
44
44
  - uses: pnpm/action-setup@v6
45
45
  with:
46
- version: 11.9.0
46
+ version: 11.22.0
47
47
 
48
48
  - uses: actions/setup-node@v6
49
49
  with:
@@ -0,0 +1,27 @@
1
+ # Seeded by `pro-gov init`. This file is project-local after seeding — add
2
+ # your own build outputs and generated paths freely.
3
+
4
+ # Dependencies and build output
5
+ node_modules
6
+ dist
7
+ build
8
+ coverage
9
+ out
10
+ .next
11
+ .turbo
12
+ .tmp
13
+
14
+ # ─────────────────────────────────────────────────────────────────────────
15
+ # PGS-managed content — DO NOT REFORMAT. Keep these entries.
16
+ #
17
+ # `pro-gov sync --check` byte-compares docs/governance against the packaged
18
+ # starter, and `pro-gov portfolio assets-check` byte-compares the shared-rule
19
+ # snapshots. A formatter that only pads Markdown table cells is enough to fail
20
+ # both, and the guardrails cannot tell cosmetic padding from a real local edit
21
+ # — re-applying the asset then refuses with asset-lock-drift.
22
+ #
23
+ # If you need one of these files changed, change it upstream in Project
24
+ # Governance System and re-sync; do not restyle it here.
25
+ # ─────────────────────────────────────────────────────────────────────────
26
+ docs/governance
27
+ docs/policy/shared-rules
package/dist/cli.js CHANGED
@@ -211,12 +211,12 @@ function toUnixPath2(path) {
211
211
  // src/asset-registry/loader.ts
212
212
  import { createHash as createHash2 } from "node:crypto";
213
213
  import { existsSync as existsSync5, readdirSync as readdirSync5, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
214
- import { dirname as dirname2, join as join5, relative as relative3 } from "node:path";
214
+ import { dirname as dirname2, join as join5, relative as relative4 } from "node:path";
215
215
  import { fileURLToPath as fileURLToPath2 } from "node:url";
216
216
 
217
217
  // src/asset-registry/registry.ts
218
- import { existsSync as existsSync4, lstatSync, readdirSync as readdirSync4 } from "node:fs";
219
- import { isAbsolute, join as join4, posix } from "node:path";
218
+ import { existsSync as existsSync4, lstatSync, readdirSync as readdirSync4, realpathSync } from "node:fs";
219
+ import { isAbsolute, join as join4, posix, relative as relative3, sep } from "node:path";
220
220
  var supportedFamilies = /* @__PURE__ */ new Set([
221
221
  "pie-skills",
222
222
  "npx-skills",
@@ -427,6 +427,13 @@ function validateAssetRegistry(registry, options = {}) {
427
427
  path: asset.sourcePath,
428
428
  message: `Asset source path does not exist: ${asset.sourcePath}`
429
429
  });
430
+ } else if (!isWithinAgentAssetsDir(options.agentAssetsDir, sourceAbsolutePath)) {
431
+ issues.push({
432
+ type: "source-target-escape",
433
+ id: asset.id,
434
+ path: asset.sourcePath,
435
+ message: `Asset source resolves outside agent-assets: ${asset.sourcePath}`
436
+ });
430
437
  } else if (asset.kind === "skill") {
431
438
  if (asset.sourceKind === "local-pack") {
432
439
  if (!isLocalSkillPack(sourceAbsolutePath)) {
@@ -483,6 +490,16 @@ function isSafeRegistrySourcePath(sourcePath) {
483
490
  function normalizeRegistrySourcePath(sourcePath) {
484
491
  return posix.normalize(sourcePath.replaceAll("\\", "/"));
485
492
  }
493
+ function isWithinAgentAssetsDir(agentAssetsDir, sourceAbsolutePath) {
494
+ try {
495
+ const agentAssetsRealPath = realpathSync(agentAssetsDir);
496
+ const sourceRealPath = realpathSync(sourceAbsolutePath);
497
+ const relativePath = relative3(agentAssetsRealPath, sourceRealPath);
498
+ return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
499
+ } catch {
500
+ return false;
501
+ }
502
+ }
486
503
  function pathExistsEvenIfDanglingSymlink(path) {
487
504
  try {
488
505
  lstatSync(path);
@@ -544,7 +561,7 @@ function hashAgentAssetContent(asset, agentAssetsDir) {
544
561
  function hashAssetPathContent(sourceAbsolutePath) {
545
562
  const hash = createHash2("sha256");
546
563
  for (const filePath of listFiles3(sourceAbsolutePath)) {
547
- const relativePath = toUnixPath3(relative3(sourceAbsolutePath, filePath));
564
+ const relativePath = toUnixPath3(relative4(sourceAbsolutePath, filePath));
548
565
  hash.update(relativePath);
549
566
  hash.update("\0");
550
567
  hash.update(readFileSync3(filePath));
@@ -712,15 +729,15 @@ import {
712
729
  lstatSync as lstatSync3,
713
730
  mkdirSync as mkdirSync2,
714
731
  readlinkSync as readlinkSync2,
715
- realpathSync as realpathSync2,
732
+ realpathSync as realpathSync3,
716
733
  symlinkSync,
717
734
  unlinkSync,
718
735
  writeFileSync
719
736
  } from "node:fs";
720
- import { dirname as dirname4, join as join8, relative as relative4, resolve as resolve2 } from "node:path";
737
+ import { dirname as dirname4, join as join8, relative as relative5, resolve as resolve2 } from "node:path";
721
738
 
722
739
  // src/asset-targets/install-plan.ts
723
- import { existsSync as existsSync7, lstatSync as lstatSync2, readFileSync as readFileSync4, readlinkSync, realpathSync, statSync as statSync3 } from "node:fs";
740
+ import { existsSync as existsSync7, lstatSync as lstatSync2, readFileSync as readFileSync4, readlinkSync, realpathSync as realpathSync2, statSync as statSync3 } from "node:fs";
724
741
  import { basename, dirname as dirname3, join as join7, resolve } from "node:path";
725
742
  function createAssetInstallPlan(options) {
726
743
  const placement = options.placement ?? "registry";
@@ -872,7 +889,7 @@ function createAssetAction(asset, agentAssetsDir, targetDir, host, placement, ma
872
889
  if (managedEntry && managedEntry.delivery === "snapshot") {
873
890
  throw new Error(`Refusing to replace managed snapshot with a symlink: ${targetPath}`);
874
891
  }
875
- if (stats.isSymbolicLink() && existsSync7(targetAbsolutePath) && realpathSync(targetAbsolutePath) === realpathSync(sourcePath)) {
892
+ if (stats.isSymbolicLink() && existsSync7(targetAbsolutePath) && realpathSync2(targetAbsolutePath) === realpathSync2(sourcePath)) {
876
893
  return {
877
894
  type: "adopt-existing-symlink",
878
895
  assetId: asset.id,
@@ -913,7 +930,7 @@ function createSnapshotAction(options) {
913
930
  }
914
931
  const stats = lstatSync2(options.targetAbsolutePath);
915
932
  if (stats.isSymbolicLink()) {
916
- if (existsSync7(options.targetAbsolutePath) && realpathSync(options.targetAbsolutePath) === realpathSync(options.sourcePath) && hashAssetPathContent(options.targetAbsolutePath) === contentHash) {
933
+ if (existsSync7(options.targetAbsolutePath) && realpathSync2(options.targetAbsolutePath) === realpathSync2(options.sourcePath) && hashAssetPathContent(options.targetAbsolutePath) === contentHash) {
917
934
  return {
918
935
  type: "migrate-symlink-to-snapshot",
919
936
  assetId: options.asset.id,
@@ -1031,7 +1048,7 @@ function createLegacyClaudeAdoptions(options) {
1031
1048
  const targetAbsolutePath = join7(options.targetDir, targetPath);
1032
1049
  const compatibilityRootPath = join7(options.targetDir, ".claude/skills");
1033
1050
  const canonicalRootPath = join7(options.targetDir, ".agents/skills");
1034
- if (!lstatSync2(canonicalRootPath).isDirectory() || !lstatSync2(compatibilityRootPath).isSymbolicLink() || readlinkSync(compatibilityRootPath) !== "../.agents/skills" || realpathSync(compatibilityRootPath) !== realpathSync(canonicalRootPath)) {
1051
+ if (!lstatSync2(canonicalRootPath).isDirectory() || !lstatSync2(compatibilityRootPath).isSymbolicLink() || readlinkSync(compatibilityRootPath) !== "../.agents/skills" || realpathSync2(compatibilityRootPath) !== realpathSync2(canonicalRootPath)) {
1035
1052
  throw new Error(
1036
1053
  `Legacy Claude compatibility root is not the exact canonical alias for ${entry.id}.`
1037
1054
  );
@@ -1044,7 +1061,7 @@ function createLegacyClaudeAdoptions(options) {
1044
1061
  const legacyAbsolutePath = join7(options.targetDir, entry.targetPath);
1045
1062
  const legacyStat = lstatSync2(legacyAbsolutePath);
1046
1063
  const expectedSourcePath = join7(options.agentAssetsDir, entry.sourcePath);
1047
- if (!targetStat.isSymbolicLink() || !legacyStat.isSymbolicLink() || targetStat.dev !== legacyStat.dev || targetStat.ino !== legacyStat.ino || realpathSync(targetAbsolutePath) !== realpathSync(expectedSourcePath)) {
1064
+ if (!targetStat.isSymbolicLink() || !legacyStat.isSymbolicLink() || targetStat.dev !== legacyStat.dev || targetStat.ino !== legacyStat.ino || realpathSync2(targetAbsolutePath) !== realpathSync2(expectedSourcePath)) {
1048
1065
  throw new Error(`Legacy Claude skill target cannot be safely adopted: ${entry.targetPath}`);
1049
1066
  }
1050
1067
  const action = {
@@ -1175,7 +1192,7 @@ function applyAction(targetDir, action) {
1175
1192
  }
1176
1193
  mkdirSync2(dirname4(targetAbsolutePath), { recursive: true });
1177
1194
  const sourceAbsolutePath = resolve2(action.sourcePath);
1178
- const symlinkTarget = relative4(realpathSync2(dirname4(targetAbsolutePath)), realpathSync2(sourceAbsolutePath)) || ".";
1195
+ const symlinkTarget = relative5(realpathSync3(dirname4(targetAbsolutePath)), realpathSync3(sourceAbsolutePath)) || ".";
1179
1196
  if (action.type === "symlink") {
1180
1197
  if (pathExistsEvenIfDanglingSymlink3(targetAbsolutePath)) {
1181
1198
  throw new Error(`Refusing to overwrite unmanaged target: ${action.targetPath}`);
@@ -1199,7 +1216,7 @@ function validateExistingSymlink(targetDir, action) {
1199
1216
  throw new Error(`Refusing unsafe existing symlink adoption: ${action.targetPath}`);
1200
1217
  }
1201
1218
  const targetAbsolutePath = join8(targetDir, action.targetPath);
1202
- if (!lstatSync3(targetAbsolutePath).isSymbolicLink() || realpathSync2(targetAbsolutePath) !== realpathSync2(action.sourcePath)) {
1219
+ if (!lstatSync3(targetAbsolutePath).isSymbolicLink() || realpathSync3(targetAbsolutePath) !== realpathSync3(action.sourcePath)) {
1203
1220
  throw new Error(`Existing skill symlink changed before apply: ${action.targetPath}`);
1204
1221
  }
1205
1222
  }
@@ -1226,7 +1243,7 @@ function validateSnapshotAction(targetDir, action) {
1226
1243
  return;
1227
1244
  }
1228
1245
  if (action.type === "migrate-symlink-to-snapshot") {
1229
- if (!pathExistsEvenIfDanglingSymlink3(targetAbsolutePath) || !lstatSync3(targetAbsolutePath).isSymbolicLink() || !existsSync8(targetAbsolutePath) || realpathSync2(targetAbsolutePath) !== realpathSync2(action.sourcePath) || hashAssetPathContent(targetAbsolutePath) !== action.contentHash) {
1246
+ if (!pathExistsEvenIfDanglingSymlink3(targetAbsolutePath) || !lstatSync3(targetAbsolutePath).isSymbolicLink() || !existsSync8(targetAbsolutePath) || realpathSync3(targetAbsolutePath) !== realpathSync3(action.sourcePath) || hashAssetPathContent(targetAbsolutePath) !== action.contentHash) {
1230
1247
  throw new Error(`Snapshot symlink changed before apply: ${action.targetPath}`);
1231
1248
  }
1232
1249
  return;
@@ -1272,14 +1289,14 @@ function validateAdoptedSymlink(targetDir, action) {
1272
1289
  const compatibilityRootPath = join8(targetDir, action.compatibilityRootPath);
1273
1290
  const targetAbsolutePath = join8(targetDir, action.targetPath);
1274
1291
  const legacyAbsolutePath = join8(targetDir, action.legacyTargetPath);
1275
- if (!lstatSync3(canonicalRootPath).isDirectory() || !lstatSync3(compatibilityRootPath).isSymbolicLink() || readlinkSync2(compatibilityRootPath) !== action.expectedCompatibilityRawTarget || realpathSync2(compatibilityRootPath) !== realpathSync2(canonicalRootPath)) {
1292
+ if (!lstatSync3(canonicalRootPath).isDirectory() || !lstatSync3(compatibilityRootPath).isSymbolicLink() || readlinkSync2(compatibilityRootPath) !== action.expectedCompatibilityRawTarget || realpathSync3(compatibilityRootPath) !== realpathSync3(canonicalRootPath)) {
1276
1293
  throw new Error(
1277
1294
  `Legacy Claude compatibility alias changed before apply: ${action.compatibilityRootPath}`
1278
1295
  );
1279
1296
  }
1280
1297
  const targetStat = lstatSync3(targetAbsolutePath);
1281
1298
  const legacyStat = lstatSync3(legacyAbsolutePath);
1282
- if (!targetStat.isSymbolicLink() || !legacyStat.isSymbolicLink() || targetStat.dev !== legacyStat.dev || targetStat.ino !== legacyStat.ino || targetStat.dev !== action.expectedDevice || targetStat.ino !== action.expectedInode || realpathSync2(targetAbsolutePath) !== realpathSync2(action.sourcePath)) {
1299
+ if (!targetStat.isSymbolicLink() || !legacyStat.isSymbolicLink() || targetStat.dev !== legacyStat.dev || targetStat.ino !== legacyStat.ino || targetStat.dev !== action.expectedDevice || targetStat.ino !== action.expectedInode || realpathSync3(targetAbsolutePath) !== realpathSync3(action.sourcePath)) {
1283
1300
  throw new Error(`Legacy Claude skill target changed before apply: ${action.targetPath}`);
1284
1301
  }
1285
1302
  }
@@ -1312,7 +1329,7 @@ function pathExistsEvenIfDanglingSymlink3(path) {
1312
1329
  }
1313
1330
 
1314
1331
  // src/asset-targets/check.ts
1315
- import { existsSync as existsSync9, lstatSync as lstatSync4, readFileSync as readFileSync5 } from "node:fs";
1332
+ import { existsSync as existsSync9, lstatSync as lstatSync4, readFileSync as readFileSync5, realpathSync as realpathSync4 } from "node:fs";
1316
1333
  import { join as join9 } from "node:path";
1317
1334
  function checkInstalledAssets(options) {
1318
1335
  const lockfilePath = join9(options.targetDir, ".pro-gov/assets.lock.json");
@@ -1469,6 +1486,18 @@ function checkInstalledAssets(options) {
1469
1486
  });
1470
1487
  continue;
1471
1488
  }
1489
+ if (delivery === "symlink") {
1490
+ const expectedSource = realpathSync4(sourceAbsolutePath);
1491
+ const actualSource = realpathSync4(targetAbsolutePath);
1492
+ if (expectedSource !== actualSource) {
1493
+ issues.push({
1494
+ type: "symlink-source-drift",
1495
+ id: entry.id,
1496
+ targetPath: entry.targetPath,
1497
+ message: `Managed symlink does not resolve to its registered source: ${entry.targetPath}`
1498
+ });
1499
+ }
1500
+ }
1472
1501
  const currentSourceHash = hashAgentAssetContent(asset, options.agentAssetsDir);
1473
1502
  if ((delivery === "snapshot" || targetHashMatchesLock) && currentSourceHash !== entry.contentHash) {
1474
1503
  issues.push({
@@ -2222,25 +2251,26 @@ function runPackageDoctor(_args, dependencies = {}) {
2222
2251
  function runDoctor(args) {
2223
2252
  return runPackageDoctor(args);
2224
2253
  }
2225
- function checkDocGov() {
2226
- const fromPath = spawnSync2("doc-gov", ["--help"], {
2254
+ function checkDocGov(options = {}) {
2255
+ const run = options.run ?? ((command2, args) => spawnSync2(command2, args, {
2227
2256
  encoding: "utf8",
2228
2257
  stdio: "ignore"
2229
- });
2230
- if (!fromPath.error && fromPath.status === 0) {
2231
- return { ok: true, message: "doc-gov: available on PATH" };
2232
- }
2233
- const dependencyCli = resolveDocGovDependencyCli();
2258
+ }));
2259
+ const dependencyCli = (options.resolveDependencyCli ?? resolveDocGovDependencyCli)();
2234
2260
  if (!dependencyCli) {
2261
+ const fromPath = run("doc-gov", ["--help"]);
2262
+ if (!fromPath.error && fromPath.status === 0) {
2263
+ return {
2264
+ ok: false,
2265
+ message: "doc-gov: found only on PATH; the required local @pieai/doc-gov package dependency is unavailable."
2266
+ };
2267
+ }
2235
2268
  return {
2236
2269
  ok: false,
2237
- message: "doc-gov: not found; install @pieai/doc-gov beside @pieai/pro-gov for validation."
2270
+ message: "doc-gov: not found; install the required @pieai/doc-gov package dependency beside @pieai/pro-gov."
2238
2271
  };
2239
2272
  }
2240
- const fromDependency = spawnSync2(process.execPath, [dependencyCli, "--help"], {
2241
- encoding: "utf8",
2242
- stdio: "ignore"
2243
- });
2273
+ const fromDependency = run(process.execPath, [dependencyCli, "--help"]);
2244
2274
  if (!fromDependency.error && fromDependency.status === 0) {
2245
2275
  return { ok: true, message: "doc-gov: available via package dependency" };
2246
2276
  }
@@ -2298,7 +2328,10 @@ function classifyOwnership(targetPath) {
2298
2328
  if (targetPath === "lefthook.yml" || targetPath === ".github/workflows/docs-check.yml") {
2299
2329
  return "optional-guardrail";
2300
2330
  }
2301
- if (targetPath === "AGENTS.md" || targetPath === "docs/policy/best-practice-for-this-project.md" || targetPath === "docs/reference/documentation-map.md" || targetPath === "docs/reference/execution/current-work.md") {
2331
+ if (targetPath === "AGENTS.md" || // Seeded so a new project starts with the managed paths protected from its
2332
+ // formatter, then owned locally — every project adds its own build outputs,
2333
+ // so this must not be byte-compared by `sync --check`.
2334
+ targetPath === ".oxfmtignore" || targetPath === "docs/policy/best-practice-for-this-project.md" || targetPath === "docs/reference/documentation-map.md" || targetPath === "docs/reference/execution/current-work.md") {
2302
2335
  return "project-local-seed";
2303
2336
  }
2304
2337
  return "shared";
@@ -2416,7 +2449,7 @@ import {
2416
2449
  writeFileSync as writeFileSync4
2417
2450
  } from "node:fs";
2418
2451
  import { homedir } from "node:os";
2419
- import { dirname as dirname8, join as join14, relative as relative5, resolve as resolve3 } from "node:path";
2452
+ import { dirname as dirname8, join as join14, relative as relative6, resolve as resolve3 } from "node:path";
2420
2453
  import { fileURLToPath as fileURLToPath3 } from "node:url";
2421
2454
  var ROOT_DEFINITIONS = [
2422
2455
  {
@@ -2563,7 +2596,11 @@ function inspectAgentBrowser(options) {
2563
2596
  const output = execFileSync(command2, ["--version"], {
2564
2597
  encoding: "utf8",
2565
2598
  stdio: ["ignore", "pipe", "ignore"],
2566
- timeout: 3e3
2599
+ // Host scans can run alongside the full portfolio test matrix. Keep the
2600
+ // probe bounded, but leave enough headroom for a busy Node process to
2601
+ // spawn the pinned CLI instead of misclassifying a slow start as an
2602
+ // unparseable version.
2603
+ timeout: 1e4
2567
2604
  }).trim();
2568
2605
  const installedVersion = output.match(/(?:^|\s)(\d+\.\d+\.\d+)(?:\s|$)/)?.[1];
2569
2606
  if (!installedVersion) {
@@ -2857,7 +2894,7 @@ function countImmediateEntries(path) {
2857
2894
  }
2858
2895
  function displayPath(path, homePath) {
2859
2896
  const absolute = resolve3(path);
2860
- const withinHome = relative5(homePath, absolute);
2897
+ const withinHome = relative6(homePath, absolute);
2861
2898
  if (withinHome === "") return "~";
2862
2899
  if (!withinHome.startsWith("..")) return `~/${withinHome}`;
2863
2900
  return absolute;
@@ -3983,7 +4020,7 @@ function printUsage2() {
3983
4020
 
3984
4021
  // src/learning/recall.ts
3985
4022
  import { existsSync as existsSync15, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
3986
- import { basename as basename3, join as join15, relative as relative6 } from "node:path";
4023
+ import { basename as basename3, join as join15, relative as relative7 } from "node:path";
3987
4024
  function recallLearnings(root, options) {
3988
4025
  const query = options.query.trim();
3989
4026
  const terms = tokenize(query);
@@ -4038,7 +4075,7 @@ function readLearningRecord(root, absolutePath) {
4038
4075
  const parsed = splitFrontmatter(content);
4039
4076
  const body = parsed.body;
4040
4077
  return {
4041
- relativePath: normalizePath(relative6(root, absolutePath)),
4078
+ relativePath: normalizePath(relative7(root, absolutePath)),
4042
4079
  title: findTitle(parsed.frontmatter, body) ?? titleFromPath(absolutePath),
4043
4080
  metadata: parsed.frontmatter,
4044
4081
  body
@@ -4120,7 +4157,7 @@ function cleanMarkdownLine(input) {
4120
4157
 
4121
4158
  // src/learning/capture.ts
4122
4159
  import { existsSync as existsSync16, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "node:fs";
4123
- import { basename as basename4, join as join16, relative as relative7 } from "node:path";
4160
+ import { basename as basename4, join as join16, relative as relative8 } from "node:path";
4124
4161
  function captureLearning(root, options) {
4125
4162
  const title = options.title.trim();
4126
4163
  const summary = options.summary.trim();
@@ -4134,7 +4171,7 @@ function captureLearning(root, options) {
4134
4171
  const idSlug = basename4(path, ".md");
4135
4172
  writeFileSync6(path, renderLearning({ title, summary, category, moduleName, idSlug }));
4136
4173
  return {
4137
- relativePath: normalizePath2(relative7(root, path)),
4174
+ relativePath: normalizePath2(relative8(root, path)),
4138
4175
  title,
4139
4176
  captureMode: "pgs-native"
4140
4177
  };
@@ -4758,7 +4795,7 @@ import { homedir as homedir3 } from "node:os";
4758
4795
  import { join as join22 } from "node:path";
4759
4796
 
4760
4797
  // src/host-ssot.ts
4761
- import { lstatSync as lstatSync7, readlinkSync as readlinkSync3, realpathSync as realpathSync3 } from "node:fs";
4798
+ import { lstatSync as lstatSync7, readlinkSync as readlinkSync3, realpathSync as realpathSync5 } from "node:fs";
4762
4799
  import { dirname as dirname12, isAbsolute as isAbsolute4, join as join18, resolve as resolve6 } from "node:path";
4763
4800
  function inspectProjectHostSsot(root) {
4764
4801
  const agentsEntry = inspectCanonicalPath(root, "AGENTS.md");
@@ -4808,7 +4845,7 @@ function inspectCanonicalPath(root, path) {
4808
4845
  if (!stat) return { path, status: "missing" };
4809
4846
  if (stat.isSymbolicLink()) {
4810
4847
  try {
4811
- realpathSync3(absolutePath);
4848
+ realpathSync5(absolutePath);
4812
4849
  return { path, status: "symlink" };
4813
4850
  } catch {
4814
4851
  return { path, status: "dangling-symlink" };
@@ -4842,7 +4879,7 @@ function inspectCompatibilityLink(root, path, expectedRawTarget) {
4842
4879
  }
4843
4880
  let targetMatches = false;
4844
4881
  try {
4845
- targetMatches = realpathSync3(absolutePath) === realpathSync3(expectedPath);
4882
+ targetMatches = realpathSync5(absolutePath) === realpathSync5(expectedPath);
4846
4883
  } catch {
4847
4884
  return { ...base, rawTarget, resolvedTarget, status: "dangling-symlink" };
4848
4885
  }
@@ -5036,7 +5073,7 @@ function readPackageJson(path) {
5036
5073
  // src/repository-files.ts
5037
5074
  import { spawnSync as spawnSync3 } from "node:child_process";
5038
5075
  import { existsSync as existsSync20, readdirSync as readdirSync10 } from "node:fs";
5039
- import { isAbsolute as isAbsolute5, join as join21, posix as posix3, relative as relative8 } from "node:path";
5076
+ import { isAbsolute as isAbsolute5, join as join21, posix as posix3, relative as relative9 } from "node:path";
5040
5077
  var gitMaxBufferBytes = 64 * 1024 * 1024;
5041
5078
  var RepositoryFileDiscoveryError = class extends Error {
5042
5079
  constructor(message) {
@@ -5104,7 +5141,7 @@ function discoverFilesystemFiles(root, options) {
5104
5141
  continue;
5105
5142
  }
5106
5143
  if (!entry.isFile()) continue;
5107
- const relativePath = normalizeRepositoryRelativePath(relative8(root, absolutePath));
5144
+ const relativePath = normalizeRepositoryRelativePath(relative9(root, absolutePath));
5108
5145
  if (isSafeRepositoryRelativePath(relativePath) && (options.fallbackIncludeFile?.(relativePath) ?? true)) {
5109
5146
  files.add(relativePath);
5110
5147
  }
@@ -5954,15 +5991,15 @@ import { homedir as homedir5 } from "node:os";
5954
5991
  import { dirname as dirname17, join as join33, resolve as resolve9 } from "node:path";
5955
5992
 
5956
5993
  // src/portfolio/ai-health/entries.ts
5957
- import { existsSync as existsSync26, lstatSync as lstatSync11, realpathSync as realpathSync5 } from "node:fs";
5994
+ import { existsSync as existsSync26, lstatSync as lstatSync11, realpathSync as realpathSync7 } from "node:fs";
5958
5995
  import { join as join26 } from "node:path";
5959
5996
 
5960
5997
  // src/portfolio/ai-health/shared.ts
5961
5998
  import { execFileSync as execFileSync2 } from "node:child_process";
5962
- import { existsSync as existsSync25, lstatSync as lstatSync10, readFileSync as readFileSync17, readdirSync as readdirSync11, realpathSync as realpathSync4, statSync as statSync7 } from "node:fs";
5999
+ import { existsSync as existsSync25, lstatSync as lstatSync10, readFileSync as readFileSync17, readdirSync as readdirSync11, realpathSync as realpathSync6, statSync as statSync7 } from "node:fs";
5963
6000
  function safeRealpath(path) {
5964
6001
  try {
5965
- return realpathSync4(path);
6002
+ return realpathSync6(path);
5966
6003
  } catch {
5967
6004
  return void 0;
5968
6005
  }
@@ -6074,7 +6111,7 @@ function inspectEntries(root) {
6074
6111
  const info = lstatSync11(claudePath);
6075
6112
  if (info.isSymbolicLink()) {
6076
6113
  try {
6077
- claude = realpathSync5(claudePath) === realpathSync5(agentsPath) ? "agents-symlink" : "custom";
6114
+ claude = realpathSync7(claudePath) === realpathSync7(agentsPath) ? "agents-symlink" : "custom";
6078
6115
  } catch {
6079
6116
  claude = "dangling-symlink";
6080
6117
  }
@@ -6107,7 +6144,7 @@ function inspectAgentLinks(root) {
6107
6144
  let kind = stat.isSymbolicLink() ? "symlink" : stat.isDirectory() ? "directory" : "file";
6108
6145
  if (stat.isSymbolicLink()) {
6109
6146
  try {
6110
- realpathSync5(path);
6147
+ realpathSync7(path);
6111
6148
  } catch {
6112
6149
  kind = "dangling-symlink";
6113
6150
  }
@@ -6126,7 +6163,7 @@ function inspectOptionalEntry(root, filename, agentsPath) {
6126
6163
  const info = lstatSync11(path);
6127
6164
  if (info.isSymbolicLink()) {
6128
6165
  try {
6129
- return realpathSync5(path) === realpathSync5(agentsPath) ? "agents-symlink" : "custom";
6166
+ return realpathSync7(path) === realpathSync7(agentsPath) ? "agents-symlink" : "custom";
6130
6167
  } catch {
6131
6168
  return "dangling-symlink";
6132
6169
  }
@@ -6257,12 +6294,12 @@ function inspectGit2(root) {
6257
6294
  // src/portfolio/ai-health/hosts.ts
6258
6295
  import { execFileSync as execFileSync5 } from "node:child_process";
6259
6296
  import { existsSync as existsSync28 } from "node:fs";
6260
- import { join as join28, resolve as resolve8, sep as sep2 } from "node:path";
6297
+ import { join as join28, resolve as resolve8, sep as sep3 } from "node:path";
6261
6298
 
6262
6299
  // src/portfolio/ai-health/devspace.ts
6263
6300
  import { execFileSync as execFileSync4 } from "node:child_process";
6264
6301
  import { existsSync as existsSync27, statSync as statSync8 } from "node:fs";
6265
- import { join as join27, relative as relative9, resolve as resolve7, sep } from "node:path";
6302
+ import { join as join27, relative as relative10, resolve as resolve7, sep as sep2 } from "node:path";
6266
6303
  function inspectDevSpaceHealth(options) {
6267
6304
  const run = options.run ?? runDevSpaceCommand;
6268
6305
  const configDirectory = join27(options.homeDir, ".devspace");
@@ -6368,8 +6405,8 @@ function isLoopbackHost(host) {
6368
6405
  return ["127.0.0.1", "localhost", "::1"].includes(host.trim().toLowerCase());
6369
6406
  }
6370
6407
  function isPathInside(path, root) {
6371
- const fromRoot = relative9(resolve7(root), resolve7(path));
6372
- return fromRoot === "" || fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`);
6408
+ const fromRoot = relative10(resolve7(root), resolve7(path));
6409
+ return fromRoot === "" || fromRoot !== ".." && !fromRoot.startsWith(`..${sep2}`);
6373
6410
  }
6374
6411
 
6375
6412
  // src/portfolio/ai-health/hosts.ts
@@ -6498,7 +6535,7 @@ function inspectGrokProject(root, homeDir, grokVersion) {
6498
6535
  if (!isRecord3(item) || !isRecord3(item.source)) continue;
6499
6536
  const type = item.source.type;
6500
6537
  const path = item.source.path;
6501
- if (typeof path === "string" && path.includes(sep2 + ".grok" + sep2 + "bundled" + sep2))
6538
+ if (typeof path === "string" && path.includes(sep3 + ".grok" + sep3 + "bundled" + sep3))
6502
6539
  skillCounts.bundled += 1;
6503
6540
  else if (type === "project") skillCounts.project += 1;
6504
6541
  else if (type === "plugin") skillCounts.plugin += 1;
@@ -6537,19 +6574,19 @@ function inferGrokMcpScope(name, sourceType, sourcePath, root, homeDir, userClau
6537
6574
  const resolvedRoot = safeRealpath(root) ?? resolve8(root);
6538
6575
  if (resolvedSource === join28(resolvedRoot, MCP_DISCOVERY_PATHS.project.claudeCodeShared))
6539
6576
  return "project-shared";
6540
- if (resolvedSource.startsWith(resolvedRoot + sep2)) return "project";
6577
+ if (resolvedSource.startsWith(resolvedRoot + sep3)) return "project";
6541
6578
  if (homeDir && resolvedSource === join28(resolve8(homeDir), MCP_DISCOVERY_PATHS.user.claudeCode)) {
6542
6579
  if (localClaudeNames.has(name)) return "project-local";
6543
6580
  if (userClaudeNames.has(name)) return "user";
6544
6581
  }
6545
- if (homeDir && resolvedSource.startsWith(resolve8(homeDir) + sep2)) return "user";
6582
+ if (homeDir && resolvedSource.startsWith(resolve8(homeDir) + sep3)) return "user";
6546
6583
  if (sourceType === "project") return "project";
6547
6584
  return "unknown";
6548
6585
  }
6549
6586
 
6550
6587
  // src/portfolio/ai-health/secrets.ts
6551
6588
  import { existsSync as existsSync29, lstatSync as lstatSync12, readdirSync as readdirSync12, statSync as statSync9 } from "node:fs";
6552
- import { join as join29, relative as relative10, sep as sep3 } from "node:path";
6589
+ import { join as join29, relative as relative11, sep as sep4 } from "node:path";
6553
6590
  function inspectRepositorySecrets(root, id, secretsRoot, isRepository, environmentPolicy) {
6554
6591
  const centralPath = join29(secretsRoot, id);
6555
6592
  const centralRealPath = safeRealpath(centralPath);
@@ -6600,7 +6637,7 @@ function collectEnvironmentFiles(root, current = root, depth = 0) {
6600
6637
  if (!SKIP_ENV_DIRECTORIES.has(entry.name))
6601
6638
  found.push(...collectEnvironmentFiles(root, join29(current, entry.name), depth + 1));
6602
6639
  } else if (isEnvironmentFilename(entry.name) && !isProviderGeneratedEnvironmentFile(entry.name)) {
6603
- found.push(relative10(root, join29(current, entry.name)));
6640
+ found.push(relative11(root, join29(current, entry.name)));
6604
6641
  }
6605
6642
  }
6606
6643
  } catch {
@@ -6615,7 +6652,7 @@ function collectCentralSecretFiles(root, current = root, depth = 0) {
6615
6652
  for (const entry of readdirSync12(current, { withFileTypes: true })) {
6616
6653
  const path = join29(current, entry.name);
6617
6654
  if (entry.isDirectory()) found.push(...collectCentralSecretFiles(root, path, depth + 1));
6618
- else found.push({ path: relative10(root, path), mode: modeString(lstatSync12(path).mode) });
6655
+ else found.push({ path: relative11(root, path), mode: modeString(lstatSync12(path).mode) });
6619
6656
  }
6620
6657
  } catch {
6621
6658
  return found;
@@ -6642,8 +6679,8 @@ function pointsInside(path, expectedRoot) {
6642
6679
  if (!expectedRoot || !lstatSync12(path).isSymbolicLink()) return false;
6643
6680
  const target = safeRealpath(path);
6644
6681
  if (!target) return false;
6645
- const fromRoot = relative10(expectedRoot, target);
6646
- return fromRoot === "" || fromRoot !== ".." && !fromRoot.startsWith(`..${sep3}`);
6682
+ const fromRoot = relative11(expectedRoot, target);
6683
+ return fromRoot === "" || fromRoot !== ".." && !fromRoot.startsWith(`..${sep4}`);
6647
6684
  }
6648
6685
  function hasUnsafeCentralSecretPermissions(secrets) {
6649
6686
  return secrets.centralDirectory === "present" && (secrets.centralMode !== "700" || secrets.centralFiles.some((file) => file.mode !== "600"));
@@ -6653,7 +6690,7 @@ function inspectSecretsRoot(path) {
6653
6690
  }
6654
6691
 
6655
6692
  // src/portfolio/ai-health/skills.ts
6656
- import { existsSync as existsSync30, lstatSync as lstatSync13, realpathSync as realpathSync6 } from "node:fs";
6693
+ import { existsSync as existsSync30, lstatSync as lstatSync13, realpathSync as realpathSync8 } from "node:fs";
6657
6694
  import { join as join30 } from "node:path";
6658
6695
  function countAutomaticSkillsNeedingReview(skills) {
6659
6696
  return skills.automatic.filter(
@@ -6741,7 +6778,7 @@ function inspectSkillItem(skillRoot, directory, name, managed, registeredSkills)
6741
6778
  let kind = stat.isSymbolicLink() ? "symlink" : stat.isDirectory() ? "directory" : "file";
6742
6779
  let realPath;
6743
6780
  try {
6744
- realPath = realpathSync6(path);
6781
+ realPath = realpathSync8(path);
6745
6782
  } catch {
6746
6783
  if (stat.isSymbolicLink()) kind = "dangling-symlink";
6747
6784
  }
@@ -6802,8 +6839,8 @@ function inspectClaudeSkillRoot(root) {
6802
6839
  const stat = lstatSync13(path);
6803
6840
  if (stat.isSymbolicLink()) {
6804
6841
  try {
6805
- const target = realpathSync6(path);
6806
- return target === realpathSync6(join30(root, ".agents/skills")) ? "shared-root" : "other";
6842
+ const target = realpathSync8(path);
6843
+ return target === realpathSync8(join30(root, ".agents/skills")) ? "shared-root" : "other";
6807
6844
  } catch {
6808
6845
  return "dangling-symlink";
6809
6846
  }
@@ -7568,8 +7605,12 @@ function runPortfolioDoctor(args) {
7568
7605
  console.error(`Unknown portfolio target: ${options.value.targetId}`);
7569
7606
  return 1;
7570
7607
  }
7608
+ const agentAssetsDir = findPortfolioAgentAssetsDir(loaded.manifest);
7609
+ if (loaded.manifest.executionEngine?.path && !agentAssetsDir) {
7610
+ return reportMissingPortfolioRegistry(loaded, options.value.json);
7611
+ }
7571
7612
  const loadedAssets = loadAgentAssetRegistry({
7572
- agentAssetsDir: findPortfolioAgentAssetsDir(loaded.manifest)
7613
+ agentAssetsDir
7573
7614
  });
7574
7615
  if (loadedAssets.issues.length > 0) {
7575
7616
  for (const issue of loadedAssets.issues) console.error(`${issue.type}: ${issue.message}`);
@@ -7670,8 +7711,12 @@ function runPortfolioPlan(args) {
7670
7711
  console.error(`Unknown portfolio target: ${options.value.targetId}`);
7671
7712
  return 1;
7672
7713
  }
7714
+ const agentAssetsDir = findPortfolioAgentAssetsDir(loaded.manifest);
7715
+ if (loaded.manifest.executionEngine?.path && !agentAssetsDir) {
7716
+ return reportMissingPortfolioRegistry(loaded, options.value.json);
7717
+ }
7673
7718
  const loadedAssets = loadAgentAssetRegistry({
7674
- agentAssetsDir: findPortfolioAgentAssetsDir(loaded.manifest)
7719
+ agentAssetsDir
7675
7720
  });
7676
7721
  if (loadedAssets.issues.length > 0) {
7677
7722
  for (const issue of loadedAssets.issues) {
@@ -7760,8 +7805,12 @@ function runPortfolioAssetsCheck(args) {
7760
7805
  console.error(`Unknown portfolio target: ${options.value.targetId}`);
7761
7806
  return 1;
7762
7807
  }
7808
+ const agentAssetsDir = findPortfolioAgentAssetsDir(loaded.manifest);
7809
+ if (loaded.manifest.executionEngine?.path && !agentAssetsDir) {
7810
+ return reportMissingPortfolioRegistry(loaded, options.value.json);
7811
+ }
7763
7812
  const loadedAssets = loadAgentAssetRegistry({
7764
- agentAssetsDir: findPortfolioAgentAssetsDir(loaded.manifest)
7813
+ agentAssetsDir
7765
7814
  });
7766
7815
  if (loadedAssets.issues.length > 0) {
7767
7816
  if (options.value.json) {
@@ -7874,6 +7923,31 @@ function findPortfolioAgentAssetsDir(manifest) {
7874
7923
  const agentAssetsDir = manifest?.executionEngine?.path ? join34(manifest.executionEngine.path, "agent-assets") : void 0;
7875
7924
  return agentAssetsDir && existsSync33(join34(agentAssetsDir, "registry.json")) ? agentAssetsDir : void 0;
7876
7925
  }
7926
+ function reportMissingPortfolioRegistry(loaded, json) {
7927
+ const expectedPath = loaded.manifest?.executionEngine?.path ? join34(loaded.manifest.executionEngine.path, "agent-assets/registry.json") : "executionEngine.path/agent-assets/registry.json";
7928
+ const issue = {
7929
+ type: "missing-control-plane-registry",
7930
+ message: `Portfolio control-plane registry is required at ${expectedPath}; refusing the package fallback registry.`
7931
+ };
7932
+ if (json) {
7933
+ console.log(
7934
+ JSON.stringify(
7935
+ {
7936
+ ok: false,
7937
+ configPath: loaded.configPath,
7938
+ portfolioId: loaded.manifest?.portfolioId,
7939
+ issues: [issue],
7940
+ targets: []
7941
+ },
7942
+ null,
7943
+ 2
7944
+ )
7945
+ );
7946
+ } else {
7947
+ console.error(`${issue.type}: ${issue.message}`);
7948
+ }
7949
+ return 1;
7950
+ }
7877
7951
  function printUsage5() {
7878
7952
  console.error("Usage:");
7879
7953
  console.error(" pro-gov portfolio check --config <path> [--json]");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pieai/pro-gov",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Project-level distribution kit for Project Governance System.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -35,7 +35,7 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "@pieai/doc-gov": "^0.9.0"
38
+ "@pieai/doc-gov": "^0.9.2"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@pieai/swimmer-ui-kit": "1.3.2",