@pieai/pro-gov 0.3.11 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/assets/docs/reference/adoption/adoption-playbook.md +14 -0
- package/assets/docs/reference/adoption/recommended-agent-tooling.md +14 -1
- package/assets/integrations/compound-engineering.md +23 -4
- package/assets/integrations/superpowers.md +15 -9
- package/assets/profiles/engineering-runtime/profile.md +5 -0
- package/assets/starter/AGENTS.template.md +4 -0
- package/cli-guide.md +33 -0
- package/dist/cli.js +1047 -274
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -79,7 +79,14 @@ function createNpxSkillsMaintenancePlan(options) {
|
|
|
79
79
|
cpSync(options.npxRoot, tempRoot, { recursive: true, dereference: false });
|
|
80
80
|
const command2 = buildNpxCommand(options);
|
|
81
81
|
const runner = options.runner ?? defaultRunner;
|
|
82
|
-
const
|
|
82
|
+
const timeoutMs = options.timeoutMs ?? 3e5;
|
|
83
|
+
const result = runner({ command: command2, cwd: tempRoot, timeoutMs });
|
|
84
|
+
if (result.timedOut) {
|
|
85
|
+
throw new Error(`npx skills ${options.operation} timed out after ${timeoutMs}ms`);
|
|
86
|
+
}
|
|
87
|
+
if (result.status === null && result.signal) {
|
|
88
|
+
throw new Error(`npx skills ${options.operation} terminated by ${result.signal}`);
|
|
89
|
+
}
|
|
83
90
|
if (result.status !== 0) {
|
|
84
91
|
throw new Error(`npx skills ${options.operation} failed with exit code ${result.status}`);
|
|
85
92
|
}
|
|
@@ -128,12 +135,18 @@ function buildNpxCommand(options) {
|
|
|
128
135
|
if (options.skill) command2.push(options.skill);
|
|
129
136
|
return command2;
|
|
130
137
|
}
|
|
131
|
-
function defaultRunner({ command: command2, cwd }) {
|
|
132
|
-
const result = spawnSync(command2[0] ?? "npx", command2.slice(1), {
|
|
138
|
+
function defaultRunner({ command: command2, cwd, timeoutMs }) {
|
|
139
|
+
const result = spawnSync(command2[0] ?? "npx", command2.slice(1), {
|
|
140
|
+
cwd,
|
|
141
|
+
encoding: "utf8",
|
|
142
|
+
timeout: timeoutMs
|
|
143
|
+
});
|
|
133
144
|
return {
|
|
134
145
|
status: result.status,
|
|
135
146
|
stdout: result.stdout,
|
|
136
|
-
stderr: result.stderr
|
|
147
|
+
stderr: result.stderr,
|
|
148
|
+
signal: result.signal,
|
|
149
|
+
timedOut: result.error?.code === "ETIMEDOUT"
|
|
137
150
|
};
|
|
138
151
|
}
|
|
139
152
|
function snapshotFiles(root) {
|
|
@@ -536,8 +549,229 @@ function resolveSafePath(root, sourcePath) {
|
|
|
536
549
|
}
|
|
537
550
|
|
|
538
551
|
// src/asset-targets/apply.ts
|
|
539
|
-
import {
|
|
540
|
-
|
|
552
|
+
import {
|
|
553
|
+
existsSync as existsSync8,
|
|
554
|
+
lstatSync as lstatSync3,
|
|
555
|
+
mkdirSync as mkdirSync2,
|
|
556
|
+
readlinkSync as readlinkSync2,
|
|
557
|
+
realpathSync,
|
|
558
|
+
symlinkSync,
|
|
559
|
+
unlinkSync,
|
|
560
|
+
writeFileSync
|
|
561
|
+
} from "node:fs";
|
|
562
|
+
import { dirname as dirname4, join as join8, relative as relative4, resolve as resolve2 } from "node:path";
|
|
563
|
+
|
|
564
|
+
// src/asset-targets/install-plan.ts
|
|
565
|
+
import { existsSync as existsSync7, lstatSync as lstatSync2, readFileSync as readFileSync4, readlinkSync } from "node:fs";
|
|
566
|
+
import { basename, dirname as dirname3, join as join7, resolve } from "node:path";
|
|
567
|
+
function createAssetInstallPlan(options) {
|
|
568
|
+
const placement = options.placement ?? "registry";
|
|
569
|
+
const assetsById = new Map(options.registry.assets.map((asset) => [asset.id, asset]));
|
|
570
|
+
const bundlesById = new Map(options.bundles.map((bundle) => [bundle.id, bundle]));
|
|
571
|
+
const assetIds = resolveBundleAssetIds(options.bundleIds, bundlesById);
|
|
572
|
+
const assets = assetIds.map((assetId) => {
|
|
573
|
+
const asset = assetsById.get(assetId);
|
|
574
|
+
if (!asset) throw new Error(`Unknown asset id in bundle: ${assetId}`);
|
|
575
|
+
if (asset.kind === "skill" && !asset.hosts.includes(options.host)) {
|
|
576
|
+
throw new Error(`Asset ${assetId} does not support host ${options.host}`);
|
|
577
|
+
}
|
|
578
|
+
return asset;
|
|
579
|
+
});
|
|
580
|
+
const lockEntries = createAgentAssetLockEntries(options.registry, options.agentAssetsDir, assetIds);
|
|
581
|
+
const managedEntries = readManagedEntries(options.targetDir);
|
|
582
|
+
const managedTargets = new Set(managedEntries.map((entry) => entry.targetPath));
|
|
583
|
+
const assetActions = assets.map(
|
|
584
|
+
(asset) => createAssetAction(asset, options.agentAssetsDir, options.targetDir, options.host, placement, managedTargets)
|
|
585
|
+
);
|
|
586
|
+
const manifest = {
|
|
587
|
+
schemaVersion: 1,
|
|
588
|
+
host: options.host,
|
|
589
|
+
placement,
|
|
590
|
+
bundleIds: [...options.bundleIds],
|
|
591
|
+
assetIds
|
|
592
|
+
};
|
|
593
|
+
const lockfile = {
|
|
594
|
+
schemaVersion: 1,
|
|
595
|
+
host: options.host,
|
|
596
|
+
placement,
|
|
597
|
+
bundleIds: [...options.bundleIds],
|
|
598
|
+
assets: lockEntries.map((entry) => {
|
|
599
|
+
const action = assetActions.find((candidate) => "assetId" in candidate && candidate.assetId === entry.id);
|
|
600
|
+
return {
|
|
601
|
+
...entry,
|
|
602
|
+
targetPath: action && "targetPath" in action ? action.targetPath : ""
|
|
603
|
+
};
|
|
604
|
+
})
|
|
605
|
+
};
|
|
606
|
+
const writeActions = [
|
|
607
|
+
{
|
|
608
|
+
type: "write-file",
|
|
609
|
+
targetPath: ".pro-gov/assets.json",
|
|
610
|
+
content: `${JSON.stringify(manifest, null, 2)}
|
|
611
|
+
`
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
type: "write-file",
|
|
615
|
+
targetPath: ".pro-gov/assets.lock.json",
|
|
616
|
+
content: `${JSON.stringify(lockfile, null, 2)}
|
|
617
|
+
`
|
|
618
|
+
}
|
|
619
|
+
];
|
|
620
|
+
const expectedTargetPaths = new Set(
|
|
621
|
+
assetActions.filter((action) => "assetId" in action).map((action) => action.targetPath)
|
|
622
|
+
);
|
|
623
|
+
const removalActions = createRemovalActions(
|
|
624
|
+
options.targetDir,
|
|
625
|
+
options.agentAssetsDir,
|
|
626
|
+
managedEntries,
|
|
627
|
+
expectedTargetPaths
|
|
628
|
+
);
|
|
629
|
+
return {
|
|
630
|
+
schemaVersion: 1,
|
|
631
|
+
dryRun: true,
|
|
632
|
+
targetDir: options.targetDir,
|
|
633
|
+
host: options.host,
|
|
634
|
+
placement,
|
|
635
|
+
bundleIds: [...options.bundleIds],
|
|
636
|
+
assetIds,
|
|
637
|
+
actions: [
|
|
638
|
+
...createDirectoryActions([...assetActions, ...writeActions]),
|
|
639
|
+
...removalActions,
|
|
640
|
+
...assetActions,
|
|
641
|
+
...writeActions
|
|
642
|
+
]
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
function resolveBundleAssetIds(bundleIds, bundlesById) {
|
|
646
|
+
const ids = /* @__PURE__ */ new Set();
|
|
647
|
+
for (const bundleId of bundleIds) {
|
|
648
|
+
const bundle = bundlesById.get(bundleId);
|
|
649
|
+
if (!bundle) throw new Error(`Unknown bundle id: ${bundleId}`);
|
|
650
|
+
for (const assetId of bundle.assets) {
|
|
651
|
+
ids.add(assetId);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return [...ids].sort();
|
|
655
|
+
}
|
|
656
|
+
function createAssetAction(asset, agentAssetsDir, targetDir, host, placement, managedTargets) {
|
|
657
|
+
if (asset.kind === "skill" && asset.defaultScope === "user") {
|
|
658
|
+
throw new Error(
|
|
659
|
+
`User-scoped asset ${asset.id} must be linked at the user level, not installed into a project target.`
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
const sourcePath = join7(agentAssetsDir, asset.sourcePath);
|
|
663
|
+
const targetPath = resolveHostTargetPath(asset, host, placement);
|
|
664
|
+
const targetAbsolutePath = join7(targetDir, targetPath);
|
|
665
|
+
const targetExists = pathExistsEvenIfDanglingSymlink2(targetAbsolutePath);
|
|
666
|
+
if (targetExists) {
|
|
667
|
+
const stats = lstatSync2(targetAbsolutePath);
|
|
668
|
+
if (stats.isSymbolicLink() && managedTargets.has(targetPath)) {
|
|
669
|
+
return {
|
|
670
|
+
type: "update-symlink",
|
|
671
|
+
assetId: asset.id,
|
|
672
|
+
sourcePath,
|
|
673
|
+
targetPath
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
throw new Error(`Refusing to overwrite unmanaged target: ${targetPath}`);
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
type: "symlink",
|
|
680
|
+
assetId: asset.id,
|
|
681
|
+
sourcePath,
|
|
682
|
+
targetPath
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function resolveHostTargetPath(asset, host, placement) {
|
|
686
|
+
if (asset.kind === "skill") {
|
|
687
|
+
const effectivePlacement = resolveSkillPlacement(asset, placement);
|
|
688
|
+
if (host === "claude-code") {
|
|
689
|
+
if (placement === "manual") {
|
|
690
|
+
throw new Error("Manual skill placement is only supported for .agents hosts");
|
|
691
|
+
}
|
|
692
|
+
return `.claude/skills/${basename(asset.sourcePath)}`;
|
|
693
|
+
}
|
|
694
|
+
if (effectivePlacement === "manual") {
|
|
695
|
+
return `.agents/manual-skills/${basename(asset.sourcePath)}`;
|
|
696
|
+
}
|
|
697
|
+
return `.agents/skills/${basename(asset.sourcePath)}`;
|
|
698
|
+
}
|
|
699
|
+
if (asset.kind === "rule") {
|
|
700
|
+
return `.pro-gov/agent-assets/rules/${basename(asset.sourcePath)}`;
|
|
701
|
+
}
|
|
702
|
+
return `.pro-gov/agent-assets/commands/${basename(asset.sourcePath)}`;
|
|
703
|
+
}
|
|
704
|
+
function resolveSkillPlacement(asset, placement) {
|
|
705
|
+
if (placement !== "registry") return placement;
|
|
706
|
+
return asset.defaultPlacement ?? "auto";
|
|
707
|
+
}
|
|
708
|
+
function createDirectoryActions(actions) {
|
|
709
|
+
const directories = /* @__PURE__ */ new Set();
|
|
710
|
+
for (const action of actions) {
|
|
711
|
+
if (action.type === "create-dir") continue;
|
|
712
|
+
const directory = dirname3(action.targetPath);
|
|
713
|
+
if (directory !== ".") directories.add(directory);
|
|
714
|
+
}
|
|
715
|
+
return [...directories].sort().map((targetPath) => ({ type: "create-dir", targetPath }));
|
|
716
|
+
}
|
|
717
|
+
function readManagedEntries(targetDir) {
|
|
718
|
+
const lockfilePath = join7(targetDir, ".pro-gov/assets.lock.json");
|
|
719
|
+
if (!existsSync7(lockfilePath)) return [];
|
|
720
|
+
try {
|
|
721
|
+
const lockfile = JSON.parse(readFileSync4(lockfilePath, "utf8"));
|
|
722
|
+
return (lockfile.assets ?? []).filter(
|
|
723
|
+
(entry) => typeof entry.id === "string" && typeof entry.sourcePath === "string" && typeof entry.targetPath === "string"
|
|
724
|
+
);
|
|
725
|
+
} catch {
|
|
726
|
+
return [];
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function createRemovalActions(targetDir, agentAssetsDir, managedEntries, expectedTargetPaths) {
|
|
730
|
+
const actions = [];
|
|
731
|
+
for (const entry of managedEntries) {
|
|
732
|
+
if (expectedTargetPaths.has(entry.targetPath)) continue;
|
|
733
|
+
if (!isManagedAssetTargetPath(entry.targetPath)) {
|
|
734
|
+
throw new Error(`Refusing to remove managed asset outside supported roots: ${entry.targetPath}`);
|
|
735
|
+
}
|
|
736
|
+
const targetAbsolutePath = join7(targetDir, entry.targetPath);
|
|
737
|
+
if (!pathExistsEvenIfDanglingSymlink2(targetAbsolutePath)) continue;
|
|
738
|
+
const stats = lstatSync2(targetAbsolutePath);
|
|
739
|
+
if (!stats.isSymbolicLink()) {
|
|
740
|
+
throw new Error(`Refusing to remove path that is no longer a managed symlink: ${entry.targetPath}`);
|
|
741
|
+
}
|
|
742
|
+
const expectedSourcePath = join7(agentAssetsDir, entry.sourcePath);
|
|
743
|
+
const actualSourcePath = resolve(dirname3(targetAbsolutePath), readlinkSync(targetAbsolutePath));
|
|
744
|
+
if (actualSourcePath !== resolve(expectedSourcePath)) {
|
|
745
|
+
throw new Error(`Refusing to remove managed symlink with changed target: ${entry.targetPath}`);
|
|
746
|
+
}
|
|
747
|
+
actions.push({
|
|
748
|
+
type: "remove-symlink",
|
|
749
|
+
assetId: entry.id,
|
|
750
|
+
expectedSourcePath,
|
|
751
|
+
targetPath: entry.targetPath
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
return actions.sort((a, b) => a.targetPath.localeCompare(b.targetPath));
|
|
755
|
+
}
|
|
756
|
+
function isManagedAssetTargetPath(path) {
|
|
757
|
+
return [
|
|
758
|
+
".agents/skills/",
|
|
759
|
+
".agents/manual-skills/",
|
|
760
|
+
".claude/skills/",
|
|
761
|
+
".pro-gov/agent-assets/rules/",
|
|
762
|
+
".pro-gov/agent-assets/commands/"
|
|
763
|
+
].some((prefix) => path.startsWith(prefix));
|
|
764
|
+
}
|
|
765
|
+
function pathExistsEvenIfDanglingSymlink2(path) {
|
|
766
|
+
try {
|
|
767
|
+
lstatSync2(path);
|
|
768
|
+
return true;
|
|
769
|
+
} catch {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/asset-targets/apply.ts
|
|
541
775
|
function applyAssetInstallPlan(plan) {
|
|
542
776
|
const appliedActions = [];
|
|
543
777
|
for (const action of plan.actions) {
|
|
@@ -547,52 +781,71 @@ function applyAssetInstallPlan(plan) {
|
|
|
547
781
|
return { appliedActions };
|
|
548
782
|
}
|
|
549
783
|
function applyAction(targetDir, action) {
|
|
550
|
-
const targetAbsolutePath =
|
|
784
|
+
const targetAbsolutePath = join8(targetDir, action.targetPath);
|
|
785
|
+
if (action.type === "remove-symlink") {
|
|
786
|
+
removeManagedSymlink(targetAbsolutePath, action);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
551
789
|
if (action.type === "create-dir") {
|
|
552
790
|
mkdirSync2(targetAbsolutePath, { recursive: true });
|
|
553
791
|
return;
|
|
554
792
|
}
|
|
555
793
|
if (action.type === "write-file") {
|
|
556
|
-
mkdirSync2(
|
|
794
|
+
mkdirSync2(dirname4(targetAbsolutePath), { recursive: true });
|
|
557
795
|
writeFileSync(targetAbsolutePath, action.content);
|
|
558
796
|
return;
|
|
559
797
|
}
|
|
560
|
-
mkdirSync2(
|
|
561
|
-
const sourceAbsolutePath =
|
|
562
|
-
const symlinkTarget = relative4(realpathSync(
|
|
798
|
+
mkdirSync2(dirname4(targetAbsolutePath), { recursive: true });
|
|
799
|
+
const sourceAbsolutePath = resolve2(action.sourcePath);
|
|
800
|
+
const symlinkTarget = relative4(realpathSync(dirname4(targetAbsolutePath)), realpathSync(sourceAbsolutePath)) || ".";
|
|
563
801
|
if (action.type === "symlink") {
|
|
564
|
-
if (
|
|
802
|
+
if (pathExistsEvenIfDanglingSymlink3(targetAbsolutePath)) {
|
|
565
803
|
throw new Error(`Refusing to overwrite unmanaged target: ${action.targetPath}`);
|
|
566
804
|
}
|
|
567
805
|
symlinkSync(symlinkTarget, targetAbsolutePath);
|
|
568
806
|
return;
|
|
569
807
|
}
|
|
570
|
-
if (!
|
|
808
|
+
if (!pathExistsEvenIfDanglingSymlink3(targetAbsolutePath)) {
|
|
571
809
|
symlinkSync(symlinkTarget, targetAbsolutePath);
|
|
572
810
|
return;
|
|
573
811
|
}
|
|
574
|
-
const stats =
|
|
812
|
+
const stats = lstatSync3(targetAbsolutePath);
|
|
575
813
|
if (!stats.isSymbolicLink()) {
|
|
576
814
|
throw new Error(`Refusing to overwrite unmanaged target: ${action.targetPath}`);
|
|
577
815
|
}
|
|
578
816
|
unlinkSync(targetAbsolutePath);
|
|
579
817
|
symlinkSync(symlinkTarget, targetAbsolutePath);
|
|
580
818
|
}
|
|
581
|
-
function
|
|
819
|
+
function removeManagedSymlink(targetAbsolutePath, action) {
|
|
820
|
+
if (!isManagedAssetTargetPath(action.targetPath)) {
|
|
821
|
+
throw new Error(`Refusing to remove managed asset outside supported roots: ${action.targetPath}`);
|
|
822
|
+
}
|
|
823
|
+
if (!pathExistsEvenIfDanglingSymlink3(targetAbsolutePath)) return;
|
|
824
|
+
const stats = lstatSync3(targetAbsolutePath);
|
|
825
|
+
if (!stats.isSymbolicLink()) {
|
|
826
|
+
throw new Error(`Refusing to remove path that is no longer a managed symlink: ${action.targetPath}`);
|
|
827
|
+
}
|
|
828
|
+
const actualSourcePath = resolve2(dirname4(targetAbsolutePath), readlinkSync2(targetAbsolutePath));
|
|
829
|
+
if (actualSourcePath !== resolve2(action.expectedSourcePath)) {
|
|
830
|
+
throw new Error(`Refusing to remove managed symlink with changed target: ${action.targetPath}`);
|
|
831
|
+
}
|
|
832
|
+
unlinkSync(targetAbsolutePath);
|
|
833
|
+
}
|
|
834
|
+
function pathExistsEvenIfDanglingSymlink3(path) {
|
|
582
835
|
try {
|
|
583
|
-
|
|
836
|
+
lstatSync3(path);
|
|
584
837
|
return true;
|
|
585
838
|
} catch {
|
|
586
|
-
return
|
|
839
|
+
return existsSync8(path);
|
|
587
840
|
}
|
|
588
841
|
}
|
|
589
842
|
|
|
590
843
|
// src/asset-targets/check.ts
|
|
591
|
-
import { existsSync as
|
|
592
|
-
import { basename, join as
|
|
844
|
+
import { existsSync as existsSync9, lstatSync as lstatSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
845
|
+
import { basename as basename2, join as join9 } from "node:path";
|
|
593
846
|
function checkInstalledAssets(options) {
|
|
594
|
-
const lockfilePath =
|
|
595
|
-
if (!
|
|
847
|
+
const lockfilePath = join9(options.targetDir, ".pro-gov/assets.lock.json");
|
|
848
|
+
if (!existsSync9(lockfilePath)) {
|
|
596
849
|
return {
|
|
597
850
|
targetDir: options.targetDir,
|
|
598
851
|
issues: [
|
|
@@ -604,12 +857,12 @@ function checkInstalledAssets(options) {
|
|
|
604
857
|
};
|
|
605
858
|
}
|
|
606
859
|
const registryById = new Map(options.registry.assets.map((asset) => [asset.id, asset]));
|
|
607
|
-
const lockfile = JSON.parse(
|
|
860
|
+
const lockfile = JSON.parse(readFileSync5(lockfilePath, "utf8"));
|
|
608
861
|
const issues = [];
|
|
609
862
|
const strictRegistry = options.strictRegistry ?? false;
|
|
610
863
|
for (const entry of lockfile.assets ?? []) {
|
|
611
864
|
const asset = registryById.get(entry.id);
|
|
612
|
-
const targetAbsolutePath =
|
|
865
|
+
const targetAbsolutePath = join9(options.targetDir, entry.targetPath);
|
|
613
866
|
if (!asset && strictRegistry) {
|
|
614
867
|
issues.push({
|
|
615
868
|
type: "unknown-asset",
|
|
@@ -636,7 +889,7 @@ function checkInstalledAssets(options) {
|
|
|
636
889
|
issues.push(placementDriftIssue);
|
|
637
890
|
}
|
|
638
891
|
}
|
|
639
|
-
if (!
|
|
892
|
+
if (!pathExistsEvenIfDanglingSymlink4(targetAbsolutePath)) {
|
|
640
893
|
issues.push({
|
|
641
894
|
type: "missing-target",
|
|
642
895
|
id: entry.id,
|
|
@@ -645,7 +898,7 @@ function checkInstalledAssets(options) {
|
|
|
645
898
|
});
|
|
646
899
|
continue;
|
|
647
900
|
}
|
|
648
|
-
const targetStats =
|
|
901
|
+
const targetStats = lstatSync4(targetAbsolutePath);
|
|
649
902
|
if (!targetStats.isSymbolicLink()) {
|
|
650
903
|
issues.push({
|
|
651
904
|
type: "unmanaged-conflict",
|
|
@@ -655,7 +908,7 @@ function checkInstalledAssets(options) {
|
|
|
655
908
|
});
|
|
656
909
|
continue;
|
|
657
910
|
}
|
|
658
|
-
if (!
|
|
911
|
+
if (!existsSync9(targetAbsolutePath)) {
|
|
659
912
|
issues.push({
|
|
660
913
|
type: "dangling-symlink",
|
|
661
914
|
id: entry.id,
|
|
@@ -675,8 +928,8 @@ function checkInstalledAssets(options) {
|
|
|
675
928
|
});
|
|
676
929
|
}
|
|
677
930
|
if (!asset || !strictRegistry) continue;
|
|
678
|
-
const sourceAbsolutePath =
|
|
679
|
-
if (!
|
|
931
|
+
const sourceAbsolutePath = join9(options.agentAssetsDir, asset.sourcePath);
|
|
932
|
+
if (!existsSync9(sourceAbsolutePath)) {
|
|
680
933
|
issues.push({
|
|
681
934
|
type: "missing-source",
|
|
682
935
|
id: entry.id,
|
|
@@ -714,10 +967,10 @@ function checkDuplicateSkillPlacements(targetDir, registry) {
|
|
|
714
967
|
const issues = [];
|
|
715
968
|
for (const asset of registry.assets) {
|
|
716
969
|
if (asset.kind !== "skill") continue;
|
|
717
|
-
const skillName =
|
|
970
|
+
const skillName = basename2(asset.sourcePath);
|
|
718
971
|
const autoPath = `.agents/skills/${skillName}`;
|
|
719
972
|
const manualPath = `.agents/manual-skills/${skillName}`;
|
|
720
|
-
if (
|
|
973
|
+
if (pathExistsEvenIfDanglingSymlink4(join9(targetDir, autoPath)) && pathExistsEvenIfDanglingSymlink4(join9(targetDir, manualPath))) {
|
|
721
974
|
issues.push({
|
|
722
975
|
type: "duplicate-skill-placement",
|
|
723
976
|
id: asset.id,
|
|
@@ -757,7 +1010,7 @@ function expectedSkillTargetPrefixes(host) {
|
|
|
757
1010
|
return void 0;
|
|
758
1011
|
}
|
|
759
1012
|
function expectedRegistrySkillTargetPath(host, sourcePath, placement) {
|
|
760
|
-
const skillName =
|
|
1013
|
+
const skillName = basename2(sourcePath);
|
|
761
1014
|
if (host === "claude-code") {
|
|
762
1015
|
return placement === "manual" ? void 0 : `.claude/skills/${skillName}`;
|
|
763
1016
|
}
|
|
@@ -766,163 +1019,6 @@ function expectedRegistrySkillTargetPath(host, sourcePath, placement) {
|
|
|
766
1019
|
}
|
|
767
1020
|
return void 0;
|
|
768
1021
|
}
|
|
769
|
-
function pathExistsEvenIfDanglingSymlink3(path) {
|
|
770
|
-
try {
|
|
771
|
-
lstatSync3(path);
|
|
772
|
-
return true;
|
|
773
|
-
} catch {
|
|
774
|
-
return false;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// src/asset-targets/install-plan.ts
|
|
779
|
-
import { existsSync as existsSync9, lstatSync as lstatSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
780
|
-
import { basename as basename2, dirname as dirname4, join as join9 } from "node:path";
|
|
781
|
-
function createAssetInstallPlan(options) {
|
|
782
|
-
const placement = options.placement ?? "registry";
|
|
783
|
-
const assetsById = new Map(options.registry.assets.map((asset) => [asset.id, asset]));
|
|
784
|
-
const bundlesById = new Map(options.bundles.map((bundle) => [bundle.id, bundle]));
|
|
785
|
-
const assetIds = resolveBundleAssetIds(options.bundleIds, bundlesById);
|
|
786
|
-
const assets = assetIds.map((assetId) => {
|
|
787
|
-
const asset = assetsById.get(assetId);
|
|
788
|
-
if (!asset) throw new Error(`Unknown asset id in bundle: ${assetId}`);
|
|
789
|
-
if (asset.kind === "skill" && !asset.hosts.includes(options.host)) {
|
|
790
|
-
throw new Error(`Asset ${assetId} does not support host ${options.host}`);
|
|
791
|
-
}
|
|
792
|
-
return asset;
|
|
793
|
-
});
|
|
794
|
-
const lockEntries = createAgentAssetLockEntries(options.registry, options.agentAssetsDir, assetIds);
|
|
795
|
-
const managedTargets = readManagedTargets(options.targetDir);
|
|
796
|
-
const assetActions = assets.map(
|
|
797
|
-
(asset) => createAssetAction(asset, options.agentAssetsDir, options.targetDir, options.host, placement, managedTargets)
|
|
798
|
-
);
|
|
799
|
-
const manifest = {
|
|
800
|
-
schemaVersion: 1,
|
|
801
|
-
host: options.host,
|
|
802
|
-
placement,
|
|
803
|
-
bundleIds: [...options.bundleIds],
|
|
804
|
-
assetIds
|
|
805
|
-
};
|
|
806
|
-
const lockfile = {
|
|
807
|
-
schemaVersion: 1,
|
|
808
|
-
host: options.host,
|
|
809
|
-
placement,
|
|
810
|
-
bundleIds: [...options.bundleIds],
|
|
811
|
-
assets: lockEntries.map((entry) => {
|
|
812
|
-
const action = assetActions.find((candidate) => "assetId" in candidate && candidate.assetId === entry.id);
|
|
813
|
-
return {
|
|
814
|
-
...entry,
|
|
815
|
-
targetPath: action && "targetPath" in action ? action.targetPath : ""
|
|
816
|
-
};
|
|
817
|
-
})
|
|
818
|
-
};
|
|
819
|
-
const writeActions = [
|
|
820
|
-
{
|
|
821
|
-
type: "write-file",
|
|
822
|
-
targetPath: ".pro-gov/assets.json",
|
|
823
|
-
content: `${JSON.stringify(manifest, null, 2)}
|
|
824
|
-
`
|
|
825
|
-
},
|
|
826
|
-
{
|
|
827
|
-
type: "write-file",
|
|
828
|
-
targetPath: ".pro-gov/assets.lock.json",
|
|
829
|
-
content: `${JSON.stringify(lockfile, null, 2)}
|
|
830
|
-
`
|
|
831
|
-
}
|
|
832
|
-
];
|
|
833
|
-
return {
|
|
834
|
-
schemaVersion: 1,
|
|
835
|
-
dryRun: true,
|
|
836
|
-
targetDir: options.targetDir,
|
|
837
|
-
host: options.host,
|
|
838
|
-
placement,
|
|
839
|
-
bundleIds: [...options.bundleIds],
|
|
840
|
-
assetIds,
|
|
841
|
-
actions: [...createDirectoryActions([...assetActions, ...writeActions]), ...assetActions, ...writeActions]
|
|
842
|
-
};
|
|
843
|
-
}
|
|
844
|
-
function resolveBundleAssetIds(bundleIds, bundlesById) {
|
|
845
|
-
const ids = /* @__PURE__ */ new Set();
|
|
846
|
-
for (const bundleId of bundleIds) {
|
|
847
|
-
const bundle = bundlesById.get(bundleId);
|
|
848
|
-
if (!bundle) throw new Error(`Unknown bundle id: ${bundleId}`);
|
|
849
|
-
for (const assetId of bundle.assets) {
|
|
850
|
-
ids.add(assetId);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
return [...ids].sort();
|
|
854
|
-
}
|
|
855
|
-
function createAssetAction(asset, agentAssetsDir, targetDir, host, placement, managedTargets) {
|
|
856
|
-
if (asset.kind === "skill" && asset.defaultScope === "user") {
|
|
857
|
-
throw new Error(
|
|
858
|
-
`User-scoped asset ${asset.id} must be linked at the user level, not installed into a project target.`
|
|
859
|
-
);
|
|
860
|
-
}
|
|
861
|
-
const sourcePath = join9(agentAssetsDir, asset.sourcePath);
|
|
862
|
-
const targetPath = resolveHostTargetPath(asset, host, placement);
|
|
863
|
-
const targetAbsolutePath = join9(targetDir, targetPath);
|
|
864
|
-
const targetExists = pathExistsEvenIfDanglingSymlink4(targetAbsolutePath);
|
|
865
|
-
if (targetExists) {
|
|
866
|
-
const stats = lstatSync4(targetAbsolutePath);
|
|
867
|
-
if (stats.isSymbolicLink() && managedTargets.has(targetPath)) {
|
|
868
|
-
return {
|
|
869
|
-
type: "update-symlink",
|
|
870
|
-
assetId: asset.id,
|
|
871
|
-
sourcePath,
|
|
872
|
-
targetPath
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
throw new Error(`Refusing to overwrite unmanaged target: ${targetPath}`);
|
|
876
|
-
}
|
|
877
|
-
return {
|
|
878
|
-
type: "symlink",
|
|
879
|
-
assetId: asset.id,
|
|
880
|
-
sourcePath,
|
|
881
|
-
targetPath
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
function resolveHostTargetPath(asset, host, placement) {
|
|
885
|
-
if (asset.kind === "skill") {
|
|
886
|
-
const effectivePlacement = resolveSkillPlacement(asset, placement);
|
|
887
|
-
if (host === "claude-code") {
|
|
888
|
-
if (placement === "manual") {
|
|
889
|
-
throw new Error("Manual skill placement is only supported for .agents hosts");
|
|
890
|
-
}
|
|
891
|
-
return `.claude/skills/${basename2(asset.sourcePath)}`;
|
|
892
|
-
}
|
|
893
|
-
if (effectivePlacement === "manual") {
|
|
894
|
-
return `.agents/manual-skills/${basename2(asset.sourcePath)}`;
|
|
895
|
-
}
|
|
896
|
-
return `.agents/skills/${basename2(asset.sourcePath)}`;
|
|
897
|
-
}
|
|
898
|
-
if (asset.kind === "rule") {
|
|
899
|
-
return `.pro-gov/agent-assets/rules/${basename2(asset.sourcePath)}`;
|
|
900
|
-
}
|
|
901
|
-
return `.pro-gov/agent-assets/commands/${basename2(asset.sourcePath)}`;
|
|
902
|
-
}
|
|
903
|
-
function resolveSkillPlacement(asset, placement) {
|
|
904
|
-
if (placement !== "registry") return placement;
|
|
905
|
-
return asset.defaultPlacement ?? "auto";
|
|
906
|
-
}
|
|
907
|
-
function createDirectoryActions(actions) {
|
|
908
|
-
const directories = /* @__PURE__ */ new Set();
|
|
909
|
-
for (const action of actions) {
|
|
910
|
-
if (action.type === "create-dir") continue;
|
|
911
|
-
const directory = dirname4(action.targetPath);
|
|
912
|
-
if (directory !== ".") directories.add(directory);
|
|
913
|
-
}
|
|
914
|
-
return [...directories].sort().map((targetPath) => ({ type: "create-dir", targetPath }));
|
|
915
|
-
}
|
|
916
|
-
function readManagedTargets(targetDir) {
|
|
917
|
-
const lockfilePath = join9(targetDir, ".pro-gov/assets.lock.json");
|
|
918
|
-
if (!existsSync9(lockfilePath)) return /* @__PURE__ */ new Set();
|
|
919
|
-
try {
|
|
920
|
-
const lockfile = JSON.parse(readFileSync5(lockfilePath, "utf8"));
|
|
921
|
-
return new Set((lockfile.assets ?? []).map((asset) => asset.targetPath).filter(Boolean));
|
|
922
|
-
} catch {
|
|
923
|
-
return /* @__PURE__ */ new Set();
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
1022
|
function pathExistsEvenIfDanglingSymlink4(path) {
|
|
927
1023
|
try {
|
|
928
1024
|
lstatSync4(path);
|
|
@@ -1642,7 +1738,7 @@ function isEngineeringRuntimeProject(root) {
|
|
|
1642
1738
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1643
1739
|
import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1644
1740
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
1645
|
-
import { basename as basename3, join as join13, resolve as
|
|
1741
|
+
import { basename as basename3, join as join13, resolve as resolve3 } from "node:path";
|
|
1646
1742
|
|
|
1647
1743
|
// src/host-hooks/host-hook-runner.ts
|
|
1648
1744
|
import { closeSync, openSync, readFileSync as readFileSync9, readSync, statSync as statSync3 } from "node:fs";
|
|
@@ -1967,7 +2063,7 @@ function defaultDebugDir() {
|
|
|
1967
2063
|
});
|
|
1968
2064
|
const value = gitPath.status === 0 ? gitPath.stdout.trim() : "";
|
|
1969
2065
|
if (value) {
|
|
1970
|
-
return
|
|
2066
|
+
return resolve3(process.cwd(), value);
|
|
1971
2067
|
}
|
|
1972
2068
|
return join13(tmpdir2(), "pro-gov-hook-debug", basename3(process.cwd()));
|
|
1973
2069
|
}
|
|
@@ -1983,8 +2079,8 @@ function readPackageVersion() {
|
|
|
1983
2079
|
}
|
|
1984
2080
|
function resolvePackageJsonPath() {
|
|
1985
2081
|
const candidates = [
|
|
1986
|
-
|
|
1987
|
-
|
|
2082
|
+
resolve3(process.cwd(), "packages/pro-gov/package.json"),
|
|
2083
|
+
resolve3(process.cwd(), "node_modules/@pieai/pro-gov/package.json")
|
|
1988
2084
|
];
|
|
1989
2085
|
return candidates.find((candidate) => existsSync13(candidate));
|
|
1990
2086
|
}
|
|
@@ -2076,36 +2172,253 @@ function applyStarterFiles(files, profile) {
|
|
|
2076
2172
|
console.error("No files were written. Use --dry-run and migrate existing files deliberately.");
|
|
2077
2173
|
return 1;
|
|
2078
2174
|
}
|
|
2079
|
-
for (const file of files) {
|
|
2080
|
-
const targetPath = join14(root, file.targetPath);
|
|
2081
|
-
mkdirSync5(dirname7(targetPath), { recursive: true });
|
|
2082
|
-
const source = readFileSync11(file.absoluteSourcePath);
|
|
2083
|
-
const content = file.targetPath === "AGENTS.md" ? renderAgentsTemplate(source.toString("utf8"), basename4(root), profile) : source;
|
|
2084
|
-
writeFileSync4(targetPath, content);
|
|
2175
|
+
for (const file of files) {
|
|
2176
|
+
const targetPath = join14(root, file.targetPath);
|
|
2177
|
+
mkdirSync5(dirname7(targetPath), { recursive: true });
|
|
2178
|
+
const source = readFileSync11(file.absoluteSourcePath);
|
|
2179
|
+
const content = file.targetPath === "AGENTS.md" ? renderAgentsTemplate(source.toString("utf8"), basename4(root), profile) : source;
|
|
2180
|
+
writeFileSync4(targetPath, content);
|
|
2181
|
+
}
|
|
2182
|
+
console.log("pro-gov init APPLIED");
|
|
2183
|
+
console.log(`profile: ${profile}`);
|
|
2184
|
+
console.log(`created-files: ${files.length}`);
|
|
2185
|
+
console.log("Existing project files were not overwritten.");
|
|
2186
|
+
console.log("Next: customize project-local policy/current-work, run doc-gov scan, then run doc-gov doctor.");
|
|
2187
|
+
return 0;
|
|
2188
|
+
}
|
|
2189
|
+
function renderAgentsTemplate(template, projectName, profile) {
|
|
2190
|
+
const selectedRoute = `docs/governance/agents-routing/${profile}-v0.9.md`;
|
|
2191
|
+
return template.replace("# PROJECT_NAME AI Router", `# ${projectName} AI Router`).replace(
|
|
2192
|
+
/6\. The selected agents routing file:\n - `docs\/governance\/agents-routing\/engineering-runtime-v0\.9\.md`, or\n - `docs\/governance\/agents-routing\/doc-only-v0\.9\.md`/,
|
|
2193
|
+
`6. The selected agents routing file: \`${selectedRoute}\``
|
|
2194
|
+
).replace(
|
|
2195
|
+
"- Name this project's adopted profile: `engineering-runtime` or `doc-only`.",
|
|
2196
|
+
`- This project adopts the \`${profile}\` profile.`
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
function readFlag(args, flag) {
|
|
2200
|
+
const index = args.indexOf(flag);
|
|
2201
|
+
if (index === -1) return null;
|
|
2202
|
+
const value = args[index + 1];
|
|
2203
|
+
if (!value || value.startsWith("--")) return null;
|
|
2204
|
+
return value;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// src/learning/recall.ts
|
|
2208
|
+
import { existsSync as existsSync15, readdirSync as readdirSync6, readFileSync as readFileSync12 } from "node:fs";
|
|
2209
|
+
import { basename as basename5, join as join15, relative as relative5 } from "node:path";
|
|
2210
|
+
function recallLearnings(root, options) {
|
|
2211
|
+
const query = options.query.trim();
|
|
2212
|
+
const terms = tokenize(query);
|
|
2213
|
+
const limit = Math.max(1, options.limit ?? 5);
|
|
2214
|
+
if (terms.length === 0) {
|
|
2215
|
+
return { query, hits: [] };
|
|
2216
|
+
}
|
|
2217
|
+
const records = loadLearningRecords(root);
|
|
2218
|
+
const hits = records.map((record) => {
|
|
2219
|
+
const score = scoreRecord(record, terms);
|
|
2220
|
+
return {
|
|
2221
|
+
relativePath: record.relativePath,
|
|
2222
|
+
title: record.title,
|
|
2223
|
+
score,
|
|
2224
|
+
summary: summarizeRecord(record, terms)
|
|
2225
|
+
};
|
|
2226
|
+
}).filter((hit) => hit.score > 0).sort((a, b) => b.score - a.score || a.relativePath.localeCompare(b.relativePath)).slice(0, limit);
|
|
2227
|
+
return { query, hits };
|
|
2228
|
+
}
|
|
2229
|
+
function loadLearningRecords(root) {
|
|
2230
|
+
const records = [];
|
|
2231
|
+
const solutionsDir = join15(root, "docs/solutions");
|
|
2232
|
+
if (existsSync15(solutionsDir)) {
|
|
2233
|
+
for (const path of listMarkdownFiles(solutionsDir)) {
|
|
2234
|
+
records.push(readLearningRecord(root, path));
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
const conceptsPath = join15(root, "CONCEPTS.md");
|
|
2238
|
+
if (existsSync15(conceptsPath)) {
|
|
2239
|
+
records.push(readLearningRecord(root, conceptsPath));
|
|
2240
|
+
}
|
|
2241
|
+
return records;
|
|
2242
|
+
}
|
|
2243
|
+
function listMarkdownFiles(dir) {
|
|
2244
|
+
const files = [];
|
|
2245
|
+
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
2246
|
+
const absolutePath = join15(dir, entry.name);
|
|
2247
|
+
if (entry.isDirectory()) {
|
|
2248
|
+
files.push(...listMarkdownFiles(absolutePath));
|
|
2249
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2250
|
+
files.push(absolutePath);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
return files.sort();
|
|
2254
|
+
}
|
|
2255
|
+
function readLearningRecord(root, absolutePath) {
|
|
2256
|
+
const content = readFileSync12(absolutePath, "utf8");
|
|
2257
|
+
const parsed = splitFrontmatter(content);
|
|
2258
|
+
const body = parsed.body;
|
|
2259
|
+
return {
|
|
2260
|
+
relativePath: normalizePath(relative5(root, absolutePath)),
|
|
2261
|
+
title: findTitle(parsed.frontmatter, body) ?? titleFromPath(absolutePath),
|
|
2262
|
+
metadata: parsed.frontmatter,
|
|
2263
|
+
body
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
function splitFrontmatter(content) {
|
|
2267
|
+
if (!content.startsWith("---\n")) {
|
|
2268
|
+
return { frontmatter: "", body: content };
|
|
2269
|
+
}
|
|
2270
|
+
const end = content.indexOf("\n---", 4);
|
|
2271
|
+
if (end === -1) {
|
|
2272
|
+
return { frontmatter: "", body: content };
|
|
2273
|
+
}
|
|
2274
|
+
return {
|
|
2275
|
+
frontmatter: content.slice(4, end).trim(),
|
|
2276
|
+
body: content.slice(end + "\n---".length).trim()
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
function findTitle(frontmatter, body) {
|
|
2280
|
+
const titleLine = frontmatter.split(/\r?\n/).find((line) => line.trim().toLowerCase().startsWith("title:"));
|
|
2281
|
+
if (titleLine) {
|
|
2282
|
+
return titleLine.slice(titleLine.indexOf(":") + 1).trim().replace(/^["']|["']$/g, "");
|
|
2283
|
+
}
|
|
2284
|
+
const heading = body.split(/\r?\n/).find((line) => line.startsWith("# "));
|
|
2285
|
+
return heading ? heading.slice(2).trim() : void 0;
|
|
2286
|
+
}
|
|
2287
|
+
function titleFromPath(path) {
|
|
2288
|
+
return basename5(path, ".md").split(/[-_]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2289
|
+
}
|
|
2290
|
+
function scoreRecord(record, terms) {
|
|
2291
|
+
const title = record.title.toLowerCase();
|
|
2292
|
+
const metadata = record.metadata.toLowerCase();
|
|
2293
|
+
const path = record.relativePath.toLowerCase();
|
|
2294
|
+
const body = record.body.toLowerCase();
|
|
2295
|
+
let score = 0;
|
|
2296
|
+
for (const term of terms) {
|
|
2297
|
+
if (title.includes(term)) score += 6;
|
|
2298
|
+
if (metadata.includes(term)) score += 4;
|
|
2299
|
+
if (path.includes(term)) score += 2;
|
|
2300
|
+
if (body.includes(term)) score += 1;
|
|
2301
|
+
}
|
|
2302
|
+
return score;
|
|
2303
|
+
}
|
|
2304
|
+
function summarizeRecord(record, terms) {
|
|
2305
|
+
const lines = record.body.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
2306
|
+
const best = lines.map((line, index) => ({
|
|
2307
|
+
line,
|
|
2308
|
+
index,
|
|
2309
|
+
score: countTermMatches(line, terms) + (line.startsWith("#") ? 0.5 : 0)
|
|
2310
|
+
})).filter((candidate) => candidate.score > 0).sort((a, b) => b.score - a.score || a.index - b.index)[0];
|
|
2311
|
+
if (!best) {
|
|
2312
|
+
return truncate(cleanMarkdownLine(lines[0] ?? record.title), 180);
|
|
2313
|
+
}
|
|
2314
|
+
const cleaned = cleanMarkdownLine(best.line);
|
|
2315
|
+
if (!best.line.startsWith("#")) {
|
|
2316
|
+
return truncate(cleaned, 180);
|
|
2317
|
+
}
|
|
2318
|
+
const nextLine = lines.slice(best.index + 1).find((line) => !line.startsWith("#"));
|
|
2319
|
+
return truncate([cleaned, nextLine].filter(Boolean).join(" - "), 180);
|
|
2320
|
+
}
|
|
2321
|
+
function tokenize(input) {
|
|
2322
|
+
return [...new Set(input.toLowerCase().match(/[a-z0-9\u4e00-\u9fff]+/g) ?? [])].filter((term) => term.length > 1);
|
|
2323
|
+
}
|
|
2324
|
+
function truncate(input, maxLength) {
|
|
2325
|
+
return input.length <= maxLength ? input : `${input.slice(0, maxLength - 1)}\u2026`;
|
|
2326
|
+
}
|
|
2327
|
+
function normalizePath(path) {
|
|
2328
|
+
return path.split("\\").join("/");
|
|
2329
|
+
}
|
|
2330
|
+
function countTermMatches(input, terms) {
|
|
2331
|
+
const lower = input.toLowerCase();
|
|
2332
|
+
return terms.filter((term) => lower.includes(term)).length;
|
|
2333
|
+
}
|
|
2334
|
+
function cleanMarkdownLine(input) {
|
|
2335
|
+
return input.replace(/^#+\s*/, "").trim();
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
// src/commands/learn.ts
|
|
2339
|
+
function runLearn(args) {
|
|
2340
|
+
const [subcommand2, ...rest] = args;
|
|
2341
|
+
if (subcommand2 === "recall") return runLearnRecall(rest);
|
|
2342
|
+
printUsage2();
|
|
2343
|
+
return 1;
|
|
2344
|
+
}
|
|
2345
|
+
function runLearnRecall(args) {
|
|
2346
|
+
const options = parseRecallOptions(args);
|
|
2347
|
+
if (!options.ok) {
|
|
2348
|
+
console.error(options.error);
|
|
2349
|
+
printUsage2();
|
|
2350
|
+
return 1;
|
|
2351
|
+
}
|
|
2352
|
+
const result = recallLearnings(options.value.targetDir, {
|
|
2353
|
+
query: options.value.query,
|
|
2354
|
+
limit: options.value.limit
|
|
2355
|
+
});
|
|
2356
|
+
if (options.value.json) {
|
|
2357
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2358
|
+
return 0;
|
|
2359
|
+
}
|
|
2360
|
+
console.log(`Learning Recall: ${result.hits.length} hit${result.hits.length === 1 ? "" : "s"}`);
|
|
2361
|
+
for (const hit of result.hits) {
|
|
2362
|
+
console.log(`${hit.relativePath} ${hit.score} ${hit.title}`);
|
|
2363
|
+
if (hit.summary) console.log(` ${hit.summary}`);
|
|
2364
|
+
}
|
|
2365
|
+
if (result.hits.length === 0) {
|
|
2366
|
+
console.log("No relevant learning records found.");
|
|
2085
2367
|
}
|
|
2086
|
-
console.log("pro-gov init APPLIED");
|
|
2087
|
-
console.log(`profile: ${profile}`);
|
|
2088
|
-
console.log(`created-files: ${files.length}`);
|
|
2089
|
-
console.log("Existing project files were not overwritten.");
|
|
2090
|
-
console.log("Next: customize project-local policy/current-work, run doc-gov scan, then run doc-gov doctor.");
|
|
2091
2368
|
return 0;
|
|
2092
2369
|
}
|
|
2093
|
-
function
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2370
|
+
function parseRecallOptions(args) {
|
|
2371
|
+
let targetDir = process.cwd();
|
|
2372
|
+
let query = "";
|
|
2373
|
+
let limit = 5;
|
|
2374
|
+
let json = false;
|
|
2375
|
+
const positional = [];
|
|
2376
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2377
|
+
const arg = args[index];
|
|
2378
|
+
if (arg === "--target") {
|
|
2379
|
+
const value = readFlagValue(args, index, "--target");
|
|
2380
|
+
if (!value.ok) return value;
|
|
2381
|
+
targetDir = value.value;
|
|
2382
|
+
index += 1;
|
|
2383
|
+
} else if (arg === "--query") {
|
|
2384
|
+
const value = readFlagValue(args, index, "--query");
|
|
2385
|
+
if (!value.ok) return value;
|
|
2386
|
+
query = value.value;
|
|
2387
|
+
index += 1;
|
|
2388
|
+
} else if (arg === "--limit") {
|
|
2389
|
+
const parsed = readFlagValue(args, index, "--limit");
|
|
2390
|
+
if (!parsed.ok) return parsed;
|
|
2391
|
+
const value = Number.parseInt(parsed.value, 10);
|
|
2392
|
+
if (!Number.isFinite(value) || value < 1) return { ok: false, error: "--limit must be a positive integer" };
|
|
2393
|
+
limit = value;
|
|
2394
|
+
index += 1;
|
|
2395
|
+
} else if (arg === "--json") {
|
|
2396
|
+
json = true;
|
|
2397
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
2398
|
+
return { ok: false, error: "Usage requested" };
|
|
2399
|
+
} else if (arg.startsWith("--")) {
|
|
2400
|
+
return { ok: false, error: `Unknown option: ${arg}` };
|
|
2401
|
+
} else {
|
|
2402
|
+
positional.push(arg);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
if (!query && positional.length > 0) {
|
|
2406
|
+
query = positional.join(" ");
|
|
2407
|
+
}
|
|
2408
|
+
if (!query.trim()) {
|
|
2409
|
+
return { ok: false, error: "Missing required --query <text>" };
|
|
2410
|
+
}
|
|
2411
|
+
return { ok: true, value: { targetDir, query, limit, json } };
|
|
2102
2412
|
}
|
|
2103
|
-
function
|
|
2104
|
-
const index = args.indexOf(flag);
|
|
2105
|
-
if (index === -1) return null;
|
|
2413
|
+
function readFlagValue(args, index, flag) {
|
|
2106
2414
|
const value = args[index + 1];
|
|
2107
|
-
if (!value || value.startsWith("--"))
|
|
2108
|
-
|
|
2415
|
+
if (!value || value.startsWith("--")) {
|
|
2416
|
+
return { ok: false, error: `Missing value for ${flag}` };
|
|
2417
|
+
}
|
|
2418
|
+
return { ok: true, value };
|
|
2419
|
+
}
|
|
2420
|
+
function printUsage2() {
|
|
2421
|
+
console.error("Usage: pro-gov learn recall --query <text> [--target <path>] [--limit <n>] [--json]");
|
|
2109
2422
|
}
|
|
2110
2423
|
|
|
2111
2424
|
// src/commands/lens.ts
|
|
@@ -2113,8 +2426,8 @@ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "node:f
|
|
|
2113
2426
|
import { dirname as dirname9 } from "node:path";
|
|
2114
2427
|
|
|
2115
2428
|
// src/lens/audit.ts
|
|
2116
|
-
import { existsSync as
|
|
2117
|
-
import { basename as
|
|
2429
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync13, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2430
|
+
import { basename as basename6, dirname as dirname8, join as join16 } from "node:path";
|
|
2118
2431
|
var REQUIRED_ARTIFACTS = [
|
|
2119
2432
|
"manifest.md",
|
|
2120
2433
|
"raw/project-lens/architecture-lens.md",
|
|
@@ -2134,20 +2447,20 @@ function createProjectLensAuditPackage(targetDir, auditDir) {
|
|
|
2134
2447
|
version: 1,
|
|
2135
2448
|
target: {
|
|
2136
2449
|
path: targetDir,
|
|
2137
|
-
name:
|
|
2450
|
+
name: basename6(targetDir) || "target"
|
|
2138
2451
|
},
|
|
2139
2452
|
requiredArtifacts: [...REQUIRED_ARTIFACTS]
|
|
2140
2453
|
};
|
|
2141
2454
|
mkdirSync6(auditDir, { recursive: true });
|
|
2142
|
-
writeJson(
|
|
2455
|
+
writeJson(join16(auditDir, "audit.contract.json"), contract);
|
|
2143
2456
|
for (const artifactPath of REQUIRED_ARTIFACTS) {
|
|
2144
|
-
writeTemplate(
|
|
2457
|
+
writeTemplate(join16(auditDir, artifactPath), renderArtifactTemplate(artifactPath, contract));
|
|
2145
2458
|
}
|
|
2146
2459
|
return contract;
|
|
2147
2460
|
}
|
|
2148
2461
|
function checkProjectLensAuditPackage(auditDir, options = {}) {
|
|
2149
|
-
const contractPath =
|
|
2150
|
-
if (!
|
|
2462
|
+
const contractPath = join16(auditDir, "audit.contract.json");
|
|
2463
|
+
if (!existsSync16(contractPath)) {
|
|
2151
2464
|
return {
|
|
2152
2465
|
ok: false,
|
|
2153
2466
|
auditDir,
|
|
@@ -2161,7 +2474,7 @@ function checkProjectLensAuditPackage(auditDir, options = {}) {
|
|
|
2161
2474
|
}
|
|
2162
2475
|
let contract;
|
|
2163
2476
|
try {
|
|
2164
|
-
contract = JSON.parse(
|
|
2477
|
+
contract = JSON.parse(readFileSync13(contractPath, "utf8"));
|
|
2165
2478
|
} catch (error) {
|
|
2166
2479
|
return {
|
|
2167
2480
|
ok: false,
|
|
@@ -2194,12 +2507,12 @@ function checkProjectLensAuditPackage(auditDir, options = {}) {
|
|
|
2194
2507
|
}
|
|
2195
2508
|
}
|
|
2196
2509
|
for (const artifactPath of REQUIRED_ARTIFACTS) {
|
|
2197
|
-
const absolutePath =
|
|
2198
|
-
if (!
|
|
2510
|
+
const absolutePath = join16(auditDir, artifactPath);
|
|
2511
|
+
if (!existsSync16(absolutePath)) {
|
|
2199
2512
|
issues.push({ type: "missing-required-artifact", path: artifactPath });
|
|
2200
2513
|
continue;
|
|
2201
2514
|
}
|
|
2202
|
-
const content =
|
|
2515
|
+
const content = readFileSync13(absolutePath, "utf8");
|
|
2203
2516
|
if (isPendingArtifact(content)) {
|
|
2204
2517
|
issues.push({ type: "artifact-not-complete", path: artifactPath });
|
|
2205
2518
|
} else if (hasTemplateBody(content)) {
|
|
@@ -2459,8 +2772,8 @@ function bulletList(values) {
|
|
|
2459
2772
|
|
|
2460
2773
|
// src/lens/scan.ts
|
|
2461
2774
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
2462
|
-
import { existsSync as
|
|
2463
|
-
import { join as
|
|
2775
|
+
import { existsSync as existsSync17, readdirSync as readdirSync7, readFileSync as readFileSync14, statSync as statSync4 } from "node:fs";
|
|
2776
|
+
import { join as join17, relative as relative6 } from "node:path";
|
|
2464
2777
|
var ignoredDirectories = /* @__PURE__ */ new Set([
|
|
2465
2778
|
".git",
|
|
2466
2779
|
".next",
|
|
@@ -2477,24 +2790,24 @@ function scanProjectLensTarget(targetDir, options = {}) {
|
|
|
2477
2790
|
return {
|
|
2478
2791
|
targetDir,
|
|
2479
2792
|
aiEntryFiles: ["AGENTS.md", "CLAUDE.md"].filter(
|
|
2480
|
-
(file) =>
|
|
2793
|
+
(file) => existsSync17(join17(targetDir, file))
|
|
2481
2794
|
),
|
|
2482
2795
|
aiConfigFiles: [],
|
|
2483
2796
|
packageJson,
|
|
2484
2797
|
docs: {
|
|
2485
|
-
hasDocsDirectory:
|
|
2798
|
+
hasDocsDirectory: existsSync17(join17(targetDir, "docs")),
|
|
2486
2799
|
markdownFileCount: markdownFiles.length,
|
|
2487
2800
|
governanceFiles: markdownFiles.filter((file) => file.startsWith("docs/governance/") || file.startsWith("docs/policy/")).sort()
|
|
2488
2801
|
},
|
|
2489
2802
|
git: readGitState(targetDir),
|
|
2490
|
-
largeFiles: files.map((file) => ({ path: file, bytes: statSync4(
|
|
2803
|
+
largeFiles: files.map((file) => ({ path: file, bytes: statSync4(join17(targetDir, file)).size })).filter((file) => file.bytes >= largeFileBytes).sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path)).slice(0, 25)
|
|
2491
2804
|
};
|
|
2492
2805
|
}
|
|
2493
2806
|
function readPackageJson(targetDir) {
|
|
2494
|
-
const packageJsonPath =
|
|
2495
|
-
if (!
|
|
2807
|
+
const packageJsonPath = join17(targetDir, "package.json");
|
|
2808
|
+
if (!existsSync17(packageJsonPath)) return void 0;
|
|
2496
2809
|
try {
|
|
2497
|
-
const packageJson = JSON.parse(
|
|
2810
|
+
const packageJson = JSON.parse(readFileSync14(packageJsonPath, "utf8"));
|
|
2498
2811
|
return {
|
|
2499
2812
|
scripts: Object.keys(packageJson.scripts ?? {}).sort(),
|
|
2500
2813
|
dependencies: Object.keys(packageJson.dependencies ?? {}).sort(),
|
|
@@ -2529,13 +2842,13 @@ function listProjectFiles(targetDir) {
|
|
|
2529
2842
|
return files.sort();
|
|
2530
2843
|
}
|
|
2531
2844
|
function collectFiles2(rootDir, currentDir, files) {
|
|
2532
|
-
if (!
|
|
2533
|
-
for (const entry of
|
|
2845
|
+
if (!existsSync17(currentDir)) return;
|
|
2846
|
+
for (const entry of readdirSync7(currentDir, { withFileTypes: true })) {
|
|
2534
2847
|
if (entry.isDirectory()) {
|
|
2535
2848
|
if (ignoredDirectories.has(entry.name)) continue;
|
|
2536
|
-
collectFiles2(rootDir,
|
|
2849
|
+
collectFiles2(rootDir, join17(currentDir, entry.name), files);
|
|
2537
2850
|
} else if (entry.isFile()) {
|
|
2538
|
-
files.push(toUnixPath4(
|
|
2851
|
+
files.push(toUnixPath4(relative6(rootDir, join17(currentDir, entry.name))));
|
|
2539
2852
|
}
|
|
2540
2853
|
}
|
|
2541
2854
|
}
|
|
@@ -2555,14 +2868,14 @@ function runLens(args) {
|
|
|
2555
2868
|
if (subcommand2 === "audit") {
|
|
2556
2869
|
return runLensAudit(rest);
|
|
2557
2870
|
}
|
|
2558
|
-
|
|
2871
|
+
printUsage3();
|
|
2559
2872
|
return 1;
|
|
2560
2873
|
}
|
|
2561
2874
|
function runLensInspect(args, subcommand2) {
|
|
2562
2875
|
const options = parseLensOptions(args, subcommand2);
|
|
2563
2876
|
if (!options.ok) {
|
|
2564
2877
|
console.error(options.error);
|
|
2565
|
-
|
|
2878
|
+
printUsage3();
|
|
2566
2879
|
return 1;
|
|
2567
2880
|
}
|
|
2568
2881
|
const report = scanProjectLensTarget(options.value.targetDir);
|
|
@@ -2577,12 +2890,12 @@ function runLensReport(args) {
|
|
|
2577
2890
|
const options = parseLensOptions(args, "report");
|
|
2578
2891
|
if (!options.ok) {
|
|
2579
2892
|
console.error(options.error);
|
|
2580
|
-
|
|
2893
|
+
printUsage3();
|
|
2581
2894
|
return 1;
|
|
2582
2895
|
}
|
|
2583
2896
|
if (!options.value.outPath) {
|
|
2584
2897
|
console.error("Expected --out <path>");
|
|
2585
|
-
|
|
2898
|
+
printUsage3();
|
|
2586
2899
|
return 1;
|
|
2587
2900
|
}
|
|
2588
2901
|
const report = scanProjectLensTarget(options.value.targetDir);
|
|
@@ -2598,12 +2911,12 @@ function runLensAudit(args) {
|
|
|
2598
2911
|
const options = parseLensOptions(rest, "audit init");
|
|
2599
2912
|
if (!options.ok) {
|
|
2600
2913
|
console.error(options.error);
|
|
2601
|
-
|
|
2914
|
+
printUsage3();
|
|
2602
2915
|
return 1;
|
|
2603
2916
|
}
|
|
2604
2917
|
if (!options.value.outPath) {
|
|
2605
2918
|
console.error("Expected --out <path>");
|
|
2606
|
-
|
|
2919
|
+
printUsage3();
|
|
2607
2920
|
return 1;
|
|
2608
2921
|
}
|
|
2609
2922
|
createProjectLensAuditPackage(options.value.targetDir, options.value.outPath);
|
|
@@ -2614,12 +2927,12 @@ function runLensAudit(args) {
|
|
|
2614
2927
|
const options = parseLensOptions(rest, "audit check");
|
|
2615
2928
|
if (!options.ok) {
|
|
2616
2929
|
console.error(options.error);
|
|
2617
|
-
|
|
2930
|
+
printUsage3();
|
|
2618
2931
|
return 1;
|
|
2619
2932
|
}
|
|
2620
2933
|
if (!options.value.auditDir) {
|
|
2621
2934
|
console.error("Expected --dir <path>");
|
|
2622
|
-
|
|
2935
|
+
printUsage3();
|
|
2623
2936
|
return 1;
|
|
2624
2937
|
}
|
|
2625
2938
|
const result = checkProjectLensAuditPackage(options.value.auditDir, { mode: options.value.auditMode });
|
|
@@ -2630,7 +2943,7 @@ function runLensAudit(args) {
|
|
|
2630
2943
|
}
|
|
2631
2944
|
return result.ok ? 0 : 1;
|
|
2632
2945
|
}
|
|
2633
|
-
|
|
2946
|
+
printUsage3();
|
|
2634
2947
|
return 1;
|
|
2635
2948
|
}
|
|
2636
2949
|
function parseLensOptions(args, subcommand2) {
|
|
@@ -2682,7 +2995,7 @@ function parseLensOptions(args, subcommand2) {
|
|
|
2682
2995
|
}
|
|
2683
2996
|
return { ok: true, value: options };
|
|
2684
2997
|
}
|
|
2685
|
-
function
|
|
2998
|
+
function printUsage3() {
|
|
2686
2999
|
console.error("Usage: pro-gov lens scan [--target <path>] [--json]");
|
|
2687
3000
|
console.error("Usage: pro-gov lens inspect [--target <path>] [--format text|json]");
|
|
2688
3001
|
console.error("Usage: pro-gov lens report --target <path> --out <path>");
|
|
@@ -2691,16 +3004,16 @@ function printUsage2() {
|
|
|
2691
3004
|
}
|
|
2692
3005
|
|
|
2693
3006
|
// src/commands/portfolio.ts
|
|
2694
|
-
import { existsSync as
|
|
2695
|
-
import { join as
|
|
3007
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
3008
|
+
import { join as join20 } from "node:path";
|
|
2696
3009
|
|
|
2697
3010
|
// src/portfolio/manifest.ts
|
|
2698
|
-
import { existsSync as
|
|
2699
|
-
import { dirname as dirname10, isAbsolute as isAbsolute3, resolve as
|
|
3011
|
+
import { existsSync as existsSync18, readFileSync as readFileSync15 } from "node:fs";
|
|
3012
|
+
import { dirname as dirname10, isAbsolute as isAbsolute3, resolve as resolve4 } from "node:path";
|
|
2700
3013
|
function loadPortfolioManifest(configPath) {
|
|
2701
3014
|
let parsed;
|
|
2702
3015
|
try {
|
|
2703
|
-
parsed = JSON.parse(
|
|
3016
|
+
parsed = JSON.parse(readFileSync15(configPath, "utf8"));
|
|
2704
3017
|
} catch (error) {
|
|
2705
3018
|
return {
|
|
2706
3019
|
configPath,
|
|
@@ -2712,7 +3025,7 @@ function loadPortfolioManifest(configPath) {
|
|
|
2712
3025
|
]
|
|
2713
3026
|
};
|
|
2714
3027
|
}
|
|
2715
|
-
const normalized = resolveManifestPaths(parsed, dirname10(
|
|
3028
|
+
const normalized = resolveManifestPaths(parsed, dirname10(resolve4(configPath)));
|
|
2716
3029
|
const issues = validatePortfolioManifest(normalized);
|
|
2717
3030
|
return {
|
|
2718
3031
|
configPath,
|
|
@@ -2726,7 +3039,7 @@ function resolveManifestPaths(value, configDir) {
|
|
|
2726
3039
|
if (!isRecord2(endpoint) || typeof endpoint.path !== "string" || isAbsolute3(endpoint.path)) {
|
|
2727
3040
|
return endpoint;
|
|
2728
3041
|
}
|
|
2729
|
-
return { ...endpoint, path:
|
|
3042
|
+
return { ...endpoint, path: resolve4(configDir, endpoint.path) };
|
|
2730
3043
|
};
|
|
2731
3044
|
return {
|
|
2732
3045
|
...value,
|
|
@@ -2760,9 +3073,15 @@ function validatePortfolioManifest(value) {
|
|
|
2760
3073
|
message: "Portfolio manifest portfolioId must be a non-empty string."
|
|
2761
3074
|
});
|
|
2762
3075
|
}
|
|
2763
|
-
validateAllowedFields(
|
|
3076
|
+
validateAllowedFields(
|
|
3077
|
+
value,
|
|
3078
|
+
"root",
|
|
3079
|
+
["schemaVersion", "portfolioId", "controlPlane", "executionEngine", "hostTooling", "targets"],
|
|
3080
|
+
issues
|
|
3081
|
+
);
|
|
2764
3082
|
validateEndpoint(value.controlPlane, "controlPlane", issues);
|
|
2765
3083
|
validateEndpoint(value.executionEngine, "executionEngine", issues);
|
|
3084
|
+
validateHostTooling(value.hostTooling, issues);
|
|
2766
3085
|
if (!Array.isArray(value.targets)) {
|
|
2767
3086
|
issues.push({
|
|
2768
3087
|
type: "invalid-field",
|
|
@@ -2854,7 +3173,7 @@ function validateEndpoint(value, field, issues) {
|
|
|
2854
3173
|
});
|
|
2855
3174
|
return;
|
|
2856
3175
|
}
|
|
2857
|
-
if (!
|
|
3176
|
+
if (!existsSync18(value.path)) {
|
|
2858
3177
|
issues.push({
|
|
2859
3178
|
type: "missing-path",
|
|
2860
3179
|
id: typeof value.id === "string" ? value.id : void 0,
|
|
@@ -2874,24 +3193,474 @@ function validateOptionalStringArray(value, id, field, issues) {
|
|
|
2874
3193
|
});
|
|
2875
3194
|
}
|
|
2876
3195
|
}
|
|
3196
|
+
function validateHostTooling(value, issues) {
|
|
3197
|
+
if (value === void 0) return;
|
|
3198
|
+
if (!Array.isArray(value)) {
|
|
3199
|
+
issues.push({
|
|
3200
|
+
type: "invalid-field",
|
|
3201
|
+
field: "hostTooling",
|
|
3202
|
+
message: "Portfolio hostTooling must be an array."
|
|
3203
|
+
});
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
const seenHosts = /* @__PURE__ */ new Set();
|
|
3207
|
+
for (const entry of value) {
|
|
3208
|
+
if (!isRecord2(entry)) {
|
|
3209
|
+
issues.push({
|
|
3210
|
+
type: "invalid-field",
|
|
3211
|
+
field: "hostTooling",
|
|
3212
|
+
message: "Portfolio hostTooling entry must be an object."
|
|
3213
|
+
});
|
|
3214
|
+
continue;
|
|
3215
|
+
}
|
|
3216
|
+
validateAllowedFields(entry, "hostTooling", ["host", "plugins"], issues);
|
|
3217
|
+
if (entry.host !== "codex" && entry.host !== "claude-code") {
|
|
3218
|
+
issues.push({
|
|
3219
|
+
type: "invalid-field",
|
|
3220
|
+
field: "hostTooling.host",
|
|
3221
|
+
message: "Portfolio hostTooling host must be codex or claude-code."
|
|
3222
|
+
});
|
|
3223
|
+
} else if (seenHosts.has(entry.host)) {
|
|
3224
|
+
issues.push({
|
|
3225
|
+
type: "invalid-field",
|
|
3226
|
+
field: "hostTooling",
|
|
3227
|
+
message: `Duplicate portfolio hostTooling entry: ${entry.host}`
|
|
3228
|
+
});
|
|
3229
|
+
} else {
|
|
3230
|
+
seenHosts.add(entry.host);
|
|
3231
|
+
}
|
|
3232
|
+
if (!Array.isArray(entry.plugins) || !entry.plugins.every((plugin) => typeof plugin === "string" && plugin.length > 0)) {
|
|
3233
|
+
issues.push({
|
|
3234
|
+
type: "invalid-field",
|
|
3235
|
+
field: "hostTooling.plugins",
|
|
3236
|
+
message: "Portfolio hostTooling plugins must be an array of non-empty strings."
|
|
3237
|
+
});
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
2877
3241
|
function isRecord2(value) {
|
|
2878
3242
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2879
3243
|
}
|
|
2880
3244
|
|
|
3245
|
+
// src/portfolio/doctor.ts
|
|
3246
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
3247
|
+
import { existsSync as existsSync20, readFileSync as readFileSync17 } from "node:fs";
|
|
3248
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
3249
|
+
import { dirname as dirname11, join as join19 } from "node:path";
|
|
3250
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3251
|
+
|
|
3252
|
+
// src/host-tooling/inventory.ts
|
|
3253
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
3254
|
+
function inspectHostTooling(requirements, runner = defaultRunner2) {
|
|
3255
|
+
const hosts = [];
|
|
3256
|
+
const issues = [];
|
|
3257
|
+
for (const requirement of requirements) {
|
|
3258
|
+
const command2 = requirement.host === "codex" ? ["codex", "plugin", "list", "--json"] : ["claude", "plugin", "list", "--json"];
|
|
3259
|
+
const result = runner({ host: requirement.host, command: command2 });
|
|
3260
|
+
if (result.status !== 0) {
|
|
3261
|
+
issues.push({
|
|
3262
|
+
type: "host-tooling-command-failed",
|
|
3263
|
+
host: requirement.host,
|
|
3264
|
+
message: `Unable to inspect ${requirement.host} plugins: ${result.stderr || `exit ${result.status}`}`
|
|
3265
|
+
});
|
|
3266
|
+
hosts.push({ host: requirement.host, plugins: [] });
|
|
3267
|
+
continue;
|
|
3268
|
+
}
|
|
3269
|
+
let plugins;
|
|
3270
|
+
try {
|
|
3271
|
+
plugins = parseHostPlugins(requirement.host, JSON.parse(result.stdout));
|
|
3272
|
+
} catch (error) {
|
|
3273
|
+
issues.push({
|
|
3274
|
+
type: "host-tooling-command-failed",
|
|
3275
|
+
host: requirement.host,
|
|
3276
|
+
message: `Unable to parse ${requirement.host} plugin inventory: ${error instanceof Error ? error.message : String(error)}`
|
|
3277
|
+
});
|
|
3278
|
+
hosts.push({ host: requirement.host, plugins: [] });
|
|
3279
|
+
continue;
|
|
3280
|
+
}
|
|
3281
|
+
const byId = new Map(plugins.map((plugin) => [plugin.id, plugin]));
|
|
3282
|
+
hosts.push({
|
|
3283
|
+
host: requirement.host,
|
|
3284
|
+
plugins: requirement.plugins.flatMap((pluginId) => {
|
|
3285
|
+
const plugin = byId.get(pluginId);
|
|
3286
|
+
return plugin ? [plugin] : [];
|
|
3287
|
+
})
|
|
3288
|
+
});
|
|
3289
|
+
for (const pluginId of requirement.plugins) {
|
|
3290
|
+
const plugin = byId.get(pluginId);
|
|
3291
|
+
if (!plugin) {
|
|
3292
|
+
issues.push({
|
|
3293
|
+
type: "host-tooling-missing",
|
|
3294
|
+
host: requirement.host,
|
|
3295
|
+
pluginId,
|
|
3296
|
+
message: `Required ${requirement.host} plugin is missing: ${pluginId}`
|
|
3297
|
+
});
|
|
3298
|
+
} else if (!plugin.enabled) {
|
|
3299
|
+
issues.push({
|
|
3300
|
+
type: "host-tooling-disabled",
|
|
3301
|
+
host: requirement.host,
|
|
3302
|
+
pluginId,
|
|
3303
|
+
message: `Required ${requirement.host} plugin is disabled: ${pluginId}`
|
|
3304
|
+
});
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
return { hosts, issues };
|
|
3309
|
+
}
|
|
3310
|
+
function parseHostPlugins(host, value) {
|
|
3311
|
+
const entries = host === "codex" ? isRecord3(value) && Array.isArray(value.installed) ? value.installed : void 0 : Array.isArray(value) ? value : void 0;
|
|
3312
|
+
if (!entries) throw new Error("expected a plugin array");
|
|
3313
|
+
return entries.flatMap((entry) => {
|
|
3314
|
+
if (!isRecord3(entry)) return [];
|
|
3315
|
+
const id = host === "codex" ? entry.pluginId : entry.id;
|
|
3316
|
+
if (typeof id !== "string") return [];
|
|
3317
|
+
return [{
|
|
3318
|
+
id,
|
|
3319
|
+
version: typeof entry.version === "string" ? entry.version : void 0,
|
|
3320
|
+
enabled: entry.enabled === true
|
|
3321
|
+
}];
|
|
3322
|
+
});
|
|
3323
|
+
}
|
|
3324
|
+
function defaultRunner2({ command: command2 }) {
|
|
3325
|
+
const result = spawnSync5(command2[0] ?? "", command2.slice(1), {
|
|
3326
|
+
encoding: "utf8",
|
|
3327
|
+
timeout: 1e4
|
|
3328
|
+
});
|
|
3329
|
+
return {
|
|
3330
|
+
status: result.status,
|
|
3331
|
+
stdout: result.stdout,
|
|
3332
|
+
stderr: result.stderr || result.error?.message || ""
|
|
3333
|
+
};
|
|
3334
|
+
}
|
|
3335
|
+
function isRecord3(value) {
|
|
3336
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
// src/portfolio/asset-state.ts
|
|
3340
|
+
import { existsSync as existsSync19, lstatSync as lstatSync5, readFileSync as readFileSync16 } from "node:fs";
|
|
3341
|
+
import { join as join18 } from "node:path";
|
|
3342
|
+
function comparePortfolioAssetState(options) {
|
|
3343
|
+
const expectedManifest = readPlanDocument(options.expectedPlan, ".pro-gov/assets.json");
|
|
3344
|
+
const expectedLock = readPlanDocument(options.expectedPlan, ".pro-gov/assets.lock.json");
|
|
3345
|
+
const currentManifest = readJsonFile(join18(options.targetDir, ".pro-gov/assets.json"));
|
|
3346
|
+
const currentLock = readJsonFile(join18(options.targetDir, ".pro-gov/assets.lock.json"));
|
|
3347
|
+
const issues = [];
|
|
3348
|
+
if (!sameStrings(currentManifest?.bundleIds, expectedManifest?.bundleIds)) {
|
|
3349
|
+
issues.push({
|
|
3350
|
+
type: "bundle-drift",
|
|
3351
|
+
message: "Target asset bundles do not match the portfolio manifest."
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
3354
|
+
if (!sameStrings(currentManifest?.assetIds, expectedManifest?.assetIds)) {
|
|
3355
|
+
issues.push({
|
|
3356
|
+
type: "asset-set-drift",
|
|
3357
|
+
message: "Target asset ids do not match the current bundle definitions."
|
|
3358
|
+
});
|
|
3359
|
+
}
|
|
3360
|
+
if (!sameLock(currentLock, expectedLock)) {
|
|
3361
|
+
issues.push({
|
|
3362
|
+
type: "asset-lock-drift",
|
|
3363
|
+
message: "Target asset lock does not match the current registry, placement, and content hashes."
|
|
3364
|
+
});
|
|
3365
|
+
}
|
|
3366
|
+
const expectedTargets = new Set((expectedLock?.assets ?? []).map((entry) => entry.targetPath));
|
|
3367
|
+
for (const entry of currentLock?.assets ?? []) {
|
|
3368
|
+
if (expectedTargets.has(entry.targetPath)) continue;
|
|
3369
|
+
const targetAbsolutePath = join18(options.targetDir, entry.targetPath);
|
|
3370
|
+
if (!pathIsSymlink(targetAbsolutePath)) continue;
|
|
3371
|
+
issues.push({
|
|
3372
|
+
type: "orphaned-managed-symlink",
|
|
3373
|
+
targetPath: entry.targetPath,
|
|
3374
|
+
message: `Previously managed symlink is absent from the expected bundle state: ${entry.targetPath}`
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
return { issues };
|
|
3378
|
+
}
|
|
3379
|
+
function readPlanDocument(plan, targetPath) {
|
|
3380
|
+
const action = plan.actions.find(
|
|
3381
|
+
(candidate) => candidate.type === "write-file" && candidate.targetPath === targetPath
|
|
3382
|
+
);
|
|
3383
|
+
if (!action || action.type !== "write-file") return void 0;
|
|
3384
|
+
try {
|
|
3385
|
+
return JSON.parse(action.content);
|
|
3386
|
+
} catch {
|
|
3387
|
+
return void 0;
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
function readJsonFile(path) {
|
|
3391
|
+
if (!existsSync19(path)) return void 0;
|
|
3392
|
+
try {
|
|
3393
|
+
return JSON.parse(readFileSync16(path, "utf8"));
|
|
3394
|
+
} catch {
|
|
3395
|
+
return void 0;
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
function sameStrings(left, right) {
|
|
3399
|
+
return JSON.stringify([...left ?? []].sort()) === JSON.stringify([...right ?? []].sort());
|
|
3400
|
+
}
|
|
3401
|
+
function sameLock(left, right) {
|
|
3402
|
+
return JSON.stringify(normalizeLock(left)) === JSON.stringify(normalizeLock(right));
|
|
3403
|
+
}
|
|
3404
|
+
function normalizeLock(lock) {
|
|
3405
|
+
return {
|
|
3406
|
+
host: lock?.host,
|
|
3407
|
+
placement: lock?.placement,
|
|
3408
|
+
bundleIds: [...lock?.bundleIds ?? []].sort(),
|
|
3409
|
+
assets: [...lock?.assets ?? []].sort((a, b) => a.id.localeCompare(b.id))
|
|
3410
|
+
};
|
|
3411
|
+
}
|
|
3412
|
+
function pathIsSymlink(path) {
|
|
3413
|
+
try {
|
|
3414
|
+
return lstatSync5(path).isSymbolicLink();
|
|
3415
|
+
} catch {
|
|
3416
|
+
return false;
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
// src/portfolio/doctor.ts
|
|
3421
|
+
function inspectPortfolio(options) {
|
|
3422
|
+
const expectedPackageVersions = getExpectedPackageVersions();
|
|
3423
|
+
const hostTooling = inspectHostTooling(options.manifest.hostTooling ?? []);
|
|
3424
|
+
const targets = options.targets.map((target) => inspectTarget({
|
|
3425
|
+
target,
|
|
3426
|
+
agentAssetsDir: options.agentAssetsDir,
|
|
3427
|
+
registry: options.registry,
|
|
3428
|
+
bundles: options.bundles,
|
|
3429
|
+
expectedPackageVersions
|
|
3430
|
+
}));
|
|
3431
|
+
return {
|
|
3432
|
+
ok: hostTooling.issues.length === 0 && targets.every((target) => target.issues.length === 0),
|
|
3433
|
+
portfolioId: options.manifest.portfolioId,
|
|
3434
|
+
expectedPackageVersions,
|
|
3435
|
+
hostTooling,
|
|
3436
|
+
targets
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3439
|
+
function inspectTarget(options) {
|
|
3440
|
+
const { target } = options;
|
|
3441
|
+
const issues = [];
|
|
3442
|
+
const packageJson = readJson2(join19(target.path, "package.json"));
|
|
3443
|
+
const packages = {};
|
|
3444
|
+
for (const packageName of ["@pieai/pro-gov", "@pieai/doc-gov"]) {
|
|
3445
|
+
const declared = packageJson?.devDependencies?.[packageName] ?? packageJson?.dependencies?.[packageName];
|
|
3446
|
+
const installedPackage = readJson2(join19(target.path, "node_modules", packageName, "package.json"));
|
|
3447
|
+
const installed = installedPackage?.version;
|
|
3448
|
+
const expected = options.expectedPackageVersions[packageName];
|
|
3449
|
+
packages[packageName] = { declared, installed, expected };
|
|
3450
|
+
if (!declared) {
|
|
3451
|
+
issues.push({
|
|
3452
|
+
type: "package-declaration-missing",
|
|
3453
|
+
packageName,
|
|
3454
|
+
message: `Target does not declare ${packageName}.`
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
3457
|
+
if (!installed || expected && installed !== expected) {
|
|
3458
|
+
issues.push({
|
|
3459
|
+
type: "package-version-drift",
|
|
3460
|
+
packageName,
|
|
3461
|
+
message: `Target ${packageName} installed version is ${installed ?? "missing"}; expected ${expected ?? "unknown"}.`
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
const checks = runTargetChecks(target);
|
|
3466
|
+
for (const check of checks) {
|
|
3467
|
+
if (check.status === 0) continue;
|
|
3468
|
+
issues.push({
|
|
3469
|
+
type: "target-check-failed",
|
|
3470
|
+
check: check.name,
|
|
3471
|
+
message: `Target check failed: ${check.name} (${check.status ?? "unavailable"}).`
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
const assetCheck = checkInstalledAssets({
|
|
3475
|
+
targetDir: target.path,
|
|
3476
|
+
agentAssetsDir: options.agentAssetsDir,
|
|
3477
|
+
registry: options.registry,
|
|
3478
|
+
strictRegistry: true
|
|
3479
|
+
});
|
|
3480
|
+
issues.push(...assetCheck.issues.map((issue) => ({
|
|
3481
|
+
type: issue.type,
|
|
3482
|
+
targetPath: issue.targetPath,
|
|
3483
|
+
message: issue.message
|
|
3484
|
+
})));
|
|
3485
|
+
try {
|
|
3486
|
+
const expectedPlan = createAssetInstallPlan({
|
|
3487
|
+
targetDir: target.path,
|
|
3488
|
+
agentAssetsDir: options.agentAssetsDir,
|
|
3489
|
+
registry: options.registry,
|
|
3490
|
+
bundles: options.bundles,
|
|
3491
|
+
bundleIds: target.assetBundles ?? [],
|
|
3492
|
+
host: readTargetAssetHost(target.path) ?? "codex"
|
|
3493
|
+
});
|
|
3494
|
+
issues.push(...comparePortfolioAssetState({ targetDir: target.path, expectedPlan }).issues);
|
|
3495
|
+
} catch (error) {
|
|
3496
|
+
issues.push({
|
|
3497
|
+
type: "asset-lock-drift",
|
|
3498
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3499
|
+
});
|
|
3500
|
+
if (!existsSync20(join19(target.path, ".pro-gov/assets.json"))) {
|
|
3501
|
+
issues.push({ type: "bundle-drift", message: "Target asset manifest is missing." });
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
return {
|
|
3505
|
+
id: target.id,
|
|
3506
|
+
path: target.path,
|
|
3507
|
+
profile: target.profile,
|
|
3508
|
+
packages,
|
|
3509
|
+
git: inspectGit(target.path),
|
|
3510
|
+
checks,
|
|
3511
|
+
issues: deduplicateIssues(issues)
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3514
|
+
function readTargetAssetHost(targetDir) {
|
|
3515
|
+
const lockfile = readJson2(join19(targetDir, ".pro-gov/assets.lock.json"));
|
|
3516
|
+
return isAssetRegistryHost(lockfile?.host) ? lockfile.host : void 0;
|
|
3517
|
+
}
|
|
3518
|
+
function isAssetRegistryHost(value) {
|
|
3519
|
+
return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
|
|
3520
|
+
}
|
|
3521
|
+
function runTargetChecks(target) {
|
|
3522
|
+
const proGovCli = join19(target.path, "node_modules/@pieai/pro-gov/dist/cli.js");
|
|
3523
|
+
const docGovCli = join19(target.path, "node_modules/@pieai/doc-gov/dist/cli.js");
|
|
3524
|
+
const commands = [
|
|
3525
|
+
{
|
|
3526
|
+
name: "pro-gov doctor",
|
|
3527
|
+
cli: proGovCli,
|
|
3528
|
+
args: target.profile === "engineering-runtime" ? ["doctor", "--strict-hooks"] : ["doctor"]
|
|
3529
|
+
},
|
|
3530
|
+
{ name: "doc-gov router-check", cli: docGovCli, args: ["router-check"] },
|
|
3531
|
+
{ name: "doc-gov scan --check", cli: docGovCli, args: ["scan", "--check"] }
|
|
3532
|
+
];
|
|
3533
|
+
return commands.map((command2) => {
|
|
3534
|
+
if (!existsSync20(command2.cli)) return { name: command2.name, status: null };
|
|
3535
|
+
const result = spawnSync6(process.execPath, [command2.cli, ...command2.args], {
|
|
3536
|
+
cwd: target.path,
|
|
3537
|
+
encoding: "utf8",
|
|
3538
|
+
timeout: 3e4
|
|
3539
|
+
});
|
|
3540
|
+
return { name: command2.name, status: result.status };
|
|
3541
|
+
});
|
|
3542
|
+
}
|
|
3543
|
+
function inspectGit(path) {
|
|
3544
|
+
const inside = spawnSync6("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
3545
|
+
cwd: path,
|
|
3546
|
+
encoding: "utf8"
|
|
3547
|
+
});
|
|
3548
|
+
if (inside.status !== 0) return { isRepository: false, dirty: false };
|
|
3549
|
+
const status = spawnSync6("git", ["status", "--porcelain"], { cwd: path, encoding: "utf8" });
|
|
3550
|
+
const branch = spawnSync6("git", ["branch", "--show-current"], { cwd: path, encoding: "utf8" });
|
|
3551
|
+
return {
|
|
3552
|
+
isRepository: true,
|
|
3553
|
+
dirty: status.stdout.trim().length > 0,
|
|
3554
|
+
branch: branch.stdout.trim() || void 0
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
3557
|
+
function getExpectedPackageVersions() {
|
|
3558
|
+
const proGovPackage = readJson2(findOwnPackageJson());
|
|
3559
|
+
let docGovVersion;
|
|
3560
|
+
try {
|
|
3561
|
+
const require2 = createRequire2(import.meta.url);
|
|
3562
|
+
const docGovPackage = readJson2(require2.resolve("@pieai/doc-gov/package.json"));
|
|
3563
|
+
docGovVersion = docGovPackage?.version;
|
|
3564
|
+
} catch {
|
|
3565
|
+
docGovVersion = void 0;
|
|
3566
|
+
}
|
|
3567
|
+
return {
|
|
3568
|
+
"@pieai/pro-gov": proGovPackage?.version,
|
|
3569
|
+
"@pieai/doc-gov": docGovVersion
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
function findOwnPackageJson() {
|
|
3573
|
+
let current = dirname11(fileURLToPath3(import.meta.url));
|
|
3574
|
+
for (let depth = 0; depth < 5; depth += 1) {
|
|
3575
|
+
const candidate = join19(current, "package.json");
|
|
3576
|
+
if (existsSync20(candidate)) return candidate;
|
|
3577
|
+
current = dirname11(current);
|
|
3578
|
+
}
|
|
3579
|
+
return "";
|
|
3580
|
+
}
|
|
3581
|
+
function readJson2(path) {
|
|
3582
|
+
if (!path || !existsSync20(path)) return void 0;
|
|
3583
|
+
try {
|
|
3584
|
+
return JSON.parse(readFileSync17(path, "utf8"));
|
|
3585
|
+
} catch {
|
|
3586
|
+
return void 0;
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
function deduplicateIssues(issues) {
|
|
3590
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3591
|
+
return issues.filter((issue) => {
|
|
3592
|
+
const key = `${issue.type}\0${issue.packageName ?? ""}\0${issue.targetPath ?? ""}\0${issue.check ?? ""}\0${issue.message}`;
|
|
3593
|
+
if (seen.has(key)) return false;
|
|
3594
|
+
seen.add(key);
|
|
3595
|
+
return true;
|
|
3596
|
+
});
|
|
3597
|
+
}
|
|
3598
|
+
|
|
2881
3599
|
// src/commands/portfolio.ts
|
|
2882
3600
|
function runPortfolio(args) {
|
|
2883
3601
|
const [subcommand2, ...rest] = args;
|
|
2884
3602
|
if (subcommand2 === "check") return runPortfolioCheck(rest);
|
|
2885
3603
|
if (subcommand2 === "plan") return runPortfolioPlan(rest);
|
|
2886
3604
|
if (subcommand2 === "assets-check") return runPortfolioAssetsCheck(rest);
|
|
2887
|
-
|
|
3605
|
+
if (subcommand2 === "doctor") return runPortfolioDoctor(rest);
|
|
3606
|
+
printUsage4();
|
|
2888
3607
|
return 1;
|
|
2889
3608
|
}
|
|
3609
|
+
function runPortfolioDoctor(args) {
|
|
3610
|
+
const options = parsePortfolioOptions(args);
|
|
3611
|
+
if (!options.ok) {
|
|
3612
|
+
console.error(options.error);
|
|
3613
|
+
printUsage4();
|
|
3614
|
+
return 1;
|
|
3615
|
+
}
|
|
3616
|
+
const loaded = loadPortfolioManifest(options.value.configPath);
|
|
3617
|
+
if (loaded.issues.length > 0 || !loaded.manifest) {
|
|
3618
|
+
if (options.value.json) {
|
|
3619
|
+
console.log(JSON.stringify({ ok: false, configPath: loaded.configPath, issues: loaded.issues, targets: [] }, null, 2));
|
|
3620
|
+
} else {
|
|
3621
|
+
for (const issue of loaded.issues) console.error(`${issue.type}: ${issue.message}`);
|
|
3622
|
+
}
|
|
3623
|
+
return 1;
|
|
3624
|
+
}
|
|
3625
|
+
const targets = getDefaultPortfolioTargets(loaded.manifest).filter(
|
|
3626
|
+
(target) => !options.value.targetId || options.value.targetId === "all" || target.id === options.value.targetId
|
|
3627
|
+
);
|
|
3628
|
+
if (targets.length === 0) {
|
|
3629
|
+
console.error(`Unknown portfolio target: ${options.value.targetId}`);
|
|
3630
|
+
return 1;
|
|
3631
|
+
}
|
|
3632
|
+
const loadedAssets = loadAgentAssetRegistry({
|
|
3633
|
+
agentAssetsDir: findPortfolioAgentAssetsDir(loaded.manifest)
|
|
3634
|
+
});
|
|
3635
|
+
if (loadedAssets.issues.length > 0) {
|
|
3636
|
+
for (const issue of loadedAssets.issues) console.error(`${issue.type}: ${issue.message}`);
|
|
3637
|
+
return 1;
|
|
3638
|
+
}
|
|
3639
|
+
const result = inspectPortfolio({
|
|
3640
|
+
manifest: loaded.manifest,
|
|
3641
|
+
targets,
|
|
3642
|
+
agentAssetsDir: loadedAssets.agentAssetsDir,
|
|
3643
|
+
registry: loadedAssets.registry,
|
|
3644
|
+
bundles: loadAgentAssetBundles(loadedAssets.agentAssetsDir)
|
|
3645
|
+
});
|
|
3646
|
+
const output = { configPath: loaded.configPath, ...result };
|
|
3647
|
+
if (options.value.json) {
|
|
3648
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3649
|
+
} else if (result.ok) {
|
|
3650
|
+
console.log(`portfolio doctor passed (${targets.length} targets)`);
|
|
3651
|
+
} else {
|
|
3652
|
+
for (const issue of result.hostTooling.issues) console.log(`${issue.host} ${issue.type}: ${issue.message}`);
|
|
3653
|
+
for (const target of result.targets) {
|
|
3654
|
+
for (const issue of target.issues) console.log(`${target.id} ${issue.type}: ${issue.message}`);
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
return result.ok ? 0 : 1;
|
|
3658
|
+
}
|
|
2890
3659
|
function runPortfolioCheck(args) {
|
|
2891
3660
|
const options = parsePortfolioOptions(args);
|
|
2892
3661
|
if (!options.ok) {
|
|
2893
3662
|
console.error(options.error);
|
|
2894
|
-
|
|
3663
|
+
printUsage4();
|
|
2895
3664
|
return 1;
|
|
2896
3665
|
}
|
|
2897
3666
|
const loaded = loadPortfolioManifest(options.value.configPath);
|
|
@@ -2923,7 +3692,7 @@ function runPortfolioPlan(args) {
|
|
|
2923
3692
|
const options = parsePortfolioOptions(args);
|
|
2924
3693
|
if (!options.ok) {
|
|
2925
3694
|
console.error(options.error);
|
|
2926
|
-
|
|
3695
|
+
printUsage4();
|
|
2927
3696
|
return 1;
|
|
2928
3697
|
}
|
|
2929
3698
|
const loaded = loadPortfolioManifest(options.value.configPath);
|
|
@@ -2997,7 +3766,7 @@ function runPortfolioAssetsCheck(args) {
|
|
|
2997
3766
|
const options = parsePortfolioOptions(args);
|
|
2998
3767
|
if (!options.ok) {
|
|
2999
3768
|
console.error(options.error);
|
|
3000
|
-
|
|
3769
|
+
printUsage4();
|
|
3001
3770
|
return 1;
|
|
3002
3771
|
}
|
|
3003
3772
|
const loaded = loadPortfolioManifest(options.value.configPath);
|
|
@@ -3113,7 +3882,7 @@ function parsePortfolioOptions(args) {
|
|
|
3113
3882
|
index += 1;
|
|
3114
3883
|
} else if (arg === "--host") {
|
|
3115
3884
|
const host = args[index + 1];
|
|
3116
|
-
if (!isHost2(host)) return { ok: false, error: "Expected --host codex|claude-code" };
|
|
3885
|
+
if (!isHost2(host)) return { ok: false, error: "Expected --host codex|claude-code|gemini-cli|antigravity" };
|
|
3117
3886
|
options.host = host;
|
|
3118
3887
|
index += 1;
|
|
3119
3888
|
} else if (arg === "--json") {
|
|
@@ -3129,19 +3898,20 @@ function isHost2(value) {
|
|
|
3129
3898
|
return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
|
|
3130
3899
|
}
|
|
3131
3900
|
function findPortfolioAgentAssetsDir(manifest) {
|
|
3132
|
-
const agentAssetsDir = manifest?.executionEngine?.path ?
|
|
3133
|
-
return agentAssetsDir &&
|
|
3901
|
+
const agentAssetsDir = manifest?.executionEngine?.path ? join20(manifest.executionEngine.path, "agent-assets") : void 0;
|
|
3902
|
+
return agentAssetsDir && existsSync21(join20(agentAssetsDir, "registry.json")) ? agentAssetsDir : void 0;
|
|
3134
3903
|
}
|
|
3135
|
-
function
|
|
3904
|
+
function printUsage4() {
|
|
3136
3905
|
console.error("Usage:");
|
|
3137
3906
|
console.error(" pro-gov portfolio check --config <path> [--json]");
|
|
3138
3907
|
console.error(" pro-gov portfolio plan --config <path> [--target <id|all>] [--host codex|claude-code|gemini-cli|antigravity] [--json]");
|
|
3139
3908
|
console.error(" pro-gov portfolio assets-check --config <path> [--target <id|all>] [--json]");
|
|
3909
|
+
console.error(" pro-gov portfolio doctor --config <path> [--target <id|all>] [--json]");
|
|
3140
3910
|
}
|
|
3141
3911
|
|
|
3142
3912
|
// src/commands/sync.ts
|
|
3143
|
-
import { existsSync as
|
|
3144
|
-
import { join as
|
|
3913
|
+
import { existsSync as existsSync22, readFileSync as readFileSync18 } from "node:fs";
|
|
3914
|
+
import { join as join21 } from "node:path";
|
|
3145
3915
|
function runSync(args) {
|
|
3146
3916
|
if (!args.includes("--check")) {
|
|
3147
3917
|
console.error("pro-gov sync is read-only and requires --check.");
|
|
@@ -3168,16 +3938,16 @@ function runSync(args) {
|
|
|
3168
3938
|
console.log("pro-gov sync check");
|
|
3169
3939
|
console.log(`profile: ${profile}`);
|
|
3170
3940
|
for (const file of planStarterFiles(profile)) {
|
|
3171
|
-
const targetPath =
|
|
3172
|
-
if (!
|
|
3941
|
+
const targetPath = join21(process.cwd(), file.targetPath);
|
|
3942
|
+
if (!existsSync22(targetPath)) {
|
|
3173
3943
|
if (file.ownership === "optional-guardrail") continue;
|
|
3174
3944
|
console.log(`missing: ${file.targetPath}`);
|
|
3175
3945
|
differences += 1;
|
|
3176
3946
|
continue;
|
|
3177
3947
|
}
|
|
3178
3948
|
if (file.ownership === "project-local-seed") continue;
|
|
3179
|
-
const source =
|
|
3180
|
-
const target =
|
|
3949
|
+
const source = readFileSync18(file.absoluteSourcePath, "utf8");
|
|
3950
|
+
const target = readFileSync18(targetPath, "utf8");
|
|
3181
3951
|
if (source !== target) {
|
|
3182
3952
|
console.log(`different: ${file.targetPath}`);
|
|
3183
3953
|
differences += 1;
|
|
@@ -3192,7 +3962,7 @@ function runSync(args) {
|
|
|
3192
3962
|
}
|
|
3193
3963
|
function inferInstalledProfile(root) {
|
|
3194
3964
|
const installed = ["engineering-runtime", "doc-only"].filter(
|
|
3195
|
-
(profile) =>
|
|
3965
|
+
(profile) => existsSync22(join21(root, `docs/governance/agents-routing/${profile}-v0.9.md`))
|
|
3196
3966
|
);
|
|
3197
3967
|
return installed.length === 1 ? installed[0] : void 0;
|
|
3198
3968
|
}
|
|
@@ -3215,6 +3985,8 @@ var COMMANDS = [
|
|
|
3215
3985
|
"portfolio check --config <path> [--json]",
|
|
3216
3986
|
"portfolio plan --config <path> [--target <id|all>] [--json]",
|
|
3217
3987
|
"portfolio assets-check --config <path> [--target <id|all>] [--json]",
|
|
3988
|
+
"portfolio doctor --config <path> [--target <id|all>] [--json]",
|
|
3989
|
+
"learn recall --query <text> [--target <path>] [--limit <n>] [--json]",
|
|
3218
3990
|
"lens scan [--target <path>] [--json]",
|
|
3219
3991
|
"lens inspect [--target <path>] [--format text|json]",
|
|
3220
3992
|
"lens report --target <path> --out <path>",
|
|
@@ -3233,6 +4005,7 @@ async function main() {
|
|
|
3233
4005
|
return command ? 0 : 1;
|
|
3234
4006
|
}
|
|
3235
4007
|
if (command === "assets") return runAssets(process.argv.slice(3));
|
|
4008
|
+
if (command === "learn") return runLearn(process.argv.slice(3));
|
|
3236
4009
|
if (command === "lens") return runLens(process.argv.slice(3));
|
|
3237
4010
|
if (command === "portfolio") return runPortfolio(process.argv.slice(3));
|
|
3238
4011
|
if (command === "host-hook") return runHostHook(process.argv.slice(3));
|