@rhize/skill-forge 0.17.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/README.md +42 -14
- package/dist/cli.js +1400 -746
- package/dist/cli.js.map +1 -1
- package/dist/refine-prompt.md +10 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/cli.ts
|
|
4
|
-
import { Command } from "commander";
|
|
5
|
-
import { createRequire } from "module";
|
|
6
|
-
|
|
7
|
-
// src/commands/add.ts
|
|
8
|
-
import { join as join24 } from "path";
|
|
9
|
-
|
|
10
|
-
// src/config.ts
|
|
11
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
12
|
-
|
|
13
3
|
// src/errors.ts
|
|
14
4
|
var UserError = class extends Error {
|
|
15
5
|
constructor(message) {
|
|
@@ -18,6 +8,13 @@ var UserError = class extends Error {
|
|
|
18
8
|
}
|
|
19
9
|
};
|
|
20
10
|
|
|
11
|
+
// src/pluginDecisions.ts
|
|
12
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync } from "fs";
|
|
13
|
+
import { isAbsolute, join as join4, resolve } from "path";
|
|
14
|
+
|
|
15
|
+
// src/config.ts
|
|
16
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
17
|
+
|
|
21
18
|
// src/gate/scanShared.ts
|
|
22
19
|
import { readFileSync, readdirSync, statSync } from "fs";
|
|
23
20
|
import { extname, join } from "path";
|
|
@@ -476,9 +473,6 @@ function saveConfig(config) {
|
|
|
476
473
|
chmodSync(path, 384);
|
|
477
474
|
}
|
|
478
475
|
|
|
479
|
-
// src/install/resolve.ts
|
|
480
|
-
import { existsSync as existsSync2 } from "fs";
|
|
481
|
-
|
|
482
476
|
// src/util.ts
|
|
483
477
|
import {
|
|
484
478
|
accessSync,
|
|
@@ -596,11 +590,83 @@ function appendPrivateJsonLine(path, value) {
|
|
|
596
590
|
}
|
|
597
591
|
}
|
|
598
592
|
|
|
593
|
+
// src/pluginDecisions.ts
|
|
594
|
+
function projectIdentity(path) {
|
|
595
|
+
return realpathSync(resolve(path));
|
|
596
|
+
}
|
|
597
|
+
function validatePluginDecision(value) {
|
|
598
|
+
const d = value;
|
|
599
|
+
if (!d || !["claude", "codex"].includes(d.host) || typeof d.pluginId !== "string" || !/^[\w.-]+@[\w.-]+$/.test(d.pluginId) || !["essential", "project-specific", "intentionally-infrequent"].includes(d.category) || typeof d.reason !== "string" || !d.reason.trim() || d.reason.length > 2e3 || /[\x00-\x1f\x7f-\x9f\u200b-\u200f\u202a-\u202e\u2066-\u2069]/.test(d.reason) || d.project !== void 0 && (typeof d.project !== "string" || !isAbsolute(d.project)) || d.category === "project-specific" && !d.project || typeof d.updatedAt !== "string" || !Number.isFinite(Date.parse(d.updatedAt))) {
|
|
600
|
+
throw new UserError("Invalid plugin keep decision: require host, plugin@marketplace, category, reason, date, and a project for project-specific decisions.");
|
|
601
|
+
}
|
|
602
|
+
return { host: d.host, pluginId: d.pluginId, category: d.category, reason: d.reason.trim(), project: d.project, updatedAt: d.updatedAt };
|
|
603
|
+
}
|
|
604
|
+
function decisionKey(d) {
|
|
605
|
+
return JSON.stringify([d.host, d.pluginId, d.project ?? null]);
|
|
606
|
+
}
|
|
607
|
+
function readPluginDecisions() {
|
|
608
|
+
const path = join4(getHomeDir(), "plugin-decisions.json");
|
|
609
|
+
if (!existsSync2(path)) return [];
|
|
610
|
+
const store = JSON.parse(readFileSync3(path, "utf8"));
|
|
611
|
+
if (store.schemaVersion !== 1 || !Array.isArray(store.records)) throw new UserError("Invalid plugin-decisions.json schema.");
|
|
612
|
+
const records = store.records.map(validatePluginDecision);
|
|
613
|
+
if (new Set(records.map(decisionKey)).size !== records.length) throw new UserError("Duplicate plugin keep decisions.");
|
|
614
|
+
return records;
|
|
615
|
+
}
|
|
616
|
+
function writePluginDecisions(records) {
|
|
617
|
+
writeJsonAtomic(join4(getHomeDir(), "plugin-decisions.json"), { schemaVersion: 1, records: records.map(validatePluginDecision) });
|
|
618
|
+
}
|
|
619
|
+
function applyPluginDecisions(report, cwd = process.cwd()) {
|
|
620
|
+
try {
|
|
621
|
+
const records = readPluginDecisions();
|
|
622
|
+
const project = projectIdentity(cwd);
|
|
623
|
+
for (const plugin of report.plugins ?? []) {
|
|
624
|
+
const matches = records.filter((d) => d.host === (plugin.host ?? "claude") && d.pluginId === plugin.pluginId);
|
|
625
|
+
const decision = matches.find((d) => d.project === project) ?? matches.find((d) => !d.project);
|
|
626
|
+
if (!decision) continue;
|
|
627
|
+
plugin.keepDecision = decision;
|
|
628
|
+
if (plugin.recommendation !== "review") plugin.recommendation = "keep";
|
|
629
|
+
}
|
|
630
|
+
} catch (error) {
|
|
631
|
+
report.notices.push(`Plugin keep decisions unavailable; retention advice unchanged: ${error.message}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/commands/plugins.ts
|
|
636
|
+
function pluginsKeepCommand(pluginId, options) {
|
|
637
|
+
const decision = validatePluginDecision({ ...options, pluginId, project: options.project ? projectIdentity(options.project) : void 0, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
638
|
+
const records = readPluginDecisions().filter((d) => decisionKey(d) !== decisionKey(decision));
|
|
639
|
+
records.push(decision);
|
|
640
|
+
writePluginDecisions(records);
|
|
641
|
+
console.log(options.json ? JSON.stringify(decision, null, 2) : `Keep decision recorded for ${decision.host}:${pluginId}. Safety findings remain active.`);
|
|
642
|
+
}
|
|
643
|
+
function pluginsListCommand(options) {
|
|
644
|
+
const records = readPluginDecisions();
|
|
645
|
+
console.log(options.json ? JSON.stringify({ schemaVersion: 1, records }, null, 2) : records.map((d) => `${d.host}:${d.pluginId} \u2014 ${d.category}${d.project ? ` (${d.project})` : ""}: ${d.reason}`).join("\n") || "No explicit plugin keep decisions.");
|
|
646
|
+
}
|
|
647
|
+
function pluginsForgetCommand(pluginId, options) {
|
|
648
|
+
if (!["claude", "codex"].includes(options.host ?? "")) throw new UserError("--host claude|codex is required.");
|
|
649
|
+
const key = decisionKey({ host: options.host, pluginId, project: options.project ? projectIdentity(options.project) : void 0 });
|
|
650
|
+
const records = readPluginDecisions();
|
|
651
|
+
const remaining = records.filter((d) => decisionKey(d) !== key);
|
|
652
|
+
if (remaining.length === records.length) throw new UserError("No matching keep decision for that host, plugin and project.");
|
|
653
|
+
writePluginDecisions(remaining);
|
|
654
|
+
console.log(options.json ? JSON.stringify({ removed: true, pluginId }) : `Keep decision removed for ${options.host}:${pluginId}.`);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/cli.ts
|
|
658
|
+
import { Command } from "commander";
|
|
659
|
+
import { createRequire } from "module";
|
|
660
|
+
|
|
661
|
+
// src/commands/add.ts
|
|
662
|
+
import { join as join26 } from "path";
|
|
663
|
+
|
|
599
664
|
// src/install/resolve.ts
|
|
665
|
+
import { existsSync as existsSync3 } from "fs";
|
|
600
666
|
var GIT_URL_PATTERN = /^(git@|https?:\/\/|ssh:\/\/)/;
|
|
601
667
|
var LOCAL_PATH_PATTERN = /^(\.{1,2}\/|\/|~\/|~$)/;
|
|
602
668
|
var NPM_SCOPED_PATTERN = /^@[^/]+\/[^/]+$/;
|
|
603
|
-
function
|
|
669
|
+
function resolve2(source, artifact = "skill") {
|
|
604
670
|
const value = source.trim();
|
|
605
671
|
if (value.startsWith("-")) {
|
|
606
672
|
throw new UserError(`skill source cannot start with "-" (looks like a flag, not a source): ${value}`);
|
|
@@ -609,7 +675,7 @@ function resolve(source, artifact = "skill") {
|
|
|
609
675
|
return { type: "git", value };
|
|
610
676
|
}
|
|
611
677
|
const localCandidate = expandHome(value);
|
|
612
|
-
if (
|
|
678
|
+
if (existsSync3(localCandidate) || LOCAL_PATH_PATTERN.test(value)) {
|
|
613
679
|
return { type: "local", value: localCandidate };
|
|
614
680
|
}
|
|
615
681
|
if (artifact === "mcp" && (NPM_SCOPED_PATTERN.test(value) || !value.includes("/"))) {
|
|
@@ -621,14 +687,14 @@ function resolve(source, artifact = "skill") {
|
|
|
621
687
|
// src/install/quarantine.ts
|
|
622
688
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
623
689
|
import { spawn } from "child_process";
|
|
624
|
-
import { cpSync, existsSync as
|
|
625
|
-
import { basename as basename2, join as
|
|
690
|
+
import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2 } from "fs";
|
|
691
|
+
import { basename as basename2, join as join5, resolve as resolvePath } from "path";
|
|
626
692
|
var SKILL_MANIFEST = "SKILL.md";
|
|
627
693
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
628
694
|
async function quarantine(source, quarantineDir, deps = {}) {
|
|
629
695
|
const exec = deps.exec ?? defaultExec;
|
|
630
696
|
const id = randomUUID2();
|
|
631
|
-
const quarantinePath =
|
|
697
|
+
const quarantinePath = join5(quarantineDir, id);
|
|
632
698
|
mkdirSync3(quarantinePath, { recursive: true });
|
|
633
699
|
let packageDir;
|
|
634
700
|
switch (source.type) {
|
|
@@ -644,7 +710,7 @@ async function quarantine(source, quarantineDir, deps = {}) {
|
|
|
644
710
|
break;
|
|
645
711
|
}
|
|
646
712
|
case "git": {
|
|
647
|
-
const clonePath =
|
|
713
|
+
const clonePath = join5(quarantinePath, deriveGitDirName(source.value));
|
|
648
714
|
const result = await exec("git", ["clone", "--depth", "1", "--", source.value, clonePath], {
|
|
649
715
|
cwd: quarantinePath
|
|
650
716
|
});
|
|
@@ -656,11 +722,11 @@ async function quarantine(source, quarantineDir, deps = {}) {
|
|
|
656
722
|
break;
|
|
657
723
|
}
|
|
658
724
|
case "local": {
|
|
659
|
-
if (!
|
|
725
|
+
if (!existsSync4(source.value)) {
|
|
660
726
|
throw new UserError(`local path not found: ${source.value}`);
|
|
661
727
|
}
|
|
662
728
|
const dirName = basename2(resolvePath(source.value));
|
|
663
|
-
cpSync(source.value,
|
|
729
|
+
cpSync(source.value, join5(quarantinePath, dirName), { recursive: true });
|
|
664
730
|
break;
|
|
665
731
|
}
|
|
666
732
|
case "npm": {
|
|
@@ -674,7 +740,7 @@ async function quarantine(source, quarantineDir, deps = {}) {
|
|
|
674
740
|
`npm pack failed for "${source.value}" (exit ${packResult.code}): ${packResult.stderr || packResult.stdout}`
|
|
675
741
|
);
|
|
676
742
|
}
|
|
677
|
-
const tarballPath =
|
|
743
|
+
const tarballPath = join5(quarantinePath, findTarball(quarantinePath, source.value));
|
|
678
744
|
const listResult = await exec("tar", ["-tzf", tarballPath], { cwd: quarantinePath });
|
|
679
745
|
if (listResult.code !== 0) {
|
|
680
746
|
throw new UserError(
|
|
@@ -690,8 +756,8 @@ async function quarantine(source, quarantineDir, deps = {}) {
|
|
|
690
756
|
`tarball extraction failed for "${source.value}" (exit ${extractResult.code}): ${extractResult.stderr || extractResult.stdout}`
|
|
691
757
|
);
|
|
692
758
|
}
|
|
693
|
-
packageDir =
|
|
694
|
-
if (!
|
|
759
|
+
packageDir = join5(quarantinePath, "package");
|
|
760
|
+
if (!existsSync4(packageDir)) {
|
|
695
761
|
throw new UserError(
|
|
696
762
|
`npm pack for "${source.value}" did not produce the expected package/ dir after extraction`
|
|
697
763
|
);
|
|
@@ -705,7 +771,7 @@ async function quarantine(source, quarantineDir, deps = {}) {
|
|
|
705
771
|
return { id, quarantinePath, skillDirs: findSkillDirs(quarantinePath), packageDir, source };
|
|
706
772
|
}
|
|
707
773
|
function listQuarantineIds(quarantineDir) {
|
|
708
|
-
if (!
|
|
774
|
+
if (!existsSync4(quarantineDir)) return [];
|
|
709
775
|
return readdirSync2(quarantineDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
710
776
|
}
|
|
711
777
|
function findSkillDirs(root) {
|
|
@@ -722,7 +788,7 @@ function findSkillDirs(root) {
|
|
|
722
788
|
}
|
|
723
789
|
for (const entry of entries) {
|
|
724
790
|
if (entry.isDirectory() && !SKIP_DIRS2.has(entry.name)) {
|
|
725
|
-
walk2(
|
|
791
|
+
walk2(join5(dir, entry.name));
|
|
726
792
|
}
|
|
727
793
|
}
|
|
728
794
|
}
|
|
@@ -765,27 +831,27 @@ function defaultExec(command, args, options) {
|
|
|
765
831
|
}
|
|
766
832
|
|
|
767
833
|
// src/install/promote.ts
|
|
768
|
-
import { cpSync as cpSync2, existsSync as
|
|
769
|
-
import { basename as basename3, join as
|
|
834
|
+
import { cpSync as cpSync2, existsSync as existsSync5, mkdirSync as mkdirSync4, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
835
|
+
import { basename as basename3, join as join7 } from "path";
|
|
770
836
|
|
|
771
837
|
// src/install/fingerprint.ts
|
|
772
838
|
import { createHash } from "crypto";
|
|
773
|
-
import { lstatSync as lstatSync2, readFileSync as
|
|
774
|
-
import { isAbsolute, join as
|
|
839
|
+
import { lstatSync as lstatSync2, readFileSync as readFileSync4, readdirSync as readdirSync3, readlinkSync, realpathSync as realpathSync2 } from "fs";
|
|
840
|
+
import { isAbsolute as isAbsolute2, join as join6, relative, sep } from "path";
|
|
775
841
|
var SKIP_DIRS3 = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
776
842
|
function isInside(child, root) {
|
|
777
843
|
const rel = relative(root, child);
|
|
778
|
-
return rel === "" || !rel.startsWith(".." + sep) && rel !== ".." && !
|
|
844
|
+
return rel === "" || !rel.startsWith(".." + sep) && rel !== ".." && !isAbsolute2(rel);
|
|
779
845
|
}
|
|
780
846
|
function fingerprintTree(root) {
|
|
781
847
|
const hash = createHash("sha256");
|
|
782
848
|
const symlinks = [];
|
|
783
849
|
const escaping = [];
|
|
784
|
-
const realRoot =
|
|
850
|
+
const realRoot = realpathSync2(root);
|
|
785
851
|
function walk2(dir) {
|
|
786
852
|
const entries = readdirSync3(dir, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
787
853
|
for (const entry of entries) {
|
|
788
|
-
const full =
|
|
854
|
+
const full = join6(dir, entry.name);
|
|
789
855
|
const rel = relative(root, full);
|
|
790
856
|
if (entry.isSymbolicLink()) {
|
|
791
857
|
const target = readlinkSync(full);
|
|
@@ -793,7 +859,7 @@ function fingerprintTree(root) {
|
|
|
793
859
|
hash.update(`L ${rel} ${target}
|
|
794
860
|
`);
|
|
795
861
|
try {
|
|
796
|
-
if (!isInside(
|
|
862
|
+
if (!isInside(realpathSync2(full), realRoot)) escaping.push(rel);
|
|
797
863
|
} catch {
|
|
798
864
|
escaping.push(rel);
|
|
799
865
|
}
|
|
@@ -807,7 +873,7 @@ function fingerprintTree(root) {
|
|
|
807
873
|
continue;
|
|
808
874
|
}
|
|
809
875
|
if (entry.isFile()) {
|
|
810
|
-
const contentHash = createHash("sha256").update(
|
|
876
|
+
const contentHash = createHash("sha256").update(readFileSync4(full)).digest("hex");
|
|
811
877
|
hash.update(`F ${rel} ${contentHash}
|
|
812
878
|
`);
|
|
813
879
|
continue;
|
|
@@ -856,8 +922,8 @@ function promote(entry, targetSkillsRoot, options = {}) {
|
|
|
856
922
|
`refusing to promote ${skillDir}: it is no longer a real directory (a symlink or a deleted path) \u2014 re-run the gate on this candidate`
|
|
857
923
|
);
|
|
858
924
|
}
|
|
859
|
-
const dest =
|
|
860
|
-
if (
|
|
925
|
+
const dest = join7(targetSkillsRoot, basename3(skillDir));
|
|
926
|
+
if (existsSync5(dest) && !force) {
|
|
861
927
|
throw new Error(`Refusing to overwrite existing skill dir: ${dest} (pass force to override)`);
|
|
862
928
|
}
|
|
863
929
|
}
|
|
@@ -876,8 +942,8 @@ function promote(entry, targetSkillsRoot, options = {}) {
|
|
|
876
942
|
mkdirSync4(targetSkillsRoot, { recursive: true });
|
|
877
943
|
const promoted = [];
|
|
878
944
|
for (const skillDir of entry.skillDirs) {
|
|
879
|
-
const dest =
|
|
880
|
-
if (
|
|
945
|
+
const dest = join7(targetSkillsRoot, basename3(skillDir));
|
|
946
|
+
if (existsSync5(dest)) {
|
|
881
947
|
rmSync2(dest, { recursive: true, force: true });
|
|
882
948
|
}
|
|
883
949
|
moveDir(skillDir, dest);
|
|
@@ -905,7 +971,7 @@ function moveDir(src, dest) {
|
|
|
905
971
|
}
|
|
906
972
|
|
|
907
973
|
// src/install/promoteMcp.ts
|
|
908
|
-
import { existsSync as
|
|
974
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
909
975
|
import { dirname as dirname2 } from "path";
|
|
910
976
|
function promoteMcp(_entry, profile, targetConfigPath, options = {}) {
|
|
911
977
|
const force = options.force ?? false;
|
|
@@ -913,8 +979,8 @@ function promoteMcp(_entry, profile, targetConfigPath, options = {}) {
|
|
|
913
979
|
const { command, args, envVarsToFill, autoPinned } = deriveServerEntry(profile);
|
|
914
980
|
let config = {};
|
|
915
981
|
let rawExisting;
|
|
916
|
-
if (
|
|
917
|
-
rawExisting =
|
|
982
|
+
if (existsSync6(targetConfigPath)) {
|
|
983
|
+
rawExisting = readFileSync5(targetConfigPath, "utf-8");
|
|
918
984
|
try {
|
|
919
985
|
config = JSON.parse(rawExisting);
|
|
920
986
|
} catch (err) {
|
|
@@ -998,11 +1064,11 @@ function deriveServerEntry(profile) {
|
|
|
998
1064
|
}
|
|
999
1065
|
|
|
1000
1066
|
// src/install/mcpCandidate.ts
|
|
1001
|
-
import { readFileSync as
|
|
1002
|
-
import { dirname as dirname3, join as
|
|
1067
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1068
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
1003
1069
|
function readPackageInfo(pkgPath) {
|
|
1004
1070
|
try {
|
|
1005
|
-
const pkg2 = JSON.parse(
|
|
1071
|
+
const pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
1006
1072
|
return {
|
|
1007
1073
|
name: typeof pkg2.name === "string" ? pkg2.name : void 0,
|
|
1008
1074
|
version: typeof pkg2.version === "string" ? pkg2.version : void 0
|
|
@@ -1013,7 +1079,7 @@ function readPackageInfo(pkgPath) {
|
|
|
1013
1079
|
}
|
|
1014
1080
|
function readConfigEntry(configPath, name) {
|
|
1015
1081
|
try {
|
|
1016
|
-
const cfg = JSON.parse(
|
|
1082
|
+
const cfg = JSON.parse(readFileSync6(configPath, "utf-8"));
|
|
1017
1083
|
const servers = cfg.mcpServers ?? cfg.servers ?? {};
|
|
1018
1084
|
const raw = servers[name] ?? {};
|
|
1019
1085
|
return {
|
|
@@ -1031,7 +1097,7 @@ function resolveMcpCandidateDetail(profile) {
|
|
|
1031
1097
|
return { name, version: readPackageInfo(profile.config).version };
|
|
1032
1098
|
}
|
|
1033
1099
|
const entry = readConfigEntry(profile.config, name);
|
|
1034
|
-
const sidecar = readPackageInfo(
|
|
1100
|
+
const sidecar = readPackageInfo(join8(dirname3(profile.config), "package.json"));
|
|
1035
1101
|
return {
|
|
1036
1102
|
name,
|
|
1037
1103
|
command: entry?.command,
|
|
@@ -1043,8 +1109,8 @@ function resolveMcpCandidateDetail(profile) {
|
|
|
1043
1109
|
}
|
|
1044
1110
|
|
|
1045
1111
|
// src/provenance.ts
|
|
1046
|
-
import { appendFileSync, existsSync as
|
|
1047
|
-
import { join as
|
|
1112
|
+
import { appendFileSync, existsSync as existsSync7, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1113
|
+
import { join as join9 } from "path";
|
|
1048
1114
|
var FORGE_VERBS = ["DEFER", "ABSORB", "FORK", "REJECT", "WATCH"];
|
|
1049
1115
|
var LEDGER_HEADER = "# Skill Forge \u2014 Provenance Ledger\n\nOne entry per external-skill ingestion decision.\n";
|
|
1050
1116
|
function todayLocalIsoDate() {
|
|
@@ -1058,7 +1124,7 @@ function appendProvenance(entry, skillsRoot) {
|
|
|
1058
1124
|
if (!FORGE_VERBS.includes(entry.verb)) {
|
|
1059
1125
|
throw new Error(`verb must be one of ${FORGE_VERBS.join(", ")}`);
|
|
1060
1126
|
}
|
|
1061
|
-
if (!
|
|
1127
|
+
if (!existsSync7(skillsRoot) || !statSync3(skillsRoot).isDirectory()) {
|
|
1062
1128
|
throw new Error(`skills-root not a directory: ${skillsRoot}`);
|
|
1063
1129
|
}
|
|
1064
1130
|
const today = todayLocalIsoDate();
|
|
@@ -1075,8 +1141,8 @@ function appendProvenance(entry, skillsRoot) {
|
|
|
1075
1141
|
- **Drift check:** \`${driftCheck}\`
|
|
1076
1142
|
- **Notes:** ${entry.notes ?? ""}
|
|
1077
1143
|
`;
|
|
1078
|
-
const ledgerPath =
|
|
1079
|
-
if (!
|
|
1144
|
+
const ledgerPath = join9(skillsRoot, "SOURCES.md");
|
|
1145
|
+
if (!existsSync7(ledgerPath)) {
|
|
1080
1146
|
writeFileSync4(ledgerPath, LEDGER_HEADER, "utf-8");
|
|
1081
1147
|
}
|
|
1082
1148
|
appendFileSync(ledgerPath, block, "utf-8");
|
|
@@ -1123,11 +1189,11 @@ function parseProvenanceLedger(text) {
|
|
|
1123
1189
|
return { entries, warnings };
|
|
1124
1190
|
}
|
|
1125
1191
|
function readProvenanceLedger(skillsRoot) {
|
|
1126
|
-
const ledgerPath =
|
|
1127
|
-
if (!
|
|
1192
|
+
const ledgerPath = join9(skillsRoot, "SOURCES.md");
|
|
1193
|
+
if (!existsSync7(ledgerPath)) {
|
|
1128
1194
|
return { ledgerPath, exists: false, entries: [], warnings: [] };
|
|
1129
1195
|
}
|
|
1130
|
-
const text =
|
|
1196
|
+
const text = readFileSync7(ledgerPath, "utf-8");
|
|
1131
1197
|
const parsed = parseProvenanceLedger(text);
|
|
1132
1198
|
return { ledgerPath, exists: true, ...parsed };
|
|
1133
1199
|
}
|
|
@@ -1136,28 +1202,134 @@ function readProvenanceLedger(skillsRoot) {
|
|
|
1136
1202
|
import { readdirSync as readdirSync7 } from "fs";
|
|
1137
1203
|
|
|
1138
1204
|
// src/auditState.ts
|
|
1139
|
-
import { existsSync as
|
|
1140
|
-
import { basename as basename7, dirname as dirname5, join as
|
|
1205
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
|
|
1206
|
+
import { basename as basename7, dirname as dirname5, join as join14 } from "path";
|
|
1207
|
+
|
|
1208
|
+
// src/auditComparison.ts
|
|
1209
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
1210
|
+
import { relative as relative2, sep as sep2 } from "path";
|
|
1211
|
+
function targetIdentity(report, target) {
|
|
1212
|
+
for (const p of report.plugins ?? []) {
|
|
1213
|
+
const rel = relative2(p.installPath, target);
|
|
1214
|
+
if (rel !== ".." && !rel.startsWith(".." + sep2) && !rel.startsWith(sep2)) return `${p.host ?? "claude"}:${p.pluginId}/${rel}`;
|
|
1215
|
+
}
|
|
1216
|
+
return target;
|
|
1217
|
+
}
|
|
1218
|
+
function scope(report) {
|
|
1219
|
+
return JSON.stringify({
|
|
1220
|
+
host: report.hostInventory?.host ?? (report.plugins ? "claude" : "configured"),
|
|
1221
|
+
roots: report.inventory.skillRoots.filter((r) => r.origin !== "plugin").map((r) => r.root).sort(),
|
|
1222
|
+
skills: report.inventory.skills.map((s) => targetIdentity(report, s.path)).sort(),
|
|
1223
|
+
mcp: report.inventory.mcpTargets.map((m) => m.path).sort(),
|
|
1224
|
+
plugins: report.plugins?.map((p) => p.pluginId).sort()
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
function incomplete(report) {
|
|
1228
|
+
return report.hostInventory?.complete === false || report.hygiene.some((f) => /unreadable|missing|symlink|parse|invalid/.test(f.rule));
|
|
1229
|
+
}
|
|
1230
|
+
function compareAudits(current, previous) {
|
|
1231
|
+
const result = { baselineAt: previous?.generatedAt, comparable: false, added: [], changed: [], resolved: [], unchanged: 0 };
|
|
1232
|
+
if (!previous || previous.schemaVersion !== 1 || !previous.inventory || !Array.isArray(previous.hygiene) || !Array.isArray(previous.inventory.skills) || !Array.isArray(previous.inventory.skillRoots) || !Array.isArray(previous.inventory.mcpTargets)) {
|
|
1233
|
+
return { ...result, reason: "Baseline must be a full schema-v1 audit JSON, not a routine/audit-state summary." };
|
|
1234
|
+
}
|
|
1235
|
+
if (!Number.isFinite(Date.parse(previous.generatedAt)) || Date.parse(previous.generatedAt) > Date.parse(current.generatedAt)) return { ...result, reason: "Baseline timestamp is invalid or newer than the current audit." };
|
|
1236
|
+
if (incomplete(current) || incomplete(previous) || scope(current) !== scope(previous)) {
|
|
1237
|
+
return { ...result, reason: "Inventory scope changed or a scan is incomplete; no resolution claims can be made. Review the full findings." };
|
|
1238
|
+
}
|
|
1239
|
+
const groups = (report) => {
|
|
1240
|
+
const map = /* @__PURE__ */ new Map();
|
|
1241
|
+
for (const f of report.hygiene) {
|
|
1242
|
+
const key = JSON.stringify([targetIdentity(report, f.target), f.rule]);
|
|
1243
|
+
map.set(key, [...map.get(key) ?? [], f]);
|
|
1244
|
+
}
|
|
1245
|
+
return map;
|
|
1246
|
+
};
|
|
1247
|
+
const old = groups(previous), next = groups(current);
|
|
1248
|
+
const content = (f) => JSON.stringify([f.severity, f.context ?? f.detail, f.classification ?? null]);
|
|
1249
|
+
for (const key of /* @__PURE__ */ new Set([...old.keys(), ...next.keys()])) {
|
|
1250
|
+
const before = [...old.get(key) ?? []];
|
|
1251
|
+
const after = [...next.get(key) ?? []];
|
|
1252
|
+
for (let i = after.length - 1; i >= 0; i--) {
|
|
1253
|
+
const match = before.findIndex((f) => content(f) === content(after[i]));
|
|
1254
|
+
if (match >= 0) {
|
|
1255
|
+
before.splice(match, 1);
|
|
1256
|
+
after.splice(i, 1);
|
|
1257
|
+
result.unchanged++;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
for (let i = after.length - 1; i >= 0; i--) {
|
|
1261
|
+
const identity = after[i].context ?? after[i].detail;
|
|
1262
|
+
const matches = before.map((f, n) => (f.context ?? f.detail) === identity ? n : -1).filter((n) => n >= 0);
|
|
1263
|
+
if (matches.length === 1 && after.filter((f) => (f.context ?? f.detail) === identity).length === 1) {
|
|
1264
|
+
result.changed.push({ before: before.splice(matches[0], 1)[0], after: after.splice(i, 1)[0] });
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if (before.length === 1 && after.length === 1) result.changed.push({ before: before[0], after: after[0] });
|
|
1268
|
+
else {
|
|
1269
|
+
result.added.push(...after);
|
|
1270
|
+
result.resolved.push(...before);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
result.comparable = true;
|
|
1274
|
+
const previousSkills = new Map(previous.inventory.skills.map((skill) => [skill.path, skill]));
|
|
1275
|
+
let coveredSkills = 0;
|
|
1276
|
+
let descriptionCodePointDelta = 0;
|
|
1277
|
+
let entrypointCodePointDelta = 0;
|
|
1278
|
+
for (const skill of current.inventory.skills) {
|
|
1279
|
+
const prior = previousSkills.get(skill.path);
|
|
1280
|
+
const currentDiagnostics = skill.instructionDiagnostics;
|
|
1281
|
+
const priorDiagnostics = prior?.instructionDiagnostics;
|
|
1282
|
+
if (!prior || !currentDiagnostics || !priorDiagnostics) continue;
|
|
1283
|
+
coveredSkills++;
|
|
1284
|
+
descriptionCodePointDelta += (currentDiagnostics.discovery.description?.unicodeCodePoints ?? 0) - (priorDiagnostics.discovery.description?.unicodeCodePoints ?? 0);
|
|
1285
|
+
entrypointCodePointDelta += currentDiagnostics.entrypoint.unicodeCodePoints - priorDiagnostics.entrypoint.unicodeCodePoints;
|
|
1286
|
+
}
|
|
1287
|
+
result.instructionDiagnostics = coveredSkills > 0 ? { comparable: true, hostLoad: "unknown", coveredSkills, descriptionCodePointDelta, entrypointCodePointDelta } : { comparable: false, hostLoad: "unknown", reason: "Instruction diagnostics are unavailable in one or both report inventories.", coveredSkills: 0 };
|
|
1288
|
+
return result;
|
|
1289
|
+
}
|
|
1290
|
+
function loadAuditComparison(current, path) {
|
|
1291
|
+
try {
|
|
1292
|
+
return compareAudits(current, JSON.parse(readFileSync8(path, "utf8")));
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
return { comparable: false, reason: `Baseline unusable: ${error.message}`, added: [], changed: [], resolved: [], unchanged: 0 };
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
function renderAuditComparison(comparison) {
|
|
1298
|
+
const c = comparison;
|
|
1299
|
+
return ["## Changes since prior audit", "", ...c.comparable ? [
|
|
1300
|
+
`${c.added.length} new \xB7 ${c.changed.length} changed \xB7 ${c.resolved.length} no longer active \xB7 ${c.unchanged} unchanged.`,
|
|
1301
|
+
...c.instructionDiagnostics?.comparable ? [`Instruction diagnostics: ${c.instructionDiagnostics.coveredSkills} shared skill(s); description ${signed(c.instructionDiagnostics.descriptionCodePointDelta ?? 0)} code points, entrypoint ${signed(c.instructionDiagnostics.entrypointCodePointDelta ?? 0)} code points.`] : c.instructionDiagnostics ? [`Instruction diagnostics unavailable: ${c.instructionDiagnostics.reason}`] : [],
|
|
1302
|
+
"No longer active can include accepted findings; it does not prove a security fix.",
|
|
1303
|
+
"",
|
|
1304
|
+
...c.added.map((f) => `- NEW ${f.severity} [${sanitizeForMarkdown(f.rule)}] ${sanitizeForMarkdown(f.target)}`),
|
|
1305
|
+
...c.changed.map((f) => `- CHANGED ${f.before.severity} \u2192 ${f.after.severity} [${sanitizeForMarkdown(f.after.rule)}] ${sanitizeForMarkdown(f.after.target)}`),
|
|
1306
|
+
...c.resolved.map((f) => `- NO LONGER ACTIVE [${sanitizeForMarkdown(f.rule)}] ${sanitizeForMarkdown(f.target)}`)
|
|
1307
|
+
] : [sanitizeForMarkdown(c.reason ?? "Comparison unavailable.")], ""];
|
|
1308
|
+
}
|
|
1309
|
+
function signed(value) {
|
|
1310
|
+
return value >= 0 ? `+${value}` : String(value);
|
|
1311
|
+
}
|
|
1141
1312
|
|
|
1142
1313
|
// src/audit.ts
|
|
1143
1314
|
import {
|
|
1144
|
-
existsSync as
|
|
1315
|
+
existsSync as existsSync10,
|
|
1145
1316
|
lstatSync as lstatSync3,
|
|
1146
1317
|
mkdtempSync,
|
|
1147
|
-
|
|
1318
|
+
opendirSync,
|
|
1319
|
+
readFileSync as readFileSync11,
|
|
1148
1320
|
readdirSync as readdirSync5,
|
|
1149
|
-
realpathSync as
|
|
1321
|
+
realpathSync as realpathSync3,
|
|
1150
1322
|
renameSync as renameSync3,
|
|
1151
1323
|
rmSync as rmSync3,
|
|
1152
1324
|
statSync as statSync6,
|
|
1153
1325
|
writeFileSync as writeFileSync5
|
|
1154
1326
|
} from "fs";
|
|
1155
|
-
import { basename as basename6, delimiter as delimiter3, extname as extname4, join as
|
|
1327
|
+
import { basename as basename6, delimiter as delimiter3, extname as extname4, join as join13, resolve as resolvePath2, sep as sep3 } from "path";
|
|
1156
1328
|
|
|
1157
1329
|
// src/gate/safety.ts
|
|
1158
1330
|
import { statSync as statSync4 } from "fs";
|
|
1159
1331
|
import { execFile } from "child_process";
|
|
1160
|
-
import { delimiter as delimiter2, extname as extname2, join as
|
|
1332
|
+
import { delimiter as delimiter2, extname as extname2, join as join10 } from "path";
|
|
1161
1333
|
import { promisify } from "util";
|
|
1162
1334
|
var execFileAsync = promisify(execFile);
|
|
1163
1335
|
var RM_RF_TARGET_RE = /\brm\s+(?:-[a-z]+\s+)+("[^"]*"|'[^']*'|[^\s;|&)"'`]+)/i;
|
|
@@ -1203,6 +1375,10 @@ function hasUnnegatedInstallCue(line) {
|
|
|
1203
1375
|
}
|
|
1204
1376
|
function refineCurlPipeShell(line, ctx) {
|
|
1205
1377
|
const base = { severity: "CRITICAL", message: "downloads a remote script and pipes it directly into a shell" };
|
|
1378
|
+
const pipelines = [...line.matchAll(/\b(?:curl|wget)\b[^\n|]*\|\s*(?:sudo\s+)?(?:bash|sh|zsh)\b([^|;\n]*)/gi)];
|
|
1379
|
+
if (!/\$\(|`/.test(line) && pipelines.length && pipelines.every((m) => /^\s+(?:\.\/)?(?:[a-zA-Z0-9_-]+\/)*[a-zA-Z0-9_-]+\.sh(?:\s|$)/.test(m[1]))) {
|
|
1380
|
+
return { ...base, message: "passes downloaded data to a named local shell script; inspect that script for evaluation of stdin before use", classification: "local-script-input" };
|
|
1381
|
+
}
|
|
1206
1382
|
if (ctx.inFence) return base;
|
|
1207
1383
|
if (!DOC_EXTENSIONS.has(extname2(ctx.file).toLowerCase())) return base;
|
|
1208
1384
|
if (!DOC_CUE_RE.test(line)) return base;
|
|
@@ -1291,7 +1467,7 @@ var DENY_RULES = [
|
|
|
1291
1467
|
message: "invokes sudo \u2014 review for unnecessary privilege escalation"
|
|
1292
1468
|
}
|
|
1293
1469
|
];
|
|
1294
|
-
function scanStaticRules(targetPath) {
|
|
1470
|
+
function scanStaticRules(targetPath, capturedFiles) {
|
|
1295
1471
|
const findings = [];
|
|
1296
1472
|
let root;
|
|
1297
1473
|
try {
|
|
@@ -1300,8 +1476,8 @@ function scanStaticRules(targetPath) {
|
|
|
1300
1476
|
return findings;
|
|
1301
1477
|
}
|
|
1302
1478
|
const files = root.isDirectory() ? Array.from(walk(targetPath)) : [targetPath];
|
|
1303
|
-
|
|
1304
|
-
|
|
1479
|
+
const sources = capturedFiles ?? files.map((file) => ({ file, content: readTextFile(file) }));
|
|
1480
|
+
for (const { file, content } of sources) {
|
|
1305
1481
|
if (content === void 0) continue;
|
|
1306
1482
|
const lines = content.split("\n");
|
|
1307
1483
|
const fenceFlags = DOC_EXTENSIONS.has(extname2(file).toLowerCase()) ? computeFenceFlags(lines) : void 0;
|
|
@@ -1330,7 +1506,7 @@ function which(cmd) {
|
|
|
1330
1506
|
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
1331
1507
|
for (const dir of dirs) {
|
|
1332
1508
|
for (const ext of exts) {
|
|
1333
|
-
const candidate =
|
|
1509
|
+
const candidate = join10(dir, cmd + ext);
|
|
1334
1510
|
try {
|
|
1335
1511
|
if (statSync4(candidate).isFile()) return candidate;
|
|
1336
1512
|
} catch {
|
|
@@ -1364,8 +1540,8 @@ async function scanWithSkillSpector(targetPath) {
|
|
|
1364
1540
|
return [];
|
|
1365
1541
|
}
|
|
1366
1542
|
}
|
|
1367
|
-
async function scanSafety(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
1368
|
-
const findings = [...scanStaticRules(targetPath), ...await scanWithSkillSpector(targetPath)];
|
|
1543
|
+
async function scanSafety(targetPath, strictness = DEFAULT_STRICTNESS, capturedFiles) {
|
|
1544
|
+
const findings = [...scanStaticRules(targetPath, capturedFiles), ...await scanWithSkillSpector(targetPath)];
|
|
1369
1545
|
return { findings, verdict: verdictFor(findings, strictness) };
|
|
1370
1546
|
}
|
|
1371
1547
|
|
|
@@ -1605,8 +1781,8 @@ async function mcpScan(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
|
1605
1781
|
}
|
|
1606
1782
|
|
|
1607
1783
|
// src/gate/overlap.ts
|
|
1608
|
-
import { existsSync as
|
|
1609
|
-
import { basename as basename5, dirname as dirname4, join as
|
|
1784
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9, readdirSync as readdirSync4 } from "fs";
|
|
1785
|
+
import { basename as basename5, dirname as dirname4, join as join11, resolve as resolve3 } from "path";
|
|
1610
1786
|
var STOPWORDS = new Set(
|
|
1611
1787
|
`
|
|
1612
1788
|
a an the and or of to for in on with use used using when whether this that these those is are be
|
|
@@ -1615,20 +1791,20 @@ skill skills user claude code based comprehensive enhanced practical including i
|
|
|
1615
1791
|
`.split(/\s+/).filter(Boolean)
|
|
1616
1792
|
);
|
|
1617
1793
|
var DEFAULT_SET_THRESHOLD = 0.45;
|
|
1618
|
-
function splitLimited(text,
|
|
1794
|
+
function splitLimited(text, sep11, maxSplit) {
|
|
1619
1795
|
const parts = [];
|
|
1620
1796
|
let rest = text;
|
|
1621
1797
|
for (let i = 0; i < maxSplit; i++) {
|
|
1622
|
-
const idx = rest.indexOf(
|
|
1798
|
+
const idx = rest.indexOf(sep11);
|
|
1623
1799
|
if (idx === -1) break;
|
|
1624
1800
|
parts.push(rest.slice(0, idx));
|
|
1625
|
-
rest = rest.slice(idx +
|
|
1801
|
+
rest = rest.slice(idx + sep11.length);
|
|
1626
1802
|
}
|
|
1627
1803
|
parts.push(rest);
|
|
1628
1804
|
return parts;
|
|
1629
1805
|
}
|
|
1630
1806
|
function readFrontmatter(skillMdPath) {
|
|
1631
|
-
const text =
|
|
1807
|
+
const text = readFileSync9(skillMdPath, "utf-8");
|
|
1632
1808
|
const meta = { name: basename5(dirname4(skillMdPath)), description: "" };
|
|
1633
1809
|
if (text.startsWith("---\n")) {
|
|
1634
1810
|
const block = splitLimited(text, "---", 2)[1] ?? "";
|
|
@@ -1723,47 +1899,47 @@ function suggestVerbCode(topScore) {
|
|
|
1723
1899
|
return verbTier(topScore).verb;
|
|
1724
1900
|
}
|
|
1725
1901
|
function listSubdirs(dir) {
|
|
1726
|
-
if (!
|
|
1902
|
+
if (!existsSync8(dir)) return [];
|
|
1727
1903
|
let entries;
|
|
1728
1904
|
try {
|
|
1729
1905
|
entries = readdirSync4(dir, { withFileTypes: true });
|
|
1730
1906
|
} catch {
|
|
1731
1907
|
return [];
|
|
1732
1908
|
}
|
|
1733
|
-
return entries.filter((e) => e.isDirectory()).map((e) =>
|
|
1909
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join11(dir, e.name));
|
|
1734
1910
|
}
|
|
1735
1911
|
function findSkillMdFiles(root) {
|
|
1736
1912
|
const found = /* @__PURE__ */ new Set();
|
|
1737
1913
|
for (const dir of listSubdirs(root)) {
|
|
1738
|
-
const candidate =
|
|
1739
|
-
if (
|
|
1914
|
+
const candidate = join11(dir, "SKILL.md");
|
|
1915
|
+
if (existsSync8(candidate)) found.add(resolve3(candidate));
|
|
1740
1916
|
}
|
|
1741
|
-
for (const dir of listSubdirs(
|
|
1742
|
-
const candidate =
|
|
1743
|
-
if (
|
|
1917
|
+
for (const dir of listSubdirs(join11(root, "skills"))) {
|
|
1918
|
+
const candidate = join11(dir, "SKILL.md");
|
|
1919
|
+
if (existsSync8(candidate)) found.add(resolve3(candidate));
|
|
1744
1920
|
}
|
|
1745
1921
|
for (const topDir of listSubdirs(root)) {
|
|
1746
|
-
for (const dir of listSubdirs(
|
|
1747
|
-
const candidate =
|
|
1748
|
-
if (
|
|
1922
|
+
for (const dir of listSubdirs(join11(topDir, "skills"))) {
|
|
1923
|
+
const candidate = join11(dir, "SKILL.md");
|
|
1924
|
+
if (existsSync8(candidate)) found.add(resolve3(candidate));
|
|
1749
1925
|
}
|
|
1750
1926
|
}
|
|
1751
1927
|
return Array.from(found).filter((p) => !p.includes("/.git/")).sort();
|
|
1752
1928
|
}
|
|
1753
1929
|
function scanOverlap(candidatePath, skillsRoot) {
|
|
1754
|
-
const root =
|
|
1930
|
+
const root = resolve3(expandHome(skillsRoot));
|
|
1755
1931
|
if (!isDir(root)) {
|
|
1756
1932
|
throw new Error(`skills-root not a directory: ${root}`);
|
|
1757
1933
|
}
|
|
1758
|
-
let cpath =
|
|
1934
|
+
let cpath = resolve3(expandHome(candidatePath));
|
|
1759
1935
|
if (isDir(cpath)) {
|
|
1760
|
-
cpath =
|
|
1936
|
+
cpath = join11(cpath, "SKILL.md");
|
|
1761
1937
|
}
|
|
1762
|
-
if (!
|
|
1938
|
+
if (!existsSync8(cpath)) {
|
|
1763
1939
|
throw new Error(`candidate SKILL.md not found: ${cpath}`);
|
|
1764
1940
|
}
|
|
1765
1941
|
const cand = readFrontmatter(cpath);
|
|
1766
|
-
const existing = findSkillMdFiles(root).filter((p) => p !==
|
|
1942
|
+
const existing = findSkillMdFiles(root).filter((p) => p !== resolve3(cpath));
|
|
1767
1943
|
if (existing.length === 0) {
|
|
1768
1944
|
throw new Error(`no existing skills found under ${root}`);
|
|
1769
1945
|
}
|
|
@@ -1804,8 +1980,8 @@ function scanOverlapPaths(skillMdPaths, threshold = DEFAULT_SET_THRESHOLD) {
|
|
|
1804
1980
|
|
|
1805
1981
|
// src/findings.ts
|
|
1806
1982
|
import { createHash as createHash2 } from "crypto";
|
|
1807
|
-
import { existsSync as
|
|
1808
|
-
import { join as
|
|
1983
|
+
import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
|
|
1984
|
+
import { join as join12 } from "path";
|
|
1809
1985
|
var FINGERPRINT_DISPLAY_LENGTH = 12;
|
|
1810
1986
|
var MIN_FINGERPRINT_PREFIX_LENGTH = 8;
|
|
1811
1987
|
var FINGERPRINT_HEX_RE = /^[0-9a-f]{64}$/;
|
|
@@ -1818,7 +1994,7 @@ function fingerprintFinding(f) {
|
|
|
1818
1994
|
return createHash2("sha256").update(`${f.severity}\0${f.rule}\0${f.target}\0${f.context}`).digest("hex");
|
|
1819
1995
|
}
|
|
1820
1996
|
function getAcceptedFindingsPath() {
|
|
1821
|
-
return
|
|
1997
|
+
return join12(getHomeDir(), "accepted-findings.json");
|
|
1822
1998
|
}
|
|
1823
1999
|
function invalidRecordReason(record) {
|
|
1824
2000
|
if (typeof record !== "object" || record === null) return "not an object";
|
|
@@ -1838,10 +2014,10 @@ function invalidRecordReason(record) {
|
|
|
1838
2014
|
}
|
|
1839
2015
|
function readAcceptedFindings() {
|
|
1840
2016
|
const path = getAcceptedFindingsPath();
|
|
1841
|
-
if (!
|
|
2017
|
+
if (!existsSync9(path)) return { records: [], notices: [] };
|
|
1842
2018
|
let parsed;
|
|
1843
2019
|
try {
|
|
1844
|
-
parsed = JSON.parse(
|
|
2020
|
+
parsed = JSON.parse(readFileSync10(path, "utf-8"));
|
|
1845
2021
|
} catch {
|
|
1846
2022
|
return { records: [], notices: ["accepted-findings store unreadable (corrupt JSON) \u2014 nothing suppressed this run"] };
|
|
1847
2023
|
}
|
|
@@ -1971,6 +2147,43 @@ function proFeatureStatus(feature, betaFree = BETA_FREE) {
|
|
|
1971
2147
|
};
|
|
1972
2148
|
}
|
|
1973
2149
|
|
|
2150
|
+
// src/instructionDiagnostics.ts
|
|
2151
|
+
function instructionTextMetrics(text) {
|
|
2152
|
+
const utf8Bytes = Buffer.byteLength(text, "utf8");
|
|
2153
|
+
return { utf8Bytes, unicodeCodePoints: Array.from(text).length, words: text.match(/\S+/gu)?.length ?? 0, estimatedTokens: Math.ceil(utf8Bytes / 4) };
|
|
2154
|
+
}
|
|
2155
|
+
function skillEntrypointBody(text) {
|
|
2156
|
+
const opening = text.match(/^---[ \t]*\r?\n/);
|
|
2157
|
+
if (!opening) return text;
|
|
2158
|
+
const closing = /^---[ \t]*(?:\r?\n|$)/gm;
|
|
2159
|
+
closing.lastIndex = opening[0].length;
|
|
2160
|
+
const match = closing.exec(text);
|
|
2161
|
+
return match ? text.slice(match.index + match[0].length) : text;
|
|
2162
|
+
}
|
|
2163
|
+
function diagnoseInstructions(name, description, entrypoint, resources) {
|
|
2164
|
+
const inputs = Array.isArray(resources) ? resources : Object.entries(resources).map(([directory, files]) => ({ directory, files }));
|
|
2165
|
+
const inventory = [...inputs].sort((a, b) => a.directory.localeCompare(b.directory)).map(({ directory, files, unavailableReason }) => ({
|
|
2166
|
+
directory,
|
|
2167
|
+
...unavailableReason ? { status: "unavailable", reason: unavailableReason } : { status: "available", files: files ?? 0 },
|
|
2168
|
+
routedFromEntrypoint: new RegExp(`(?:^|[^A-Za-z0-9_-])${escapeRegExp(directory)}/`, "m").test(entrypoint)
|
|
2169
|
+
}));
|
|
2170
|
+
const advice = [];
|
|
2171
|
+
if (description && instructionTextMetrics(description).unicodeCodePoints > 500) advice.push({ kind: "long-description", detail: "description exceeds the advisory 500-code-point review bucket" });
|
|
2172
|
+
if (description && /\b(?:always|any|every|all)\b[^.\n]{0,90}\b(?:request|task|work|question)\b/i.test(description)) advice.push({ kind: "broad-trigger-wording", detail: "description contains broad/catchall trigger wording; review selection scope" });
|
|
2173
|
+
const entrypointMetrics = instructionTextMetrics(entrypoint);
|
|
2174
|
+
const entrypointLines = entrypoint === "" ? 0 : entrypoint.split(/\r\n|\r|\n/).length;
|
|
2175
|
+
if (entrypointLines > 500 || entrypointMetrics.words > 4e3) advice.push({ kind: "dense-entrypoint", detail: `entrypoint has ${entrypointLines} lines and ${entrypointMetrics.words} words; review routing before moving content` });
|
|
2176
|
+
const unavailable = inventory.filter((resource) => resource.status === "unavailable");
|
|
2177
|
+
if (unavailable.length > 0) advice.push({ kind: "resource-inventory-unavailable", detail: `resource inventory unavailable for ${unavailable.map((resource) => resource.directory).join(", ")}; no file count was inferred` });
|
|
2178
|
+
const unrouted = inventory.filter((resource) => resource.status === "available" && !resource.routedFromEntrypoint);
|
|
2179
|
+
if (unrouted.length > 0) advice.push({ kind: "unrouted-resources", detail: `entrypoint does not name ${unrouted.map((resource) => resource.directory).join(", ")}; confirm conditional routing or document it` });
|
|
2180
|
+
if (/\b(?:always|must)\s+(?:read|load)\b[^\n]*(?:references|reference|assets|templates|scripts|commands|hooks)\//i.test(entrypoint)) advice.push({ kind: "unconditional-resource-routing", detail: "entrypoint appears to require resource loading unconditionally; review whether a narrower trigger is possible" });
|
|
2181
|
+
return { hostLoad: "unknown", discovery: { ...name ? { name: instructionTextMetrics(name) } : {}, ...description ? { description: instructionTextMetrics(description) } : {} }, entrypoint: entrypointMetrics, resources: inventory, advice };
|
|
2182
|
+
}
|
|
2183
|
+
function escapeRegExp(value) {
|
|
2184
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2185
|
+
}
|
|
2186
|
+
|
|
1974
2187
|
// src/audit.ts
|
|
1975
2188
|
var OVERSIZED_SKILL_EST_TOKENS = 5e3;
|
|
1976
2189
|
function toAuditFindings(source) {
|
|
@@ -2011,7 +2224,7 @@ function listSubdirsFollowingSymlinks(dir) {
|
|
|
2011
2224
|
}
|
|
2012
2225
|
const out = [];
|
|
2013
2226
|
for (const entry of entries) {
|
|
2014
|
-
const full =
|
|
2227
|
+
const full = join13(dir, entry.name);
|
|
2015
2228
|
if (entry.isDirectory() || entry.isSymbolicLink() && isDirFollowingSymlink(full)) {
|
|
2016
2229
|
out.push(full);
|
|
2017
2230
|
}
|
|
@@ -2025,20 +2238,20 @@ function listSubdirsNoSymlinks(dir) {
|
|
|
2025
2238
|
} catch {
|
|
2026
2239
|
return [];
|
|
2027
2240
|
}
|
|
2028
|
-
return entries.filter((e) => e.isDirectory()).map((e) =>
|
|
2241
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join13(dir, e.name));
|
|
2029
2242
|
}
|
|
2030
2243
|
function findSkillDirsUnderRoot(root) {
|
|
2031
2244
|
const found = /* @__PURE__ */ new Set();
|
|
2032
2245
|
const rootSubdirs = listSubdirsFollowingSymlinks(root);
|
|
2033
2246
|
for (const dir of rootSubdirs) {
|
|
2034
|
-
if (
|
|
2247
|
+
if (existsSync10(join13(dir, "SKILL.md"))) found.add(dir);
|
|
2035
2248
|
}
|
|
2036
|
-
for (const dir of listSubdirsFollowingSymlinks(
|
|
2037
|
-
if (
|
|
2249
|
+
for (const dir of listSubdirsFollowingSymlinks(join13(root, "skills"))) {
|
|
2250
|
+
if (existsSync10(join13(dir, "SKILL.md"))) found.add(dir);
|
|
2038
2251
|
}
|
|
2039
2252
|
for (const topDir of rootSubdirs) {
|
|
2040
|
-
for (const dir of listSubdirsNoSymlinks(
|
|
2041
|
-
if (
|
|
2253
|
+
for (const dir of listSubdirsNoSymlinks(join13(topDir, "skills"))) {
|
|
2254
|
+
if (existsSync10(join13(dir, "SKILL.md"))) found.add(dir);
|
|
2042
2255
|
}
|
|
2043
2256
|
}
|
|
2044
2257
|
return Array.from(found).filter((p) => !p.includes("/.git/"));
|
|
@@ -2052,7 +2265,7 @@ function collectSkillCandidates(roots) {
|
|
|
2052
2265
|
let realPath;
|
|
2053
2266
|
if (exists) {
|
|
2054
2267
|
try {
|
|
2055
|
-
realPath =
|
|
2268
|
+
realPath = realpathSync3(resolvedRoot);
|
|
2056
2269
|
} catch {
|
|
2057
2270
|
realPath = void 0;
|
|
2058
2271
|
}
|
|
@@ -2071,7 +2284,7 @@ function dedupeSkillCandidates(candidates, findings) {
|
|
|
2071
2284
|
for (const candidate of candidates) {
|
|
2072
2285
|
let realPath;
|
|
2073
2286
|
try {
|
|
2074
|
-
realPath =
|
|
2287
|
+
realPath = realpathSync3(candidate.configuredPath);
|
|
2075
2288
|
} catch (err) {
|
|
2076
2289
|
findings.push({
|
|
2077
2290
|
severity: "MEDIUM",
|
|
@@ -2134,7 +2347,7 @@ function estimateTokens(text) {
|
|
|
2134
2347
|
return Math.ceil(Buffer.byteLength(text, "utf-8") / 4);
|
|
2135
2348
|
}
|
|
2136
2349
|
function profileAuditedSkill(group, findings, unreadableSkillMdPaths, pluginId) {
|
|
2137
|
-
const skillMdPath =
|
|
2350
|
+
const skillMdPath = join13(group.canonicalPath, "SKILL.md");
|
|
2138
2351
|
const base = {
|
|
2139
2352
|
path: group.canonicalPath,
|
|
2140
2353
|
skillMdPath,
|
|
@@ -2146,7 +2359,7 @@ function profileAuditedSkill(group, findings, unreadableSkillMdPaths, pluginId)
|
|
|
2146
2359
|
let sizeBytes;
|
|
2147
2360
|
try {
|
|
2148
2361
|
sizeBytes = statSync6(skillMdPath).size;
|
|
2149
|
-
text =
|
|
2362
|
+
text = readFileSync11(skillMdPath, "utf-8");
|
|
2150
2363
|
} catch (err) {
|
|
2151
2364
|
findings.push({
|
|
2152
2365
|
severity: "MEDIUM",
|
|
@@ -2169,9 +2382,88 @@ function profileAuditedSkill(group, findings, unreadableSkillMdPaths, pluginId)
|
|
|
2169
2382
|
validFrontmatter: fm.valid,
|
|
2170
2383
|
description: fm.description,
|
|
2171
2384
|
sizeBytes,
|
|
2172
|
-
estTokens: estimateTokens(text)
|
|
2385
|
+
estTokens: estimateTokens(text),
|
|
2386
|
+
instructionDiagnostics: diagnoseInstructions(fm.name, fm.description, skillEntrypointBody(text), countSkillResources(group.canonicalPath))
|
|
2173
2387
|
};
|
|
2174
2388
|
}
|
|
2389
|
+
var RESOURCE_INVENTORY_SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv"]);
|
|
2390
|
+
var MAX_RESOURCE_INVENTORY_ENTRIES = 1e4;
|
|
2391
|
+
var MAX_RESOURCE_INVENTORY_DEPTH = 32;
|
|
2392
|
+
function countSkillResources(root) {
|
|
2393
|
+
const resourceDirs = ["scripts", "references", "reference", "commands", "hooks", "assets", "templates", "sub-skills"];
|
|
2394
|
+
const inventory = [];
|
|
2395
|
+
for (const directory of resourceDirs) {
|
|
2396
|
+
const resourceRoot = join13(root, directory);
|
|
2397
|
+
let rootStat;
|
|
2398
|
+
try {
|
|
2399
|
+
rootStat = lstatSync3(resourceRoot);
|
|
2400
|
+
} catch (error) {
|
|
2401
|
+
if (error.code !== "ENOENT") inventory.push({ directory, unavailableReason: `could not inspect resource directory: ${error.message}` });
|
|
2402
|
+
continue;
|
|
2403
|
+
}
|
|
2404
|
+
if (rootStat.isSymbolicLink()) {
|
|
2405
|
+
inventory.push({ directory, unavailableReason: "resource directory is a symlink and was not followed" });
|
|
2406
|
+
continue;
|
|
2407
|
+
}
|
|
2408
|
+
if (!rootStat.isDirectory()) continue;
|
|
2409
|
+
const pending = [{ path: resourceRoot, depth: 0 }];
|
|
2410
|
+
let files = 0;
|
|
2411
|
+
let entries = 0;
|
|
2412
|
+
let unavailableReason;
|
|
2413
|
+
while (pending.length > 0 && !unavailableReason) {
|
|
2414
|
+
const current = pending.pop();
|
|
2415
|
+
let handle;
|
|
2416
|
+
try {
|
|
2417
|
+
handle = opendirSync(current.path);
|
|
2418
|
+
} catch (error) {
|
|
2419
|
+
unavailableReason = `could not read resource subtree: ${error.message}`;
|
|
2420
|
+
break;
|
|
2421
|
+
}
|
|
2422
|
+
try {
|
|
2423
|
+
let entry;
|
|
2424
|
+
while ((entry = handle.readSync()) !== null) {
|
|
2425
|
+
if (++entries > MAX_RESOURCE_INVENTORY_ENTRIES) {
|
|
2426
|
+
unavailableReason = `resource inventory exceeded ${MAX_RESOURCE_INVENTORY_ENTRIES} entries`;
|
|
2427
|
+
break;
|
|
2428
|
+
}
|
|
2429
|
+
const path = join13(current.path, entry.name);
|
|
2430
|
+
let stat;
|
|
2431
|
+
try {
|
|
2432
|
+
stat = lstatSync3(path);
|
|
2433
|
+
} catch (error) {
|
|
2434
|
+
unavailableReason = `could not inspect resource entry: ${error.message}`;
|
|
2435
|
+
break;
|
|
2436
|
+
}
|
|
2437
|
+
if (stat.isSymbolicLink()) {
|
|
2438
|
+
unavailableReason = `resource subtree contains a symlink that was not followed: ${entry.name}`;
|
|
2439
|
+
break;
|
|
2440
|
+
}
|
|
2441
|
+
if (stat.isDirectory()) {
|
|
2442
|
+
if (RESOURCE_INVENTORY_SKIP_DIRS.has(entry.name)) {
|
|
2443
|
+
unavailableReason = `resource subtree contains skipped directory: ${entry.name}`;
|
|
2444
|
+
break;
|
|
2445
|
+
}
|
|
2446
|
+
if (current.depth >= MAX_RESOURCE_INVENTORY_DEPTH) {
|
|
2447
|
+
unavailableReason = `resource inventory exceeded depth ${MAX_RESOURCE_INVENTORY_DEPTH}`;
|
|
2448
|
+
break;
|
|
2449
|
+
}
|
|
2450
|
+
pending.push({ path, depth: current.depth + 1 });
|
|
2451
|
+
} else if (stat.isFile()) files++;
|
|
2452
|
+
}
|
|
2453
|
+
} catch (error) {
|
|
2454
|
+
unavailableReason = `could not read resource subtree: ${error.message}`;
|
|
2455
|
+
} finally {
|
|
2456
|
+
try {
|
|
2457
|
+
handle.closeSync();
|
|
2458
|
+
} catch (error) {
|
|
2459
|
+
if (!unavailableReason) unavailableReason = `could not close resource subtree: ${error.message}`;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
inventory.push(unavailableReason ? { directory, unavailableReason } : { directory, files });
|
|
2464
|
+
}
|
|
2465
|
+
return inventory;
|
|
2466
|
+
}
|
|
2175
2467
|
function hasRecognizedMcpShape(parsed) {
|
|
2176
2468
|
if (typeof parsed !== "object" || parsed === null) return false;
|
|
2177
2469
|
const obj = parsed;
|
|
@@ -2203,7 +2495,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
2203
2495
|
const findings = [];
|
|
2204
2496
|
const isToml = extname4(targetPath).toLowerCase() === ".toml";
|
|
2205
2497
|
const format = isToml ? "toml" : "json";
|
|
2206
|
-
if (!
|
|
2498
|
+
if (!existsSync10(targetPath)) {
|
|
2207
2499
|
findings.push({
|
|
2208
2500
|
severity: "LOW",
|
|
2209
2501
|
rule: "missing-mcp-target",
|
|
@@ -2224,7 +2516,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
2224
2516
|
}
|
|
2225
2517
|
if (isToml) {
|
|
2226
2518
|
try {
|
|
2227
|
-
|
|
2519
|
+
readFileSync11(targetPath, "utf-8");
|
|
2228
2520
|
} catch (err) {
|
|
2229
2521
|
findings.push({
|
|
2230
2522
|
severity: "MEDIUM",
|
|
@@ -2239,7 +2531,7 @@ function auditMcpTarget(targetPath, notices) {
|
|
|
2239
2531
|
}
|
|
2240
2532
|
let raw;
|
|
2241
2533
|
try {
|
|
2242
|
-
raw =
|
|
2534
|
+
raw = readFileSync11(targetPath, "utf-8");
|
|
2243
2535
|
} catch (err) {
|
|
2244
2536
|
findings.push({
|
|
2245
2537
|
severity: "MEDIUM",
|
|
@@ -2373,18 +2665,32 @@ function buildUsageJoinIndex(usage, skills) {
|
|
|
2373
2665
|
}
|
|
2374
2666
|
const inventoryNameCounts = /* @__PURE__ */ new Map();
|
|
2375
2667
|
for (const skill of skills) inventoryNameCounts.set(skill.name, (inventoryNameCounts.get(skill.name) ?? 0) + 1);
|
|
2376
|
-
|
|
2668
|
+
const compositeCounts = /* @__PURE__ */ new Map();
|
|
2669
|
+
for (const skill of skills) {
|
|
2670
|
+
if (!skill.pluginId) continue;
|
|
2671
|
+
const key = `${skill.pluginId.split("@")[0]}:${skill.name}`;
|
|
2672
|
+
compositeCounts.set(key, (compositeCounts.get(key) ?? 0) + 1);
|
|
2673
|
+
}
|
|
2674
|
+
return { bareKeys, inventoryNameCounts, compositeCounts, noticed: /* @__PURE__ */ new Set(), uncertainPaths: /* @__PURE__ */ new Set() };
|
|
2377
2675
|
}
|
|
2378
2676
|
function matchUsage(skill, usage, index, notices) {
|
|
2379
|
-
if (usage.has(skill.name)) return usage.get(skill.name);
|
|
2380
2677
|
if (skill.pluginId) {
|
|
2381
2678
|
const composite = `${skill.pluginId.split("@")[0]}:${skill.name}`;
|
|
2382
|
-
if (usage.has(composite)
|
|
2679
|
+
if (usage.has(composite) && index.compositeCounts.get(composite) === 1) {
|
|
2680
|
+
if (usage.get(composite) === 0 && (usage.get(skill.name) ?? 0) > 0) {
|
|
2681
|
+
index.uncertainPaths.add(skill.path);
|
|
2682
|
+
notices.push(`usage snapshot: conflicting zero and positive aliases for "${skill.name}" \u2014 skipping attribution`);
|
|
2683
|
+
return void 0;
|
|
2684
|
+
}
|
|
2685
|
+
return usage.get(composite);
|
|
2686
|
+
}
|
|
2383
2687
|
}
|
|
2384
2688
|
const bareMatches = index.bareKeys.get(skill.name) ?? [];
|
|
2385
2689
|
if (bareMatches.length === 0) return void 0;
|
|
2386
2690
|
const installedWithName = index.inventoryNameCounts.get(skill.name) ?? 0;
|
|
2387
|
-
if (
|
|
2691
|
+
if (installedWithName === 1 && usage.has(skill.name)) return usage.get(skill.name);
|
|
2692
|
+
if (bareMatches.length === 1 && installedWithName === 1 && !skill.pluginId) return usage.get(bareMatches[0]);
|
|
2693
|
+
index.uncertainPaths.add(skill.path);
|
|
2388
2694
|
if (!index.noticed.has(skill.name)) {
|
|
2389
2695
|
index.noticed.add(skill.name);
|
|
2390
2696
|
notices.push(
|
|
@@ -2394,9 +2700,9 @@ function matchUsage(skill, usage, index, notices) {
|
|
|
2394
2700
|
return void 0;
|
|
2395
2701
|
}
|
|
2396
2702
|
function isUnderInstallPath(target, installPath) {
|
|
2397
|
-
return target === installPath || target.startsWith(installPath +
|
|
2703
|
+
return target === installPath || target.startsWith(installPath + sep3);
|
|
2398
2704
|
}
|
|
2399
|
-
function computePluginSummaries(pluginRoots, activeFindings, skills, usage, usageJoined) {
|
|
2705
|
+
function computePluginSummaries(pluginRoots, activeFindings, skills, usage, usageJoined, uncertainPaths, positiveJoined) {
|
|
2400
2706
|
return pluginRoots.map((pr) => {
|
|
2401
2707
|
const findingCounts = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0 };
|
|
2402
2708
|
for (const f of activeFindings) {
|
|
@@ -2410,9 +2716,10 @@ function computePluginSummaries(pluginRoots, activeFindings, skills, usage, usag
|
|
|
2410
2716
|
let recommendation;
|
|
2411
2717
|
const reasons = [];
|
|
2412
2718
|
const longWindow = windowDays !== void 0 && windowDays >= 28;
|
|
2719
|
+
const ambiguousUsage = skills.some((s) => s.pluginId === pr.pluginId && uncertainPaths.has(s.path));
|
|
2413
2720
|
if (findingCounts.HIGH > 0 || findingCounts.CRITICAL > 0) {
|
|
2414
2721
|
recommendation = "review";
|
|
2415
|
-
} else if (exhaustive && longWindow && skillCount > 0 && observedSkillCount === 0 &&
|
|
2722
|
+
} else if (!ambiguousUsage && exhaustive && longWindow && skillCount > 0 && observedSkillCount === 0 && positiveJoined > 0) {
|
|
2416
2723
|
recommendation = "unobserved";
|
|
2417
2724
|
} else if (exhaustive && (observedSkillCount ?? 0) >= 1) {
|
|
2418
2725
|
recommendation = "keep";
|
|
@@ -2421,9 +2728,11 @@ function computePluginSummaries(pluginRoots, activeFindings, skills, usage, usag
|
|
|
2421
2728
|
if (skillCount === 0) reasons.push("no skills to observe");
|
|
2422
2729
|
if (!hasUsage) reasons.push("no usage telemetry");
|
|
2423
2730
|
else if (usage.usage.size === 0) reasons.push("usage snapshot is empty or unreadable (see notices)");
|
|
2424
|
-
else if (!exhaustive) reasons.push("usage snapshot is not exhaustive (top-50 only)");
|
|
2731
|
+
else if (!exhaustive) reasons.push("usage snapshot is not exhaustive (top-50 only or invalid counts)");
|
|
2732
|
+
else if (ambiguousUsage) reasons.push("ambiguous usage identity \u2014 absence is not zero usage");
|
|
2425
2733
|
else if (!longWindow) reasons.push(`window ${windowDays ?? "unknown"} < 28 days`);
|
|
2426
2734
|
else if ((usageJoined ?? 0) === 0) reasons.push("usage snapshot joined 0 installed skills");
|
|
2735
|
+
else if (positiveJoined === 0) reasons.push("usage snapshot joined no positive installed-skill usage");
|
|
2427
2736
|
}
|
|
2428
2737
|
return {
|
|
2429
2738
|
pluginId: pr.pluginId,
|
|
@@ -2443,7 +2752,7 @@ function defaultWhich(cmd) {
|
|
|
2443
2752
|
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
2444
2753
|
for (const dir of dirs) {
|
|
2445
2754
|
for (const ext of exts) {
|
|
2446
|
-
const candidate =
|
|
2755
|
+
const candidate = join13(dir, cmd + ext);
|
|
2447
2756
|
try {
|
|
2448
2757
|
if (statSync6(candidate).isFile()) return candidate;
|
|
2449
2758
|
} catch {
|
|
@@ -2643,18 +2952,21 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2643
2952
|
message: "no business-foundation skill yet \u2014 `skill-forge audit --foundation` can scaffold one to ground future skills in your business profile"
|
|
2644
2953
|
};
|
|
2645
2954
|
const usage = options.usage;
|
|
2955
|
+
const uncertainPaths = /* @__PURE__ */ new Set();
|
|
2646
2956
|
if (usage) {
|
|
2647
2957
|
const index = buildUsageJoinIndex(usage.usage, skills);
|
|
2648
2958
|
for (const skill of skills) {
|
|
2649
2959
|
const matched = matchUsage(skill, usage.usage, index, notices);
|
|
2650
2960
|
if (matched !== void 0) skill.usage = matched;
|
|
2961
|
+
if (index.uncertainPaths.has(skill.path)) uncertainPaths.add(skill.path);
|
|
2651
2962
|
}
|
|
2652
2963
|
}
|
|
2964
|
+
const positiveJoined = skills.filter((s) => (s.usage ?? 0) > 0).length;
|
|
2653
2965
|
const usageJoined = usage ? skills.filter((s) => s.usage !== void 0).length : void 0;
|
|
2654
2966
|
if (usage && usageJoined === 0) {
|
|
2655
2967
|
notices.push(`usage snapshot (${usage.sourcePath}) joined 0 of ${skills.length} skill(s)`);
|
|
2656
2968
|
}
|
|
2657
|
-
const unobservedInWindow = usage && usage.exhaustive && usage.windowDays !== void 0 && usage.windowDays >= 28 &&
|
|
2969
|
+
const unobservedInWindow = usage && usage.exhaustive && usage.windowDays !== void 0 && usage.windowDays >= 28 && positiveJoined > 0 ? skills.filter((s) => (s.usage ?? 0) === 0 && !uncertainPaths.has(s.path)).map((s) => ({ name: s.name, path: s.path, pluginId: s.pluginId })) : [];
|
|
2658
2970
|
const findingCounts = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0 };
|
|
2659
2971
|
for (const f of activeFindings) findingCounts[f.severity]++;
|
|
2660
2972
|
const summary = {
|
|
@@ -2669,7 +2981,7 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2669
2981
|
documentationExampleCount: activeFindings.filter((f) => f.classification === "documentation-example").length,
|
|
2670
2982
|
...usage ? { usageJoined, usageExhaustive: usage.exhaustive, usageWindowDays: usage.windowDays } : {}
|
|
2671
2983
|
};
|
|
2672
|
-
const
|
|
2984
|
+
const plugins2 = options.pluginRoots ? computePluginSummaries(pluginRoots, activeFindings, skills, usage, usageJoined, uncertainPaths, positiveJoined) : void 0;
|
|
2673
2985
|
const tooling = checkTooling(options.which ?? defaultWhich, options.env ?? process.env);
|
|
2674
2986
|
return {
|
|
2675
2987
|
schemaVersion: 1,
|
|
@@ -2681,7 +2993,7 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2681
2993
|
tooling,
|
|
2682
2994
|
notices,
|
|
2683
2995
|
summary,
|
|
2684
|
-
plugins
|
|
2996
|
+
plugins: plugins2
|
|
2685
2997
|
};
|
|
2686
2998
|
}
|
|
2687
2999
|
function bulletList(items, emptyPlaceholder) {
|
|
@@ -2721,6 +3033,17 @@ function renderSkillsSection(skills) {
|
|
|
2721
3033
|
)
|
|
2722
3034
|
];
|
|
2723
3035
|
}
|
|
3036
|
+
function renderInstructionDiagnosticsSection(skills) {
|
|
3037
|
+
const lines = ["## Instruction diagnostics (advisory)", "", "- Static measurements only. Actual host loading and truncation are unknown; estimated tokens use ceil(UTF-8 bytes / 4) and are not observed usage."];
|
|
3038
|
+
for (const skill of skills) {
|
|
3039
|
+
const diagnostics = skill.instructionDiagnostics;
|
|
3040
|
+
if (!diagnostics) continue;
|
|
3041
|
+
const description = diagnostics.discovery.description;
|
|
3042
|
+
lines.push(`- ${sanitizeForMarkdown(skill.name)} \u2014 description: ${description ? `${description.utf8Bytes} UTF-8 bytes, ${description.unicodeCodePoints} Unicode code points, ${description.words} words, est. ${description.estimatedTokens} tokens` : "unavailable"}; entrypoint: ${diagnostics.entrypoint.utf8Bytes} UTF-8 bytes, ${diagnostics.entrypoint.unicodeCodePoints} Unicode code points, ${diagnostics.entrypoint.words} words, est. ${diagnostics.entrypoint.estimatedTokens} tokens; host load: unknown`);
|
|
3043
|
+
for (const advice of diagnostics.advice) lines.push(` - advisory ${advice.kind}: ${sanitizeForMarkdown(advice.detail)}`);
|
|
3044
|
+
}
|
|
3045
|
+
return lines;
|
|
3046
|
+
}
|
|
2724
3047
|
function renderMcpTargetsSection(targets) {
|
|
2725
3048
|
const lines = ["### MCP targets", ""];
|
|
2726
3049
|
if (targets.length === 0) {
|
|
@@ -2816,16 +3139,17 @@ function renderUsageSection(report) {
|
|
|
2816
3139
|
}
|
|
2817
3140
|
return lines;
|
|
2818
3141
|
}
|
|
2819
|
-
function renderPluginsSection(
|
|
3142
|
+
function renderPluginsSection(plugins2) {
|
|
2820
3143
|
return [
|
|
2821
3144
|
"## Plugins",
|
|
2822
3145
|
"",
|
|
2823
3146
|
...bulletList(
|
|
2824
|
-
|
|
3147
|
+
plugins2.map((p) => {
|
|
2825
3148
|
const findingsPart = `${p.findingCounts.CRITICAL} CRITICAL, ${p.findingCounts.HIGH} HIGH, ${p.findingCounts.MEDIUM} MEDIUM, ${p.findingCounts.LOW} LOW`;
|
|
2826
3149
|
const observedPart = p.observedSkillCount !== void 0 ? `, observed ${p.observedSkillCount}` : "";
|
|
2827
|
-
const reasonsPart = p.reasons.length > 0 ? ` (${p.reasons.join("; ")})` : "";
|
|
2828
|
-
|
|
3150
|
+
const reasonsPart = p.reasons.length > 0 ? ` (${p.reasons.map(sanitizeForMarkdown).join("; ")})` : "";
|
|
3151
|
+
const action = p.recommendation === "review" ? " \u2014 next: inspect finding context, update or fix the owning source; do not edit an installed cache" : p.recommendation === "unobserved" ? " \u2014 next: check hooks, commands and occasional use; record a keep rationale or explicitly review disabling" : p.recommendation === "unknown" ? " \u2014 next: resolve the listed evidence gaps before a retention decision" : "";
|
|
3152
|
+
return `${sanitizeForMarkdown(p.pluginId)} @ ${sanitizeForMarkdown(p.version)} \u2014 ${sanitizeForMarkdown(p.installPath)} \u2014 ${p.skillCount} skill(s)${observedPart} \u2014 findings: ${findingsPart} \u2014 **${p.recommendation}**${reasonsPart}` + (p.keepDecision ? ` \u2014 explicit keep (${p.keepDecision.category}): ${sanitizeForMarkdown(p.keepDecision.reason)}` : "") + action;
|
|
2829
3153
|
}),
|
|
2830
3154
|
"(no enabled plugins)"
|
|
2831
3155
|
)
|
|
@@ -2858,6 +3182,7 @@ function renderAuditMarkdown(report) {
|
|
|
2858
3182
|
const lines = [
|
|
2859
3183
|
`# skill-forge audit \u2014 ${report.generatedAt}`,
|
|
2860
3184
|
"",
|
|
3185
|
+
...report.comparison ? renderAuditComparison(report.comparison) : [],
|
|
2861
3186
|
"## Summary",
|
|
2862
3187
|
"",
|
|
2863
3188
|
`- Skill roots: ${summary.rootCount}`,
|
|
@@ -2876,6 +3201,8 @@ function renderAuditMarkdown(report) {
|
|
|
2876
3201
|
"",
|
|
2877
3202
|
...renderSkillsSection(report.inventory.skills),
|
|
2878
3203
|
"",
|
|
3204
|
+
...renderInstructionDiagnosticsSection(report.inventory.skills),
|
|
3205
|
+
"",
|
|
2879
3206
|
...renderMcpTargetsSection(report.inventory.mcpTargets),
|
|
2880
3207
|
"",
|
|
2881
3208
|
...renderFindingsSection(report.hygiene),
|
|
@@ -2933,7 +3260,7 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
|
|
|
2933
3260
|
if (!isDir(resolvedRoot)) {
|
|
2934
3261
|
throw new UserError(`skills root does not exist: ${resolvedRoot}`);
|
|
2935
3262
|
}
|
|
2936
|
-
const destDir =
|
|
3263
|
+
const destDir = join13(resolvedRoot, "business-foundation");
|
|
2937
3264
|
let destLstat;
|
|
2938
3265
|
try {
|
|
2939
3266
|
destLstat = lstatSync3(destDir);
|
|
@@ -2947,9 +3274,9 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
|
|
|
2947
3274
|
throw new UserError(`refusing to scaffold a foundation skill at ${destDir}: already exists`);
|
|
2948
3275
|
}
|
|
2949
3276
|
const content = renderFoundationSkillMd(profile);
|
|
2950
|
-
const tempDir = mkdtempSync(
|
|
3277
|
+
const tempDir = mkdtempSync(join13(resolvedRoot, ".business-foundation-"));
|
|
2951
3278
|
try {
|
|
2952
|
-
writeFileSync5(
|
|
3279
|
+
writeFileSync5(join13(tempDir, "SKILL.md"), content, { flag: "wx", mode: 384 });
|
|
2953
3280
|
const safety = await scanSafety(tempDir, config.strictness);
|
|
2954
3281
|
if (safety.verdict === "block") {
|
|
2955
3282
|
throw new UserError(
|
|
@@ -2983,7 +3310,7 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
|
|
|
2983
3310
|
|
|
2984
3311
|
// src/auditState.ts
|
|
2985
3312
|
function getAuditStatePath() {
|
|
2986
|
-
return
|
|
3313
|
+
return join14(getHomeDir(), "audit-state.json");
|
|
2987
3314
|
}
|
|
2988
3315
|
function emptyCounts() {
|
|
2989
3316
|
return { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
|
|
@@ -3046,9 +3373,9 @@ function writeAuditState(state) {
|
|
|
3046
3373
|
}
|
|
3047
3374
|
function readAuditState() {
|
|
3048
3375
|
const path = getAuditStatePath();
|
|
3049
|
-
if (!
|
|
3376
|
+
if (!existsSync11(path)) return void 0;
|
|
3050
3377
|
try {
|
|
3051
|
-
const parsed = JSON.parse(
|
|
3378
|
+
const parsed = JSON.parse(readFileSync12(path, "utf-8"));
|
|
3052
3379
|
if (parsed.schemaVersion !== 1 || typeof parsed.lastRunAt !== "string") return void 0;
|
|
3053
3380
|
return {
|
|
3054
3381
|
schemaVersion: 1,
|
|
@@ -3110,32 +3437,32 @@ function formatAuditAdvisory(state, options = {}) {
|
|
|
3110
3437
|
}
|
|
3111
3438
|
|
|
3112
3439
|
// src/commands/gatePipeline.ts
|
|
3113
|
-
import { join as
|
|
3440
|
+
import { join as join17 } from "path";
|
|
3114
3441
|
|
|
3115
3442
|
// src/gate/profile.ts
|
|
3116
3443
|
import { execFileSync } from "child_process";
|
|
3117
3444
|
import {
|
|
3118
3445
|
closeSync as closeSync2,
|
|
3119
|
-
existsSync as
|
|
3446
|
+
existsSync as existsSync13,
|
|
3120
3447
|
mkdtempSync as mkdtempSync2,
|
|
3121
3448
|
openSync as openSync2,
|
|
3122
|
-
readFileSync as
|
|
3449
|
+
readFileSync as readFileSync14,
|
|
3123
3450
|
readdirSync as readdirSync6,
|
|
3124
3451
|
readSync,
|
|
3125
3452
|
rmSync as rmSync4,
|
|
3126
3453
|
statSync as statSync7
|
|
3127
3454
|
} from "fs";
|
|
3128
3455
|
import { tmpdir } from "os";
|
|
3129
|
-
import { basename as basename8, dirname as dirname6, extname as extname6, join as
|
|
3456
|
+
import { basename as basename8, dirname as dirname6, extname as extname6, join as join16, resolve as resolve4 } from "path";
|
|
3130
3457
|
|
|
3131
3458
|
// src/gate/mcpCapabilities.ts
|
|
3132
|
-
import { existsSync as
|
|
3133
|
-
import { extname as extname5, join as
|
|
3459
|
+
import { existsSync as existsSync12, readFileSync as readFileSync13 } from "fs";
|
|
3460
|
+
import { extname as extname5, join as join15 } from "path";
|
|
3134
3461
|
var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
3135
3462
|
var MAX_ITEMS_PER_CATEGORY = 200;
|
|
3136
3463
|
function readJsonSafe(file) {
|
|
3137
3464
|
try {
|
|
3138
|
-
return JSON.parse(
|
|
3465
|
+
return JSON.parse(readFileSync13(file, "utf-8"));
|
|
3139
3466
|
} catch {
|
|
3140
3467
|
return void 0;
|
|
3141
3468
|
}
|
|
@@ -3236,16 +3563,16 @@ var MCP_SDK_IMPORT_RE = /@modelcontextprotocol\/sdk/;
|
|
|
3236
3563
|
function extractMcpCapabilities(packageDir) {
|
|
3237
3564
|
const buckets = { tools: /* @__PURE__ */ new Set(), resources: /* @__PURE__ */ new Set(), prompts: /* @__PURE__ */ new Set() };
|
|
3238
3565
|
let hasPackageJsonMarker = false;
|
|
3239
|
-
const pkgPath =
|
|
3240
|
-
if (
|
|
3566
|
+
const pkgPath = join15(packageDir, "package.json");
|
|
3567
|
+
if (existsSync12(pkgPath)) {
|
|
3241
3568
|
const pkg2 = readJsonSafe(pkgPath);
|
|
3242
3569
|
if (pkg2 && typeof pkg2 === "object" && !Array.isArray(pkg2)) {
|
|
3243
3570
|
hasPackageJsonMarker = hasPackageJsonMcpMarkers(pkg2);
|
|
3244
3571
|
}
|
|
3245
3572
|
}
|
|
3246
3573
|
for (const manifestName of [".mcp.json", "mcp.json"]) {
|
|
3247
|
-
const manifestPath =
|
|
3248
|
-
if (!
|
|
3574
|
+
const manifestPath = join15(packageDir, manifestName);
|
|
3575
|
+
if (!existsSync12(manifestPath)) continue;
|
|
3249
3576
|
const parsed = readJsonSafe(manifestPath);
|
|
3250
3577
|
if (parsed !== void 0) {
|
|
3251
3578
|
collectDeclaredFromManifest(parsed, buckets);
|
|
@@ -3321,8 +3648,8 @@ var PYTHON_STDLIB = /* @__PURE__ */ new Set([
|
|
|
3321
3648
|
"__future__"
|
|
3322
3649
|
]);
|
|
3323
3650
|
function profileSkill(inputPath) {
|
|
3324
|
-
const p =
|
|
3325
|
-
if (!
|
|
3651
|
+
const p = resolve4(expandHome(inputPath));
|
|
3652
|
+
if (!existsSync13(p)) {
|
|
3326
3653
|
throw new Error(`path does not exist: ${p}`);
|
|
3327
3654
|
}
|
|
3328
3655
|
const stat = statSync7(p);
|
|
@@ -3352,7 +3679,7 @@ function asProfile(candidatePath, artifact) {
|
|
|
3352
3679
|
return asSkillProfile(profile, `at ${candidatePath}`);
|
|
3353
3680
|
}
|
|
3354
3681
|
function classifyInput(p) {
|
|
3355
|
-
if (
|
|
3682
|
+
if (existsSync13(join16(p, ".claude-plugin", "plugin.json"))) return "plugin";
|
|
3356
3683
|
if (detectArtifactType(p) === "mcp") return "mcp";
|
|
3357
3684
|
return "skill";
|
|
3358
3685
|
}
|
|
@@ -3360,10 +3687,10 @@ var MCP_SDK_DEP2 = "@modelcontextprotocol/sdk";
|
|
|
3360
3687
|
var MCP_KEYWORD2 = "mcp-server";
|
|
3361
3688
|
var MCP_BIN_PREFIX2 = /^mcp-/i;
|
|
3362
3689
|
function readPackageJsonSafe(root) {
|
|
3363
|
-
const pkgPath =
|
|
3364
|
-
if (!
|
|
3690
|
+
const pkgPath = join16(root, "package.json");
|
|
3691
|
+
if (!existsSync13(pkgPath)) return void 0;
|
|
3365
3692
|
try {
|
|
3366
|
-
return JSON.parse(
|
|
3693
|
+
return JSON.parse(readFileSync14(pkgPath, "utf-8"));
|
|
3367
3694
|
} catch {
|
|
3368
3695
|
return void 0;
|
|
3369
3696
|
}
|
|
@@ -3384,7 +3711,7 @@ function hasMcpPackageMarkers(pkg2) {
|
|
|
3384
3711
|
return false;
|
|
3385
3712
|
}
|
|
3386
3713
|
function detectArtifactType(root) {
|
|
3387
|
-
if (
|
|
3714
|
+
if (existsSync13(join16(root, ".mcp.json")) || existsSync13(join16(root, "mcp.json"))) return "mcp";
|
|
3388
3715
|
const pkg2 = readPackageJsonSafe(root);
|
|
3389
3716
|
if (pkg2 && hasMcpPackageMarkers(pkg2)) return "mcp";
|
|
3390
3717
|
return "skill";
|
|
@@ -3394,7 +3721,7 @@ function resolveSkillMd(inputPath) {
|
|
|
3394
3721
|
let tmpDir;
|
|
3395
3722
|
const initialStat = statSync7(currentPath);
|
|
3396
3723
|
if (extname6(currentPath) === ".skill" || initialStat.isFile() && isZipFile(currentPath)) {
|
|
3397
|
-
tmpDir = mkdtempSync2(
|
|
3724
|
+
tmpDir = mkdtempSync2(join16(tmpdir(), "forge_"));
|
|
3398
3725
|
extractZip(currentPath, tmpDir);
|
|
3399
3726
|
currentPath = tmpDir;
|
|
3400
3727
|
}
|
|
@@ -3403,8 +3730,8 @@ function resolveSkillMd(inputPath) {
|
|
|
3403
3730
|
return { skillMd: currentPath, root: dirname6(currentPath), tmpDir };
|
|
3404
3731
|
}
|
|
3405
3732
|
if (stat.isDirectory()) {
|
|
3406
|
-
const direct =
|
|
3407
|
-
if (
|
|
3733
|
+
const direct = join16(currentPath, "SKILL.md");
|
|
3734
|
+
if (existsSync13(direct)) {
|
|
3408
3735
|
return { skillMd: direct, root: currentPath, tmpDir };
|
|
3409
3736
|
}
|
|
3410
3737
|
const matches = findSkillMdOneLevelDown(currentPath);
|
|
@@ -3429,16 +3756,16 @@ function findSkillMdOneLevelDown(dir) {
|
|
|
3429
3756
|
const matches = /* @__PURE__ */ new Set();
|
|
3430
3757
|
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
3431
3758
|
if (entry.isDirectory()) {
|
|
3432
|
-
const candidate =
|
|
3433
|
-
if (
|
|
3759
|
+
const candidate = join16(dir, entry.name, "SKILL.md");
|
|
3760
|
+
if (existsSync13(candidate)) matches.add(candidate);
|
|
3434
3761
|
}
|
|
3435
3762
|
}
|
|
3436
|
-
const skillsDir =
|
|
3763
|
+
const skillsDir = join16(dir, "skills");
|
|
3437
3764
|
if (isDir(skillsDir)) {
|
|
3438
3765
|
for (const entry of readdirSync6(skillsDir, { withFileTypes: true })) {
|
|
3439
3766
|
if (entry.isDirectory()) {
|
|
3440
|
-
const candidate =
|
|
3441
|
-
if (
|
|
3767
|
+
const candidate = join16(skillsDir, entry.name, "SKILL.md");
|
|
3768
|
+
if (existsSync13(candidate)) matches.add(candidate);
|
|
3442
3769
|
}
|
|
3443
3770
|
}
|
|
3444
3771
|
}
|
|
@@ -3549,9 +3876,9 @@ function detectLicense(root, fm) {
|
|
|
3549
3876
|
if (fm.license) return fm.license;
|
|
3550
3877
|
const names = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "license"];
|
|
3551
3878
|
for (const name of names) {
|
|
3552
|
-
const p =
|
|
3553
|
-
if (
|
|
3554
|
-
const head =
|
|
3879
|
+
const p = join16(root, name);
|
|
3880
|
+
if (existsSync13(p) && statSync7(p).isFile()) {
|
|
3881
|
+
const head = readFileSync14(p, "utf-8").slice(0, 2e3).toLowerCase();
|
|
3555
3882
|
for (const [hint, label2] of Object.entries(SPDX_HINTS)) {
|
|
3556
3883
|
if (head.includes(hint)) return label2;
|
|
3557
3884
|
}
|
|
@@ -3563,7 +3890,7 @@ function detectLicense(root, fm) {
|
|
|
3563
3890
|
function walkFiles(dir) {
|
|
3564
3891
|
const out = [];
|
|
3565
3892
|
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
3566
|
-
const full =
|
|
3893
|
+
const full = join16(dir, entry.name);
|
|
3567
3894
|
if (entry.isDirectory()) {
|
|
3568
3895
|
out.push(...walkFiles(full));
|
|
3569
3896
|
} else if (entry.isFile()) {
|
|
@@ -3575,7 +3902,7 @@ function walkFiles(dir) {
|
|
|
3575
3902
|
function countResources(root) {
|
|
3576
3903
|
const resources = {};
|
|
3577
3904
|
for (const d of RESOURCE_DIRS) {
|
|
3578
|
-
const dp =
|
|
3905
|
+
const dp = join16(root, d);
|
|
3579
3906
|
if (isDir(dp)) {
|
|
3580
3907
|
resources[d] = walkFiles(dp).length;
|
|
3581
3908
|
}
|
|
@@ -3586,11 +3913,11 @@ function scanMcpAndImports(text, root) {
|
|
|
3586
3913
|
const mcpRefRe = /mcp__[a-zA-Z0-9_]+/g;
|
|
3587
3914
|
const mcpRefs = new Set(text.match(mcpRefRe) ?? []);
|
|
3588
3915
|
const imports = /* @__PURE__ */ new Set();
|
|
3589
|
-
const scriptsDir =
|
|
3916
|
+
const scriptsDir = join16(root, "scripts");
|
|
3590
3917
|
if (isDir(scriptsDir)) {
|
|
3591
3918
|
for (const f of walkFiles(scriptsDir)) {
|
|
3592
3919
|
if (!f.endsWith(".py")) continue;
|
|
3593
|
-
const ft =
|
|
3920
|
+
const ft = readFileSync14(f, "utf-8");
|
|
3594
3921
|
for (const m2 of ft.match(mcpRefRe) ?? []) mcpRefs.add(m2);
|
|
3595
3922
|
const importRe = /^\s*(?:import|from)\s+([a-zA-Z0-9_]+)/gm;
|
|
3596
3923
|
let m;
|
|
@@ -3607,7 +3934,7 @@ function scanMcpAndImports(text, root) {
|
|
|
3607
3934
|
function profileSkillDir(inputPath) {
|
|
3608
3935
|
const { skillMd, root, tmpDir } = resolveSkillMd(inputPath);
|
|
3609
3936
|
try {
|
|
3610
|
-
const text =
|
|
3937
|
+
const text = readFileSync14(skillMd, "utf-8");
|
|
3611
3938
|
const fm = parseFrontmatter(text);
|
|
3612
3939
|
const bodyLines = (text.match(/\n/g)?.length ?? 0) + 1;
|
|
3613
3940
|
const headers = Array.from(text.matchAll(/^#{1,3}\s+(.+)$/gm)).map((m) => m[1]);
|
|
@@ -3628,6 +3955,7 @@ function profileSkillDir(inputPath) {
|
|
|
3628
3955
|
resources,
|
|
3629
3956
|
mcpDependencies: mcpDeps,
|
|
3630
3957
|
externalPythonDeps: externalDeps,
|
|
3958
|
+
instructionDiagnostics: diagnoseInstructions(fm.name, fm.description, skillEntrypointBody(text), resources),
|
|
3631
3959
|
artifactType: detectArtifactType(root),
|
|
3632
3960
|
extends: fm.extends ?? []
|
|
3633
3961
|
};
|
|
@@ -3636,19 +3964,19 @@ function profileSkillDir(inputPath) {
|
|
|
3636
3964
|
}
|
|
3637
3965
|
}
|
|
3638
3966
|
function profilePlugin(p) {
|
|
3639
|
-
const manifestPath =
|
|
3967
|
+
const manifestPath = join16(p, ".claude-plugin", "plugin.json");
|
|
3640
3968
|
let manifest;
|
|
3641
3969
|
try {
|
|
3642
|
-
manifest = JSON.parse(
|
|
3970
|
+
manifest = JSON.parse(readFileSync14(manifestPath, "utf-8"));
|
|
3643
3971
|
} catch (err) {
|
|
3644
3972
|
throw new Error(`could not parse plugin.json under ${p}: ${err.message}`);
|
|
3645
3973
|
}
|
|
3646
|
-
const skillsDir =
|
|
3974
|
+
const skillsDir = join16(p, "skills");
|
|
3647
3975
|
let skills = [];
|
|
3648
3976
|
if (isDir(skillsDir)) {
|
|
3649
|
-
skills = readdirSync6(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() &&
|
|
3977
|
+
skills = readdirSync6(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync13(join16(skillsDir, d.name, "SKILL.md"))).map((d) => d.name).sort();
|
|
3650
3978
|
}
|
|
3651
|
-
const commandsDir =
|
|
3979
|
+
const commandsDir = join16(p, "commands");
|
|
3652
3980
|
const commands = isDir(commandsDir) ? readdirSync6(commandsDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
3653
3981
|
const mc = manifest.mcpServers ?? manifest.mcp_servers ?? {};
|
|
3654
3982
|
const mcpServers = typeof mc === "object" && mc !== null ? Object.keys(mc).sort() : [];
|
|
@@ -3666,11 +3994,11 @@ function profilePlugin(p) {
|
|
|
3666
3994
|
};
|
|
3667
3995
|
}
|
|
3668
3996
|
function profileMcp(p) {
|
|
3669
|
-
const explicitConfig =
|
|
3997
|
+
const explicitConfig = existsSync13(join16(p, ".mcp.json")) ? join16(p, ".mcp.json") : existsSync13(join16(p, "mcp.json")) ? join16(p, "mcp.json") : void 0;
|
|
3670
3998
|
if (explicitConfig) {
|
|
3671
3999
|
let cfg;
|
|
3672
4000
|
try {
|
|
3673
|
-
cfg = JSON.parse(
|
|
4001
|
+
cfg = JSON.parse(readFileSync14(explicitConfig, "utf-8"));
|
|
3674
4002
|
} catch (err) {
|
|
3675
4003
|
throw new Error(`could not parse MCP config ${explicitConfig}: ${err.message}`);
|
|
3676
4004
|
}
|
|
@@ -3686,10 +4014,10 @@ function profileMcp(p) {
|
|
|
3686
4014
|
capabilities: extractMcpCapabilities(p)
|
|
3687
4015
|
};
|
|
3688
4016
|
}
|
|
3689
|
-
const pkgPath =
|
|
4017
|
+
const pkgPath = join16(p, "package.json");
|
|
3690
4018
|
let pkg2;
|
|
3691
4019
|
try {
|
|
3692
|
-
pkg2 = JSON.parse(
|
|
4020
|
+
pkg2 = JSON.parse(readFileSync14(pkgPath, "utf-8"));
|
|
3693
4021
|
} catch (err) {
|
|
3694
4022
|
throw new Error(`could not parse package.json ${pkgPath}: ${err.message}`);
|
|
3695
4023
|
}
|
|
@@ -3706,14 +4034,14 @@ function profileMcp(p) {
|
|
|
3706
4034
|
}
|
|
3707
4035
|
|
|
3708
4036
|
// src/gate/mcpOverlap.ts
|
|
3709
|
-
import { existsSync as
|
|
3710
|
-
import { resolve as
|
|
4037
|
+
import { existsSync as existsSync14, readFileSync as readFileSync15 } from "fs";
|
|
4038
|
+
import { resolve as resolve5 } from "path";
|
|
3711
4039
|
function readTargetServers(targetPath) {
|
|
3712
|
-
const path =
|
|
3713
|
-
if (!
|
|
4040
|
+
const path = resolve5(expandHome(targetPath));
|
|
4041
|
+
if (!existsSync14(path)) return [];
|
|
3714
4042
|
let cfg;
|
|
3715
4043
|
try {
|
|
3716
|
-
cfg = JSON.parse(
|
|
4044
|
+
cfg = JSON.parse(readFileSync15(path, "utf-8"));
|
|
3717
4045
|
} catch {
|
|
3718
4046
|
return [];
|
|
3719
4047
|
}
|
|
@@ -3769,7 +4097,7 @@ function score2(candidate, target) {
|
|
|
3769
4097
|
function scanMcpOverlap(candidate, mcpTargetPaths) {
|
|
3770
4098
|
const ranking = [];
|
|
3771
4099
|
for (const targetPath of mcpTargetPaths) {
|
|
3772
|
-
const resolvedPath =
|
|
4100
|
+
const resolvedPath = resolve5(expandHome(targetPath));
|
|
3773
4101
|
for (const target of readTargetServers(targetPath)) {
|
|
3774
4102
|
ranking.push({ skill: target.name, path: resolvedPath, score: score2(candidate, target) });
|
|
3775
4103
|
}
|
|
@@ -3786,13 +4114,13 @@ function scanMcpOverlap(candidate, mcpTargetPaths) {
|
|
|
3786
4114
|
}
|
|
3787
4115
|
|
|
3788
4116
|
// src/gate/skillMap.ts
|
|
3789
|
-
import { readFileSync as
|
|
3790
|
-
import { resolve as
|
|
4117
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
4118
|
+
import { resolve as resolve6 } from "path";
|
|
3791
4119
|
function loadSkillMap(path) {
|
|
3792
|
-
const resolved =
|
|
4120
|
+
const resolved = resolve6(expandHome(path));
|
|
3793
4121
|
let text;
|
|
3794
4122
|
try {
|
|
3795
|
-
text =
|
|
4123
|
+
text = readFileSync16(resolved, "utf-8");
|
|
3796
4124
|
} catch (err) {
|
|
3797
4125
|
throw new Error(`could not read skill map at ${resolved}: ${err.message}`);
|
|
3798
4126
|
}
|
|
@@ -4092,7 +4420,7 @@ function resolveMcpCandidatePath(entry) {
|
|
|
4092
4420
|
children = readdirSync7(entry.quarantinePath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
4093
4421
|
} catch {
|
|
4094
4422
|
}
|
|
4095
|
-
return children.length === 1 ?
|
|
4423
|
+
return children.length === 1 ? join17(entry.quarantinePath, children[0]) : entry.quarantinePath;
|
|
4096
4424
|
}
|
|
4097
4425
|
async function runGate(candidatePath, skillsRoot, {
|
|
4098
4426
|
proFeatureLabel = "overlap analysis",
|
|
@@ -4141,17 +4469,17 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
4141
4469
|
|
|
4142
4470
|
// src/queue.ts
|
|
4143
4471
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
4144
|
-
import { existsSync as
|
|
4145
|
-
import { join as
|
|
4472
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync17, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
4473
|
+
import { join as join18 } from "path";
|
|
4146
4474
|
function getQueuePath() {
|
|
4147
|
-
return
|
|
4475
|
+
return join18(getHomeDir(), "queue.json");
|
|
4148
4476
|
}
|
|
4149
4477
|
function readQueue() {
|
|
4150
4478
|
const path = getQueuePath();
|
|
4151
|
-
if (!
|
|
4479
|
+
if (!existsSync15(path)) {
|
|
4152
4480
|
return { version: 1, entries: [] };
|
|
4153
4481
|
}
|
|
4154
|
-
const raw =
|
|
4482
|
+
const raw = readFileSync17(path, "utf-8");
|
|
4155
4483
|
const parsed = JSON.parse(raw);
|
|
4156
4484
|
return { version: 1, entries: parsed.entries ?? [] };
|
|
4157
4485
|
}
|
|
@@ -4159,7 +4487,7 @@ function writeQueue(queue2) {
|
|
|
4159
4487
|
const home = getHomeDir();
|
|
4160
4488
|
mkdirSync6(home, { recursive: true });
|
|
4161
4489
|
const finalPath = getQueuePath();
|
|
4162
|
-
const tempPath =
|
|
4490
|
+
const tempPath = join18(home, `.queue.json.tmp-${randomUUID3()}`);
|
|
4163
4491
|
try {
|
|
4164
4492
|
writeFileSync6(tempPath, JSON.stringify(queue2, null, 2) + "\n", "utf-8");
|
|
4165
4493
|
renameSync4(tempPath, finalPath);
|
|
@@ -4208,19 +4536,19 @@ function generateId(existingIds) {
|
|
|
4208
4536
|
|
|
4209
4537
|
// src/handoff.ts
|
|
4210
4538
|
import { spawnSync } from "child_process";
|
|
4211
|
-
import { existsSync as
|
|
4212
|
-
import { dirname as dirname7, isAbsolute as
|
|
4539
|
+
import { existsSync as existsSync16 } from "fs";
|
|
4540
|
+
import { dirname as dirname7, isAbsolute as isAbsolute3, join as join19, win32 } from "path";
|
|
4213
4541
|
import { fileURLToPath } from "url";
|
|
4214
4542
|
var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
|
|
4215
4543
|
var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
|
|
4216
4544
|
function resolvePromptPath(assetName = "ingest-prompt.md") {
|
|
4217
|
-
if (assetName.length === 0 || assetName.includes("\0") ||
|
|
4545
|
+
if (assetName.length === 0 || assetName.includes("\0") || isAbsolute3(assetName) || win32.isAbsolute(assetName) || assetName.split(/[\\/]/).includes("..")) {
|
|
4218
4546
|
throw new UserError(`Invalid handoff prompt asset path: ${assetName}`);
|
|
4219
4547
|
}
|
|
4220
4548
|
const here = dirname7(fileURLToPath(import.meta.url));
|
|
4221
|
-
const builtCandidate =
|
|
4222
|
-
if (
|
|
4223
|
-
return
|
|
4549
|
+
const builtCandidate = join19(here, assetName);
|
|
4550
|
+
if (existsSync16(builtCandidate)) return builtCandidate;
|
|
4551
|
+
return join19(here, "..", "assets", assetName);
|
|
4224
4552
|
}
|
|
4225
4553
|
function defaultIsOnPath(bin) {
|
|
4226
4554
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
@@ -4282,15 +4610,15 @@ function runHandoff(config, installedPath, options = {}) {
|
|
|
4282
4610
|
}
|
|
4283
4611
|
|
|
4284
4612
|
// src/commands/init.ts
|
|
4285
|
-
import { existsSync as
|
|
4613
|
+
import { existsSync as existsSync19 } from "fs";
|
|
4286
4614
|
import { homedir as homedir5 } from "os";
|
|
4287
|
-
import { join as
|
|
4615
|
+
import { join as join25 } from "path";
|
|
4288
4616
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
4289
4617
|
|
|
4290
4618
|
// src/agents.ts
|
|
4291
4619
|
import { statSync as statSync8 } from "fs";
|
|
4292
4620
|
import { homedir as homedir3 } from "os";
|
|
4293
|
-
import { join as
|
|
4621
|
+
import { join as join20, resolve as resolve7 } from "path";
|
|
4294
4622
|
function isFile(p) {
|
|
4295
4623
|
try {
|
|
4296
4624
|
return statSync8(p).isFile();
|
|
@@ -4299,12 +4627,12 @@ function isFile(p) {
|
|
|
4299
4627
|
}
|
|
4300
4628
|
}
|
|
4301
4629
|
function projectPath(root, cwd) {
|
|
4302
|
-
return root ?
|
|
4630
|
+
return root ? resolve7(cwd, root) : void 0;
|
|
4303
4631
|
}
|
|
4304
4632
|
function globalPath(root, home) {
|
|
4305
4633
|
if (!root) return void 0;
|
|
4306
4634
|
if (root === "~") return home;
|
|
4307
|
-
if (root.startsWith("~/")) return
|
|
4635
|
+
if (root.startsWith("~/")) return join20(home, root.slice(2));
|
|
4308
4636
|
return root;
|
|
4309
4637
|
}
|
|
4310
4638
|
function makeAgent(spec) {
|
|
@@ -4321,30 +4649,30 @@ function makeAgent(spec) {
|
|
|
4321
4649
|
};
|
|
4322
4650
|
return agent;
|
|
4323
4651
|
}
|
|
4324
|
-
function resolveAgentRoot(agent,
|
|
4325
|
-
return
|
|
4652
|
+
function resolveAgentRoot(agent, scope2, cwd = process.cwd(), home = homedir3()) {
|
|
4653
|
+
return scope2 === "project" ? projectPath(agent.projectRoot, cwd) : globalPath(agent.globalRoot, home);
|
|
4326
4654
|
}
|
|
4327
4655
|
function agentCliPresence(agent, pathEnv = process.env.PATH) {
|
|
4328
4656
|
if (!agent.cliBinaries || agent.cliBinaries.length === 0) return "unknown";
|
|
4329
4657
|
return agent.cliBinaries.some((bin) => resolveBinaryOnPath(bin, pathEnv) !== void 0) ? "installed" : "absent";
|
|
4330
4658
|
}
|
|
4331
4659
|
function detectAgentRoot(agent, cwd = process.cwd(), home = homedir3()) {
|
|
4332
|
-
for (const
|
|
4333
|
-
const root = resolveAgentRoot(agent,
|
|
4334
|
-
if (root && isDir(root)) return { scope, root };
|
|
4660
|
+
for (const scope2 of ["project", "global"]) {
|
|
4661
|
+
const root = resolveAgentRoot(agent, scope2, cwd, home);
|
|
4662
|
+
if (root && isDir(root)) return { scope: scope2, root };
|
|
4335
4663
|
}
|
|
4336
4664
|
return void 0;
|
|
4337
4665
|
}
|
|
4338
|
-
function resolveMcpConfigPath(agent,
|
|
4666
|
+
function resolveMcpConfigPath(agent, scope2, cwd = process.cwd(), home = homedir3()) {
|
|
4339
4667
|
if (!agent.mcpConfig) return void 0;
|
|
4340
|
-
return
|
|
4668
|
+
return scope2 === "project" ? projectPath(agent.mcpConfig.project, cwd) : globalPath(agent.mcpConfig.global, home);
|
|
4341
4669
|
}
|
|
4342
4670
|
function detectMcpConfigFiles(agent, cwd = process.cwd(), home = homedir3()) {
|
|
4343
4671
|
if (!agent.mcpConfig) return [];
|
|
4344
4672
|
const found = [];
|
|
4345
|
-
for (const
|
|
4346
|
-
const path = resolveMcpConfigPath(agent,
|
|
4347
|
-
if (path && isFile(path)) found.push({ scope, path });
|
|
4673
|
+
for (const scope2 of ["project", "global"]) {
|
|
4674
|
+
const path = resolveMcpConfigPath(agent, scope2, cwd, home);
|
|
4675
|
+
if (path && isFile(path)) found.push({ scope: scope2, path });
|
|
4348
4676
|
}
|
|
4349
4677
|
return found;
|
|
4350
4678
|
}
|
|
@@ -4534,12 +4862,12 @@ var AGENTS = [
|
|
|
4534
4862
|
].map(makeAgent);
|
|
4535
4863
|
|
|
4536
4864
|
// src/gitPreflight.ts
|
|
4537
|
-
import { relative as
|
|
4865
|
+
import { relative as relative3, sep as sep4 } from "path";
|
|
4538
4866
|
function splitNulTerminated(output) {
|
|
4539
4867
|
return output.split("\0").filter((s) => s.length > 0);
|
|
4540
4868
|
}
|
|
4541
4869
|
function toPosixPath(p) {
|
|
4542
|
-
return p.split(
|
|
4870
|
+
return p.split(sep4).join("/");
|
|
4543
4871
|
}
|
|
4544
4872
|
var missingResult = (root) => ({
|
|
4545
4873
|
root,
|
|
@@ -4569,7 +4897,7 @@ async function evaluateGitPreflight(root, deps = {}) {
|
|
|
4569
4897
|
if (toplevel.code !== 0) return notInRepoResult(root);
|
|
4570
4898
|
const repoRoot = toplevel.stdout.trim();
|
|
4571
4899
|
if (!repoRoot) return notInRepoResult(root);
|
|
4572
|
-
const rel = toPosixPath(
|
|
4900
|
+
const rel = toPosixPath(relative3(repoRoot, root));
|
|
4573
4901
|
const pathspec = rel === "" ? "." : rel;
|
|
4574
4902
|
const tracked = await exec("git", ["--literal-pathspecs", "-C", repoRoot, "ls-files", "-z", "--", pathspec], { cwd: repoRoot });
|
|
4575
4903
|
const untracked = await exec(
|
|
@@ -4640,7 +4968,7 @@ function untrackedAddCommitCommands(root, repoRoot) {
|
|
|
4640
4968
|
];
|
|
4641
4969
|
}
|
|
4642
4970
|
function ignoredUnignoreSuggestions(root, repoRoot) {
|
|
4643
|
-
const rel = toPosixPath(
|
|
4971
|
+
const rel = toPosixPath(relative3(repoRoot, root));
|
|
4644
4972
|
return [`!${rel}/`, `!${rel}/**`];
|
|
4645
4973
|
}
|
|
4646
4974
|
async function runGitCommands(commands, deps = {}) {
|
|
@@ -4663,9 +4991,52 @@ function describeGitPreflight(result) {
|
|
|
4663
4991
|
return parts.join(", ");
|
|
4664
4992
|
}
|
|
4665
4993
|
|
|
4994
|
+
// src/pluginInventory.ts
|
|
4995
|
+
import { readFileSync as readFileSync18, realpathSync as realpathSync4, statSync as statSync9 } from "fs";
|
|
4996
|
+
import { isAbsolute as isAbsolute4, join as join21, relative as relative4, sep as sep5 } from "path";
|
|
4997
|
+
function readPluginInventory(path) {
|
|
4998
|
+
const data = JSON.parse(readFileSync18(path, "utf8"));
|
|
4999
|
+
if (data.schemaVersion !== 1 || data.kind !== "skill-forge-host-inventory" || !["claude", "codex"].includes(data.host) || !Array.isArray(data.plugins) || typeof data.complete !== "boolean" || typeof data.generatedAt !== "string" || !Number.isFinite(Date.parse(data.generatedAt))) throw new UserError("Invalid host inventory export.");
|
|
5000
|
+
const roots = [];
|
|
5001
|
+
const notices = ["Imported host inventory is static evidence; runtime discovery/invocation is unverified."];
|
|
5002
|
+
if (Array.isArray(data.notices)) notices.push(...data.notices.filter((n) => typeof n === "string"));
|
|
5003
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5004
|
+
let complete = data.complete;
|
|
5005
|
+
for (const row of data.plugins) {
|
|
5006
|
+
if (!row || row.host !== data.host || typeof row.pluginId !== "string" || !/^[\w.-]+@[\w.-]+$/.test(row.pluginId) || ids.has(row.pluginId)) {
|
|
5007
|
+
throw new UserError("Invalid or duplicate host inventory plugin identity.");
|
|
5008
|
+
}
|
|
5009
|
+
ids.add(row.pluginId);
|
|
5010
|
+
if (row.enabled === false && row.status === "disabled") continue;
|
|
5011
|
+
if (row.enabled !== true || row.status !== "installed") {
|
|
5012
|
+
complete = false;
|
|
5013
|
+
notices.push(`${row.pluginId}: installed skills unavailable (${typeof row.reason === "string" ? row.reason : "unknown state"}).`);
|
|
5014
|
+
continue;
|
|
5015
|
+
}
|
|
5016
|
+
if (typeof row.installPath !== "string" || !isAbsolute4(row.installPath) || typeof row.version !== "string" || !Array.isArray(row.skillDirs) || row.skillDirs.some((p) => typeof p !== "string" || !isAbsolute4(p))) {
|
|
5017
|
+
throw new UserError("Invalid host inventory install paths/version.");
|
|
5018
|
+
}
|
|
5019
|
+
const installPath = realpathSync4(row.installPath);
|
|
5020
|
+
const skillDirs = [...new Set(row.skillDirs.map((dir) => {
|
|
5021
|
+
const real = realpathSync4(dir);
|
|
5022
|
+
for (const target of [real, realpathSync4(join21(real, "SKILL.md"))]) {
|
|
5023
|
+
const rel = relative4(installPath, target);
|
|
5024
|
+
if (rel === ".." || rel.startsWith(".." + sep5) || isAbsolute4(rel)) throw new UserError("Imported skill escapes its plugin install.");
|
|
5025
|
+
}
|
|
5026
|
+
if (!statSync9(join21(real, "SKILL.md")).isFile()) throw new UserError("Imported SKILL.md is not a file.");
|
|
5027
|
+
return real;
|
|
5028
|
+
}))].sort();
|
|
5029
|
+
const [plugin, marketplace] = row.pluginId.split("@");
|
|
5030
|
+
roots.push({ pluginId: row.pluginId, plugin, marketplace, version: row.version, installPath, skillDirs });
|
|
5031
|
+
}
|
|
5032
|
+
if (!complete) notices.push("Host inventory coverage is incomplete; missing plugins are not evidence of removal.");
|
|
5033
|
+
if (Date.now() - Date.parse(data.generatedAt) > 24 * 60 * 60 * 1e3) notices.push("Host inventory export is older than 24 hours; refresh it before making retention decisions.");
|
|
5034
|
+
return { host: data.host, roots, notices, complete, generatedAt: data.generatedAt };
|
|
5035
|
+
}
|
|
5036
|
+
|
|
4666
5037
|
// src/commands/audit.ts
|
|
4667
|
-
import { existsSync as
|
|
4668
|
-
import { join as
|
|
5038
|
+
import { existsSync as existsSync18 } from "fs";
|
|
5039
|
+
import { join as join24, resolve as resolvePath5 } from "path";
|
|
4669
5040
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
4670
5041
|
|
|
4671
5042
|
// src/decision.ts
|
|
@@ -4698,13 +5069,13 @@ ${prompt}`);
|
|
|
4698
5069
|
}
|
|
4699
5070
|
|
|
4700
5071
|
// src/pluginRoots.ts
|
|
4701
|
-
import { existsSync as
|
|
5072
|
+
import { existsSync as existsSync17, readFileSync as readFileSync19, readdirSync as readdirSync8, realpathSync as realpathSync5 } from "fs";
|
|
4702
5073
|
import { homedir as homedir4 } from "os";
|
|
4703
|
-
import { isAbsolute as
|
|
5074
|
+
import { isAbsolute as isAbsolute5, join as join22, relative as relative5, resolve as resolvePath3, sep as sep6 } from "path";
|
|
4704
5075
|
function readJsonFileSafe(path) {
|
|
4705
5076
|
let raw;
|
|
4706
5077
|
try {
|
|
4707
|
-
raw =
|
|
5078
|
+
raw = readFileSync19(path, "utf-8");
|
|
4708
5079
|
} catch (err) {
|
|
4709
5080
|
return { problem: err.code === "ENOENT" ? "missing" : "unreadable" };
|
|
4710
5081
|
}
|
|
@@ -4740,12 +5111,12 @@ function extractRegistry(data) {
|
|
|
4740
5111
|
const entries = [];
|
|
4741
5112
|
for (const raw of entriesRaw) {
|
|
4742
5113
|
const rec = typeof raw === "object" && raw !== null ? raw : {};
|
|
4743
|
-
const
|
|
5114
|
+
const scope2 = rec.scope;
|
|
4744
5115
|
const installPath = rec.installPath;
|
|
4745
5116
|
const version = rec.version;
|
|
4746
|
-
if ((
|
|
5117
|
+
if ((scope2 === "user" || scope2 === "project" || scope2 === "local") && typeof installPath === "string" && typeof version === "string") {
|
|
4747
5118
|
entries.push({
|
|
4748
|
-
scope,
|
|
5119
|
+
scope: scope2,
|
|
4749
5120
|
projectPath: typeof rec.projectPath === "string" ? rec.projectPath : void 0,
|
|
4750
5121
|
installPath,
|
|
4751
5122
|
version
|
|
@@ -4765,7 +5136,7 @@ function splitPluginId(id) {
|
|
|
4765
5136
|
}
|
|
4766
5137
|
function safeRealpath(p) {
|
|
4767
5138
|
try {
|
|
4768
|
-
return
|
|
5139
|
+
return realpathSync5(p);
|
|
4769
5140
|
} catch {
|
|
4770
5141
|
return void 0;
|
|
4771
5142
|
}
|
|
@@ -4800,8 +5171,8 @@ function selectInstallEntry(entries, cwd) {
|
|
|
4800
5171
|
}
|
|
4801
5172
|
function isInside2(child, root) {
|
|
4802
5173
|
if (child === root) return true;
|
|
4803
|
-
const rel =
|
|
4804
|
-
return rel !== "" && !rel.startsWith(".." +
|
|
5174
|
+
const rel = relative5(root, child);
|
|
5175
|
+
return rel !== "" && !rel.startsWith(".." + sep6) && rel !== ".." && !isAbsolute5(rel);
|
|
4805
5176
|
}
|
|
4806
5177
|
var DEFAULT_SKILL_LAYOUT = { paths: ["./skills/"], explicit: false };
|
|
4807
5178
|
function normalizeDeclaredSkillsField(manifest, pluginId, notices) {
|
|
@@ -4817,7 +5188,7 @@ function resolveDeclaredPath(installPath, realInstall, declaredPath, explicit, p
|
|
|
4817
5188
|
const joined = resolvePath3(installPath, declaredPath);
|
|
4818
5189
|
let real;
|
|
4819
5190
|
try {
|
|
4820
|
-
real =
|
|
5191
|
+
real = realpathSync5(joined);
|
|
4821
5192
|
} catch {
|
|
4822
5193
|
if (explicit) notices.push(`plugin ${pluginId}: declared skill path does not exist, skipping: ${declaredPath}`);
|
|
4823
5194
|
return [];
|
|
@@ -4826,7 +5197,7 @@ function resolveDeclaredPath(installPath, realInstall, declaredPath, explicit, p
|
|
|
4826
5197
|
notices.push(`plugin ${pluginId}: declared skill path escapes the install root, skipping: ${declaredPath}`);
|
|
4827
5198
|
return [];
|
|
4828
5199
|
}
|
|
4829
|
-
if (
|
|
5200
|
+
if (existsSync17(join22(real, "SKILL.md"))) return [real];
|
|
4830
5201
|
let entries;
|
|
4831
5202
|
try {
|
|
4832
5203
|
entries = readdirSync8(real, { withFileTypes: true });
|
|
@@ -4835,24 +5206,24 @@ function resolveDeclaredPath(installPath, realInstall, declaredPath, explicit, p
|
|
|
4835
5206
|
}
|
|
4836
5207
|
const found = [];
|
|
4837
5208
|
for (const entry of entries) {
|
|
4838
|
-
let childPath =
|
|
5209
|
+
let childPath = join22(real, entry.name);
|
|
4839
5210
|
if (entry.isSymbolicLink()) {
|
|
4840
5211
|
const target = safeRealpath(childPath);
|
|
4841
5212
|
if (!target || !isInside2(target, realInstall)) {
|
|
4842
|
-
notices.push(`plugin ${pluginId}: skill dir symlink escapes the install root, skipping: ${
|
|
5213
|
+
notices.push(`plugin ${pluginId}: skill dir symlink escapes the install root, skipping: ${join22(declaredPath, entry.name)}`);
|
|
4843
5214
|
continue;
|
|
4844
5215
|
}
|
|
4845
5216
|
childPath = target;
|
|
4846
5217
|
} else if (!entry.isDirectory()) {
|
|
4847
5218
|
continue;
|
|
4848
5219
|
}
|
|
4849
|
-
if (
|
|
5220
|
+
if (existsSync17(join22(childPath, "SKILL.md"))) found.push(childPath);
|
|
4850
5221
|
}
|
|
4851
5222
|
return found;
|
|
4852
5223
|
}
|
|
4853
5224
|
function resolveSkillDirs(installPath, pluginId) {
|
|
4854
5225
|
const notices = [];
|
|
4855
|
-
const manifestPath =
|
|
5226
|
+
const manifestPath = join22(installPath, ".claude-plugin", "plugin.json");
|
|
4856
5227
|
const manifestResult = readJsonFileSafe(manifestPath);
|
|
4857
5228
|
if (manifestResult.problem) {
|
|
4858
5229
|
notices.push(`plugin ${pluginId}: ${manifestResult.problem} manifest at ${manifestPath} \u2014 using default skill layout`);
|
|
@@ -4870,9 +5241,9 @@ function resolveSkillDirs(installPath, pluginId) {
|
|
|
4870
5241
|
function discoverClaudePluginRoots(options = {}) {
|
|
4871
5242
|
const homeDir = options.homeDir ?? homedir4();
|
|
4872
5243
|
const cwd = options.cwd ?? process.cwd();
|
|
4873
|
-
const settingsPath = options.settingsPath ??
|
|
4874
|
-
const localSettingsPath = options.localSettingsPath ??
|
|
4875
|
-
const installedPluginsPath = options.installedPluginsPath ??
|
|
5244
|
+
const settingsPath = options.settingsPath ?? join22(homeDir, ".claude", "settings.json");
|
|
5245
|
+
const localSettingsPath = options.localSettingsPath ?? join22(cwd, ".claude", "settings.local.json");
|
|
5246
|
+
const installedPluginsPath = options.installedPluginsPath ?? join22(homeDir, ".claude", "plugins", "installed_plugins.json");
|
|
4876
5247
|
const notices = [];
|
|
4877
5248
|
const settingsResult = readJsonFileSafe(settingsPath);
|
|
4878
5249
|
if (settingsResult.problem) {
|
|
@@ -4886,7 +5257,7 @@ function discoverClaudePluginRoots(options = {}) {
|
|
|
4886
5257
|
}
|
|
4887
5258
|
const globalEnabled = extractEnabledMap(settingsResult.data);
|
|
4888
5259
|
let localEnabled = {};
|
|
4889
|
-
if (
|
|
5260
|
+
if (existsSync17(localSettingsPath)) {
|
|
4890
5261
|
const localResult = readJsonFileSafe(localSettingsPath);
|
|
4891
5262
|
if (localResult.problem) {
|
|
4892
5263
|
notices.push(`local settings ${describeProblem(localResult.problem, localSettingsPath)} \u2014 ignoring local plugin overrides`);
|
|
@@ -4919,10 +5290,10 @@ function discoverClaudePluginRoots(options = {}) {
|
|
|
4919
5290
|
}
|
|
4920
5291
|
|
|
4921
5292
|
// src/usageSnapshot.ts
|
|
4922
|
-
import { readFileSync as
|
|
4923
|
-
import { extname as extname7, join as
|
|
5293
|
+
import { readFileSync as readFileSync20, readdirSync as readdirSync9 } from "fs";
|
|
5294
|
+
import { extname as extname7, join as join23, resolve as resolvePath4 } from "path";
|
|
4924
5295
|
function isValidCount(value) {
|
|
4925
|
-
return typeof value === "number" && Number.
|
|
5296
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
4926
5297
|
}
|
|
4927
5298
|
function toSnapshot(parsed, sourcePath) {
|
|
4928
5299
|
return {
|
|
@@ -4949,24 +5320,28 @@ function parseSnapshotFile(raw) {
|
|
|
4949
5320
|
if (!report) return { error: 'usage snapshot has no "report" object' };
|
|
4950
5321
|
const generatedAt = typeof report.generated_at === "string" ? report.generated_at : void 0;
|
|
4951
5322
|
const windowDays = typeof report.window_days === "number" ? report.window_days : void 0;
|
|
4952
|
-
const uniqueSkillsUsed =
|
|
5323
|
+
const uniqueSkillsUsed = report.unique_skills_used;
|
|
4953
5324
|
const skillTotalsRaw = report.skill_totals;
|
|
4954
5325
|
const skillTotals = skillTotalsRaw && typeof skillTotalsRaw === "object" && !Array.isArray(skillTotalsRaw) ? skillTotalsRaw : void 0;
|
|
4955
|
-
|
|
5326
|
+
let warning = skillTotalsRaw !== void 0 && skillTotals === void 0 ? 'usage snapshot: "skill_totals" is not an object \u2014 ignoring it' : void 0;
|
|
4956
5327
|
const topSkills = Array.isArray(report.top_skills) ? report.top_skills : void 0;
|
|
4957
5328
|
const usage = /* @__PURE__ */ new Map();
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
if (Array.isArray(row) && row.length === 2 && typeof row[0] === "string" && isValidCount(row[1])) {
|
|
4965
|
-
usage.set(row[0], row[1]);
|
|
4966
|
-
}
|
|
5329
|
+
let invalidRows = false;
|
|
5330
|
+
const rows = skillTotals ? Object.entries(skillTotals) : topSkills ?? [];
|
|
5331
|
+
for (const row of rows) {
|
|
5332
|
+
if (!Array.isArray(row) || row.length !== 2 || typeof row[0] !== "string" || !row[0].trim() || !isValidCount(row[1]) || usage.has(row[0])) {
|
|
5333
|
+
invalidRows = true;
|
|
5334
|
+
continue;
|
|
4967
5335
|
}
|
|
5336
|
+
usage.set(row[0], row[1]);
|
|
5337
|
+
}
|
|
5338
|
+
const observed = [...usage.values()].filter((count) => count > 0).length;
|
|
5339
|
+
const validUniqueCount = isValidCount(uniqueSkillsUsed);
|
|
5340
|
+
const inconsistentTotal = uniqueSkillsUsed !== void 0 && (!validUniqueCount || (skillTotals ? observed !== uniqueSkillsUsed : observed > uniqueSkillsUsed));
|
|
5341
|
+
const exhaustive = !invalidRows && !inconsistentTotal && (Boolean(skillTotals) || topSkills !== void 0 && validUniqueCount && observed === uniqueSkillsUsed);
|
|
5342
|
+
if (invalidRows || inconsistentTotal) {
|
|
5343
|
+
warning = [warning, "usage snapshot contains invalid, duplicate, or inconsistent counts \u2014 coverage is not exhaustive"].filter(Boolean).join("; ");
|
|
4968
5344
|
}
|
|
4969
|
-
const exhaustive = Boolean(skillTotals) || topSkills !== void 0 && uniqueSkillsUsed !== void 0 && topSkills.length === uniqueSkillsUsed;
|
|
4970
5345
|
return { usage, exhaustive, windowDays, generatedAt, warning };
|
|
4971
5346
|
}
|
|
4972
5347
|
function pickLatest(candidates) {
|
|
@@ -4995,10 +5370,10 @@ function loadUsageSnapshot(pathOrDir, opts = {}) {
|
|
|
4995
5370
|
}
|
|
4996
5371
|
const candidates = [];
|
|
4997
5372
|
for (const name of entries) {
|
|
4998
|
-
const filePath =
|
|
5373
|
+
const filePath = join23(resolved, name);
|
|
4999
5374
|
let raw2;
|
|
5000
5375
|
try {
|
|
5001
|
-
raw2 =
|
|
5376
|
+
raw2 = readFileSync20(filePath, "utf-8");
|
|
5002
5377
|
} catch {
|
|
5003
5378
|
continue;
|
|
5004
5379
|
}
|
|
@@ -5018,7 +5393,7 @@ function loadUsageSnapshot(pathOrDir, opts = {}) {
|
|
|
5018
5393
|
}
|
|
5019
5394
|
let raw;
|
|
5020
5395
|
try {
|
|
5021
|
-
raw =
|
|
5396
|
+
raw = readFileSync20(resolved, "utf-8");
|
|
5022
5397
|
} catch (err) {
|
|
5023
5398
|
return emptySnapshot(resolved, `could not read usage snapshot (${err.message})`);
|
|
5024
5399
|
}
|
|
@@ -5033,18 +5408,21 @@ function resolvePluginAndUsageContext(config, options, deps = {}) {
|
|
|
5033
5408
|
if (options.usageWindow !== void 0 && (!Number.isInteger(options.usageWindow) || options.usageWindow < 1)) {
|
|
5034
5409
|
throw new UserError(`--usage-window must be a positive integer (got "${options.usageWindow}")`);
|
|
5035
5410
|
}
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
const
|
|
5411
|
+
if (options.pluginInventory && options.claudePlugins === true) throw new UserError("--plugin-inventory and --claude-plugins cannot be combined. Audit each host separately.");
|
|
5412
|
+
const importedInventory = options.pluginInventory ? readPluginInventory(options.pluginInventory) : void 0;
|
|
5413
|
+
const wantPlugins = !importedInventory && (options.claudePlugins ?? config.claudePlugins ?? false);
|
|
5414
|
+
let pluginRoots = importedInventory?.roots;
|
|
5415
|
+
const notices = [...importedInventory?.notices ?? []];
|
|
5039
5416
|
if (wantPlugins) {
|
|
5040
5417
|
const discovered = discoverClaudePluginRoots(deps);
|
|
5041
5418
|
pluginRoots = discovered.roots;
|
|
5042
5419
|
notices.push(...discovered.notices);
|
|
5043
5420
|
}
|
|
5044
|
-
|
|
5421
|
+
if (importedInventory?.host === "codex" && options.usageSnapshot) throw new UserError("Claude skill-monitor snapshots cannot be joined to Codex inventory.");
|
|
5422
|
+
const usageSnapshotPath = importedInventory?.host === "codex" ? void 0 : options.usageSnapshot ?? config.usageSnapshotPath;
|
|
5045
5423
|
const usage = usageSnapshotPath ? loadUsageSnapshot(usageSnapshotPath, { windowDays: options.usageWindow }) : void 0;
|
|
5046
5424
|
if (usage?.warning) notices.push(usage.warning);
|
|
5047
|
-
return { pluginRoots, usage, notices };
|
|
5425
|
+
return { pluginRoots, usage, notices, importedInventory };
|
|
5048
5426
|
}
|
|
5049
5427
|
function splitCsv(s) {
|
|
5050
5428
|
return s.split(",").map((x) => x.trim()).filter(Boolean);
|
|
@@ -5080,12 +5458,12 @@ async function captureBusinessProfile(rl, existing) {
|
|
|
5080
5458
|
function resolveReportPath(reportOption) {
|
|
5081
5459
|
if (reportOption) return resolvePath5(reportOption);
|
|
5082
5460
|
const iso = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5083
|
-
return
|
|
5461
|
+
return join24(getHomeDir(), "reports", `audit-${iso}.md`);
|
|
5084
5462
|
}
|
|
5085
|
-
function summarizePluginRecommendations(
|
|
5463
|
+
function summarizePluginRecommendations(plugins2) {
|
|
5086
5464
|
const counts = { keep: 0, review: 0, unobserved: 0, unknown: 0 };
|
|
5087
|
-
for (const p of
|
|
5088
|
-
return `plugins: ${
|
|
5465
|
+
for (const p of plugins2) counts[p.recommendation]++;
|
|
5466
|
+
return `plugins: ${plugins2.length} (review ${counts.review}, unobserved ${counts.unobserved}, unknown ${counts.unknown})`;
|
|
5089
5467
|
}
|
|
5090
5468
|
function printTerminalSummary(report, reportPath, usage) {
|
|
5091
5469
|
const { summary } = report;
|
|
@@ -5141,7 +5519,7 @@ async function maybeHandoff(config, reportPath, options, rl) {
|
|
|
5141
5519
|
}
|
|
5142
5520
|
async function auditCommand(options = {}, deps = {}) {
|
|
5143
5521
|
await maybeOfferInit({ json: options.json, yes: options.yes, suppressAuditOffer: true });
|
|
5144
|
-
if (!
|
|
5522
|
+
if (!existsSync18(getConfigPath())) {
|
|
5145
5523
|
throw new UserError(
|
|
5146
5524
|
"no skill-forge config found \u2014 run `skill-forge init` first (audit requires a configured skill/MCP set; it never audits an invented default)."
|
|
5147
5525
|
);
|
|
@@ -5157,11 +5535,17 @@ async function auditCommand(options = {}, deps = {}) {
|
|
|
5157
5535
|
saveConfig(config);
|
|
5158
5536
|
}
|
|
5159
5537
|
}
|
|
5160
|
-
const { pluginRoots, usage, notices: pluginUsageNotices } = resolvePluginAndUsageContext(config, options, deps);
|
|
5538
|
+
const { pluginRoots, usage, importedInventory, notices: pluginUsageNotices } = resolvePluginAndUsageContext(config, options, deps);
|
|
5161
5539
|
const { records: accepted, notices: storeNotices } = readAcceptedFindings();
|
|
5162
5540
|
const report = await auditConfiguredSet(config, { accepted, pluginRoots, usage });
|
|
5541
|
+
if (importedInventory) {
|
|
5542
|
+
report.hostInventory = { host: importedInventory.host, complete: importedInventory.complete, generatedAt: importedInventory.generatedAt };
|
|
5543
|
+
for (const plugin of report.plugins ?? []) plugin.host = importedInventory.host;
|
|
5544
|
+
}
|
|
5545
|
+
applyPluginDecisions(report, deps.cwd);
|
|
5163
5546
|
const extraNotices = [...storeNotices, ...pluginUsageNotices];
|
|
5164
5547
|
if (extraNotices.length > 0) report.notices = [...report.notices, ...extraNotices];
|
|
5548
|
+
if (options.compare) report.comparison = loadAuditComparison(report, options.compare);
|
|
5165
5549
|
const reportPath = resolveReportPath(options.report);
|
|
5166
5550
|
writeNewFile(reportPath, renderAuditMarkdown(report), "--report");
|
|
5167
5551
|
try {
|
|
@@ -5212,19 +5596,19 @@ function hasInstalledMember(group) {
|
|
|
5212
5596
|
return group.members.some((m) => m.presence === "installed");
|
|
5213
5597
|
}
|
|
5214
5598
|
function describeGroup(members, scopes) {
|
|
5215
|
-
const
|
|
5599
|
+
const scope2 = [...scopes].join("+");
|
|
5216
5600
|
const installed = members.filter((m) => m.presence === "installed").map((m) => m.agent.displayName);
|
|
5217
5601
|
const others = members.length - installed.length;
|
|
5218
5602
|
if (installed.length > 0) {
|
|
5219
5603
|
const share = others > 0 ? ` (+${others} other agent${others === 1 ? "" : "s"} share this path)` : "";
|
|
5220
|
-
return `on PATH: ${installed.join(", ")} (${
|
|
5604
|
+
return `on PATH: ${installed.join(", ")} (${scope2})${share}`;
|
|
5221
5605
|
}
|
|
5222
5606
|
const names = members.map((m) => m.agent.displayName);
|
|
5223
5607
|
const shown = names.slice(0, 3).join(", ");
|
|
5224
5608
|
const more = names.length > 3 ? `, +${names.length - 3} more` : "";
|
|
5225
5609
|
const suffix = members.every((m) => m.presence === "absent") ? "CLI not on PATH" : "no verified CLI name to check";
|
|
5226
5610
|
const who = names.length === 1 ? names[0] : `${names.length} agents: ${shown}${more}`;
|
|
5227
|
-
return `${who} (${
|
|
5611
|
+
return `${who} (${scope2}) \u2014 ${suffix}`;
|
|
5228
5612
|
}
|
|
5229
5613
|
function detectAgents(cwd, home, pathEnv) {
|
|
5230
5614
|
const found = [];
|
|
@@ -5490,8 +5874,8 @@ async function init(options = {}, deps = {}) {
|
|
|
5490
5874
|
"[skill-forge] No detected agent CLI was found on PATH \u2014 keeping every detected target selected."
|
|
5491
5875
|
);
|
|
5492
5876
|
}
|
|
5493
|
-
const installedPluginsPath = options.installedPluginsPath ??
|
|
5494
|
-
const claudePluginsRegistryExists =
|
|
5877
|
+
const installedPluginsPath = options.installedPluginsPath ?? join25(home, ".claude", "plugins", "installed_plugins.json");
|
|
5878
|
+
const claudePluginsRegistryExists = existsSync19(installedPluginsPath);
|
|
5495
5879
|
if (options.defaults) {
|
|
5496
5880
|
const selectedRoots = narrow ? rootGroups.filter(hasInstalledMember) : rootGroups;
|
|
5497
5881
|
const selectedMcpTargets = narrow ? mcpGroups.filter(hasInstalledMember) : mcpGroups;
|
|
@@ -5550,7 +5934,7 @@ async function init(options = {}, deps = {}) {
|
|
|
5550
5934
|
}
|
|
5551
5935
|
async function maybeOfferInit(options = {}) {
|
|
5552
5936
|
if (options.json || options.yes || !process.stdin.isTTY) return;
|
|
5553
|
-
if (
|
|
5937
|
+
if (existsSync19(getConfigPath())) return;
|
|
5554
5938
|
console.log(
|
|
5555
5939
|
"[skill-forge] No config found \u2014 run `skill-forge init` to detect your agents and set gate targets (init finishes with a `skill-forge audit` health check over what it just configured)."
|
|
5556
5940
|
);
|
|
@@ -5581,7 +5965,7 @@ async function add(source, options = {}) {
|
|
|
5581
5965
|
if (!targetRoot) {
|
|
5582
5966
|
throw new Error("no target skills root configured; pass --target <dir> or set skillsRoots in the config");
|
|
5583
5967
|
}
|
|
5584
|
-
const resolved =
|
|
5968
|
+
const resolved = resolve2(source, artifact);
|
|
5585
5969
|
const entry = await quarantine(resolved, config.quarantineDir);
|
|
5586
5970
|
const gatedFingerprint = fingerprintDirs(entry.skillDirs).digest;
|
|
5587
5971
|
if (entry.skillDirs.length === 0) {
|
|
@@ -5665,7 +6049,7 @@ async function add(source, options = {}) {
|
|
|
5665
6049
|
}
|
|
5666
6050
|
}
|
|
5667
6051
|
async function addMcp(source, config, options) {
|
|
5668
|
-
const resolved =
|
|
6052
|
+
const resolved = resolve2(source, "mcp");
|
|
5669
6053
|
const entry = await quarantine(resolved, config.quarantineDir);
|
|
5670
6054
|
const candidatePath = resolveMcpCandidatePath(entry);
|
|
5671
6055
|
const { safety, gateResult, proEnabled } = await runGate(candidatePath, "", {
|
|
@@ -5701,7 +6085,7 @@ async function addMcp(source, config, options) {
|
|
|
5701
6085
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
5702
6086
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
5703
6087
|
};
|
|
5704
|
-
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ??
|
|
6088
|
+
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join26(process.cwd(), ".mcp.json");
|
|
5705
6089
|
const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
|
|
5706
6090
|
reject(entry);
|
|
5707
6091
|
logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
|
|
@@ -5756,7 +6140,7 @@ async function scan(source, options = {}) {
|
|
|
5756
6140
|
if (artifact === "skill" && !skillsRoot) {
|
|
5757
6141
|
throw new Error("no skills root configured; set skillsRoots in the config");
|
|
5758
6142
|
}
|
|
5759
|
-
const resolved =
|
|
6143
|
+
const resolved = resolve2(source, artifact);
|
|
5760
6144
|
const entry = await quarantine(resolved, config.quarantineDir);
|
|
5761
6145
|
try {
|
|
5762
6146
|
let candidatePath;
|
|
@@ -5797,7 +6181,7 @@ async function scan(source, options = {}) {
|
|
|
5797
6181
|
}
|
|
5798
6182
|
|
|
5799
6183
|
// src/commands/list.ts
|
|
5800
|
-
import { join as
|
|
6184
|
+
import { join as join27 } from "path";
|
|
5801
6185
|
async function list() {
|
|
5802
6186
|
await maybeOfferInit();
|
|
5803
6187
|
const config = loadConfig();
|
|
@@ -5810,14 +6194,14 @@ async function list() {
|
|
|
5810
6194
|
}
|
|
5811
6195
|
console.log(`[skill-forge] ${ids.length} held in quarantine (${quarantineDir}):`);
|
|
5812
6196
|
for (const id of ids) {
|
|
5813
|
-
const skillDirs = findSkillDirs(
|
|
6197
|
+
const skillDirs = findSkillDirs(join27(quarantineDir, id));
|
|
5814
6198
|
if (skillDirs.length === 0) {
|
|
5815
6199
|
console.log(` ${id} (no SKILL.md found)`);
|
|
5816
6200
|
continue;
|
|
5817
6201
|
}
|
|
5818
6202
|
const names = skillDirs.map((dir) => {
|
|
5819
6203
|
try {
|
|
5820
|
-
return readFrontmatter(
|
|
6204
|
+
return readFrontmatter(join27(dir, "SKILL.md")).name;
|
|
5821
6205
|
} catch {
|
|
5822
6206
|
return dir;
|
|
5823
6207
|
}
|
|
@@ -5827,23 +6211,23 @@ async function list() {
|
|
|
5827
6211
|
}
|
|
5828
6212
|
|
|
5829
6213
|
// src/commands/guide.ts
|
|
5830
|
-
import { existsSync as
|
|
6214
|
+
import { existsSync as existsSync21 } from "fs";
|
|
5831
6215
|
|
|
5832
6216
|
// src/commands/held.ts
|
|
5833
|
-
import { existsSync as
|
|
5834
|
-
import { basename as basename9, join as
|
|
6217
|
+
import { existsSync as existsSync20, realpathSync as realpathSync6 } from "fs";
|
|
6218
|
+
import { basename as basename9, join as join28, resolve as resolvePath6, sep as sep7 } from "path";
|
|
5835
6219
|
var PRO_FEATURE_LABEL2 = "overlap analysis, the provenance ledger, and the pending-ingestion queue";
|
|
5836
6220
|
function resolveHeldPath(quarantineDir, id) {
|
|
5837
6221
|
if (id === "" || id !== basename9(id) || id === "." || id === "..") {
|
|
5838
6222
|
throw new UserError(`invalid quarantine id "${id}" \u2014 pass a single id exactly as printed by \`skill-forge list\``);
|
|
5839
6223
|
}
|
|
5840
|
-
const candidate =
|
|
5841
|
-
if (!
|
|
6224
|
+
const candidate = join28(quarantineDir, id);
|
|
6225
|
+
if (!existsSync20(candidate)) {
|
|
5842
6226
|
throw new UserError(`no held entry with id "${id}" \u2014 run \`skill-forge list\` to see what is in quarantine`);
|
|
5843
6227
|
}
|
|
5844
|
-
const realRoot =
|
|
5845
|
-
const realCandidate =
|
|
5846
|
-
if (!realCandidate.startsWith(realRoot +
|
|
6228
|
+
const realRoot = realpathSync6(resolvePath6(quarantineDir));
|
|
6229
|
+
const realCandidate = realpathSync6(candidate);
|
|
6230
|
+
if (!realCandidate.startsWith(realRoot + sep7)) {
|
|
5847
6231
|
throw new UserError(`refusing to act on "${id}": it resolves outside the quarantine dir (${realRoot})`);
|
|
5848
6232
|
}
|
|
5849
6233
|
return realCandidate;
|
|
@@ -5866,7 +6250,7 @@ function resolveTargetRoot(config, target) {
|
|
|
5866
6250
|
function heldSkillNames(skillDirs) {
|
|
5867
6251
|
return skillDirs.map((dir) => {
|
|
5868
6252
|
try {
|
|
5869
|
-
return readFrontmatter(
|
|
6253
|
+
return readFrontmatter(join28(dir, "SKILL.md")).name;
|
|
5870
6254
|
} catch {
|
|
5871
6255
|
return basename9(dir);
|
|
5872
6256
|
}
|
|
@@ -5874,7 +6258,7 @@ function heldSkillNames(skillDirs) {
|
|
|
5874
6258
|
}
|
|
5875
6259
|
function looksLikeMcpSandbox(quarantinePath) {
|
|
5876
6260
|
const candidate = resolveMcpCandidatePath({ quarantinePath });
|
|
5877
|
-
return [".mcp.json", "mcp.json", "package.json"].some((f) =>
|
|
6261
|
+
return [".mcp.json", "mcp.json", "package.json"].some((f) => existsSync20(join28(candidate, f)));
|
|
5878
6262
|
}
|
|
5879
6263
|
async function promoteHeldMcp(id, entry, config, options) {
|
|
5880
6264
|
const candidatePath = resolveMcpCandidatePath(entry);
|
|
@@ -5908,7 +6292,7 @@ async function promoteHeldMcp(id, entry, config, options) {
|
|
|
5908
6292
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
5909
6293
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
5910
6294
|
};
|
|
5911
|
-
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ??
|
|
6295
|
+
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join28(process.cwd(), ".mcp.json");
|
|
5912
6296
|
const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
|
|
5913
6297
|
reject(entry);
|
|
5914
6298
|
logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
|
|
@@ -6153,7 +6537,7 @@ var TOPICS = [
|
|
|
6153
6537
|
];
|
|
6154
6538
|
function readState() {
|
|
6155
6539
|
const configPath = getConfigPath();
|
|
6156
|
-
const configured =
|
|
6540
|
+
const configured = existsSync21(configPath);
|
|
6157
6541
|
const base = {
|
|
6158
6542
|
configured,
|
|
6159
6543
|
configPath,
|
|
@@ -6387,17 +6771,17 @@ async function status() {
|
|
|
6387
6771
|
// src/evolve.ts
|
|
6388
6772
|
import { createInterface as createInterface5 } from "readline/promises";
|
|
6389
6773
|
import { createHash as createHash3 } from "crypto";
|
|
6390
|
-
import { cpSync as cpSync3, existsSync as
|
|
6774
|
+
import { cpSync as cpSync3, existsSync as existsSync22, lstatSync as lstatSync4, mkdtempSync as mkdtempSync3, readFileSync as readFileSync21, rmSync as rmSync6 } from "fs";
|
|
6391
6775
|
import { tmpdir as tmpdir2 } from "os";
|
|
6392
|
-
import { basename as basename10, dirname as dirname9, join as
|
|
6776
|
+
import { basename as basename10, dirname as dirname9, join as join29, resolve as resolvePath7, sep as sep8 } from "path";
|
|
6393
6777
|
var SKILLOPT_BIN = "skillopt-sleep";
|
|
6394
6778
|
var PRO_FEATURE_LABEL3 = "self-evolution via SkillOpt-Sleep (evolve) \u2014 re-gating, provenance, and the pending-ingestion queue";
|
|
6395
6779
|
async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
6396
6780
|
const exec = deps.exec ?? defaultExec;
|
|
6397
6781
|
const json = options.json;
|
|
6398
6782
|
const resolvedTargetDir = resolvePath7(targetSkillDir);
|
|
6399
|
-
const skillMdPath =
|
|
6400
|
-
if (!
|
|
6783
|
+
const skillMdPath = join29(resolvedTargetDir, "SKILL.md");
|
|
6784
|
+
if (!existsSync22(skillMdPath)) {
|
|
6401
6785
|
throw new UserError(`no SKILL.md found at ${skillMdPath} \u2014 evolve requires an existing, installed skill directory`);
|
|
6402
6786
|
}
|
|
6403
6787
|
const proStatus = proFeatureStatus(PRO_FEATURE_LABEL3);
|
|
@@ -6455,33 +6839,33 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
6455
6839
|
}
|
|
6456
6840
|
return;
|
|
6457
6841
|
}
|
|
6458
|
-
const stagingRoot =
|
|
6842
|
+
const stagingRoot = join29(project, ".skillopt-sleep", "staging");
|
|
6459
6843
|
const stagingDir = resolvePath7(parsed.staging_dir);
|
|
6460
|
-
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot +
|
|
6844
|
+
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep8)) {
|
|
6461
6845
|
throw new UserError(
|
|
6462
6846
|
`skillopt-sleep reported a staging_dir outside the expected sandbox (${stagingRoot}): ${stagingDir} \u2014 refusing to use it`
|
|
6463
6847
|
);
|
|
6464
6848
|
}
|
|
6465
|
-
const proposedSkillMd =
|
|
6466
|
-
const proposedClaudeMd =
|
|
6467
|
-
const reportMdPath =
|
|
6468
|
-
if (!
|
|
6849
|
+
const proposedSkillMd = join29(stagingDir, "proposed_SKILL.md");
|
|
6850
|
+
const proposedClaudeMd = join29(stagingDir, "proposed_CLAUDE.md");
|
|
6851
|
+
const reportMdPath = join29(stagingDir, "report.md");
|
|
6852
|
+
if (!existsSync22(proposedSkillMd)) {
|
|
6469
6853
|
throw new UserError(`skillopt-sleep reported an accepted proposal but ${proposedSkillMd} is missing`);
|
|
6470
6854
|
}
|
|
6471
|
-
if (
|
|
6855
|
+
if (existsSync22(reportMdPath)) {
|
|
6472
6856
|
notices.push(`SkillOpt-Sleep's full report: ${reportMdPath}`);
|
|
6473
6857
|
}
|
|
6474
6858
|
assertNotSymlink(proposedSkillMd, "proposed_SKILL.md");
|
|
6475
6859
|
assertNotSymlink(proposedClaudeMd, "proposed_CLAUDE.md");
|
|
6476
6860
|
const scannedSkillHash = sha256File(proposedSkillMd);
|
|
6477
6861
|
const scannedClaudeHash = sha256File(proposedClaudeMd);
|
|
6478
|
-
const tempDir = mkdtempSync3(
|
|
6862
|
+
const tempDir = mkdtempSync3(join29(tmpdir2(), "skill-forge-evolve-"));
|
|
6479
6863
|
let profile;
|
|
6480
6864
|
let safety;
|
|
6481
6865
|
try {
|
|
6482
|
-
cpSync3(proposedSkillMd,
|
|
6483
|
-
if (
|
|
6484
|
-
cpSync3(proposedClaudeMd,
|
|
6866
|
+
cpSync3(proposedSkillMd, join29(tempDir, "SKILL.md"));
|
|
6867
|
+
if (existsSync22(proposedClaudeMd)) {
|
|
6868
|
+
cpSync3(proposedClaudeMd, join29(tempDir, "CLAUDE.md"));
|
|
6485
6869
|
}
|
|
6486
6870
|
profile = asSkillProfile(profileSkill(tempDir), "for the evolved proposal");
|
|
6487
6871
|
const strictness = loadConfig().strictness;
|
|
@@ -6580,7 +6964,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
6580
6964
|
if (safetyVerdict === "block") process.exitCode = 1;
|
|
6581
6965
|
} else {
|
|
6582
6966
|
logOutcome(`[skill-forge] held \u2014 staging dir left in place -> ${stagingDir}`, json);
|
|
6583
|
-
if (
|
|
6967
|
+
if (existsSync22(reportMdPath)) {
|
|
6584
6968
|
logOutcome(`[skill-forge] see ${reportMdPath} for the full evidence report`, json);
|
|
6585
6969
|
}
|
|
6586
6970
|
}
|
|
@@ -6622,10 +7006,10 @@ function validateRunOutput(raw) {
|
|
|
6622
7006
|
return { accepted: Boolean(obj.accepted), staging_dir: stagingDirRaw ?? null };
|
|
6623
7007
|
}
|
|
6624
7008
|
function readManifest(stagingDir) {
|
|
6625
|
-
const manifestPath =
|
|
7009
|
+
const manifestPath = join29(stagingDir, "manifest.json");
|
|
6626
7010
|
let raw;
|
|
6627
7011
|
try {
|
|
6628
|
-
raw =
|
|
7012
|
+
raw = readFileSync21(manifestPath, "utf-8");
|
|
6629
7013
|
} catch (err) {
|
|
6630
7014
|
throw new UserError(`could not read staging manifest ${manifestPath}: ${err.message}`);
|
|
6631
7015
|
}
|
|
@@ -6659,18 +7043,18 @@ function validateManifest(raw, manifestPath) {
|
|
|
6659
7043
|
};
|
|
6660
7044
|
}
|
|
6661
7045
|
function filesIdentical(a, b) {
|
|
6662
|
-
if (!
|
|
6663
|
-
return
|
|
7046
|
+
if (!existsSync22(a) || !existsSync22(b)) return false;
|
|
7047
|
+
return readFileSync21(a).equals(readFileSync21(b));
|
|
6664
7048
|
}
|
|
6665
7049
|
function isAlreadyAdopted(stagingDir, manifest) {
|
|
6666
|
-
const proposedSkillMd =
|
|
6667
|
-
const proposedClaudeMd =
|
|
7050
|
+
const proposedSkillMd = join29(stagingDir, "proposed_SKILL.md");
|
|
7051
|
+
const proposedClaudeMd = join29(stagingDir, "proposed_CLAUDE.md");
|
|
6668
7052
|
let sawComparison = false;
|
|
6669
|
-
if (
|
|
7053
|
+
if (existsSync22(proposedSkillMd)) {
|
|
6670
7054
|
sawComparison = true;
|
|
6671
7055
|
if (!filesIdentical(proposedSkillMd, manifest.live_skill_path)) return false;
|
|
6672
7056
|
}
|
|
6673
|
-
if (
|
|
7057
|
+
if (existsSync22(proposedClaudeMd) && manifest.live_memory_path) {
|
|
6674
7058
|
sawComparison = true;
|
|
6675
7059
|
if (!filesIdentical(proposedClaudeMd, manifest.live_memory_path)) return false;
|
|
6676
7060
|
}
|
|
@@ -6688,8 +7072,8 @@ function assertNotSymlink(path, label2) {
|
|
|
6688
7072
|
}
|
|
6689
7073
|
}
|
|
6690
7074
|
function sha256File(path) {
|
|
6691
|
-
if (!
|
|
6692
|
-
return createHash3("sha256").update(
|
|
7075
|
+
if (!existsSync22(path)) return null;
|
|
7076
|
+
return createHash3("sha256").update(readFileSync21(path)).digest("hex");
|
|
6693
7077
|
}
|
|
6694
7078
|
|
|
6695
7079
|
// src/commands/evolve.ts
|
|
@@ -6698,9 +7082,9 @@ async function evolveCommand(targetSkillDir, options = {}) {
|
|
|
6698
7082
|
}
|
|
6699
7083
|
|
|
6700
7084
|
// src/commands/finding.ts
|
|
6701
|
-
import { existsSync as
|
|
7085
|
+
import { existsSync as existsSync23 } from "fs";
|
|
6702
7086
|
function requireSavedConfig() {
|
|
6703
|
-
if (!
|
|
7087
|
+
if (!existsSync23(getConfigPath())) {
|
|
6704
7088
|
throw new UserError(
|
|
6705
7089
|
"no skill-forge config found \u2014 run `skill-forge init` first (`finding accept` re-runs the audit engine to resolve a live finding, and audit never runs against an invented default)."
|
|
6706
7090
|
);
|
|
@@ -6786,7 +7170,7 @@ async function findingRevokeCommand(prefix, options = {}) {
|
|
|
6786
7170
|
async function findingListCommand(options = {}) {
|
|
6787
7171
|
const { records, notices } = readAcceptedFindings();
|
|
6788
7172
|
for (const notice of notices) console.error(`[skill-forge] ${notice}`);
|
|
6789
|
-
const rows = options.stale ? records.filter((r) => !
|
|
7173
|
+
const rows = options.stale ? records.filter((r) => !existsSync23(r.target)) : records;
|
|
6790
7174
|
if (options.json) {
|
|
6791
7175
|
console.log(JSON.stringify(rows, null, 2));
|
|
6792
7176
|
return;
|
|
@@ -6807,8 +7191,8 @@ async function findingListCommand(options = {}) {
|
|
|
6807
7191
|
import { resolve as resolvePath9 } from "path";
|
|
6808
7192
|
|
|
6809
7193
|
// src/organize.ts
|
|
6810
|
-
import { existsSync as
|
|
6811
|
-
import { basename as basename11, join as
|
|
7194
|
+
import { existsSync as existsSync24, readFileSync as readFileSync22, readdirSync as readdirSync10, realpathSync as realpathSync7, statSync as statSync10 } from "fs";
|
|
7195
|
+
import { basename as basename11, join as join30, resolve as resolvePath8 } from "path";
|
|
6812
7196
|
var PRO_FEATURE_LABEL4 = "set-level organizer reports";
|
|
6813
7197
|
var TIERS = /* @__PURE__ */ new Set(["resource", "custom"]);
|
|
6814
7198
|
var MATURITIES = /* @__PURE__ */ new Set(["seedling", "stable", "deprecated", "experimental"]);
|
|
@@ -6821,14 +7205,14 @@ function listSubdirs2(dir) {
|
|
|
6821
7205
|
}
|
|
6822
7206
|
const out = [];
|
|
6823
7207
|
for (const entry of entries) {
|
|
6824
|
-
const full =
|
|
7208
|
+
const full = join30(dir, entry.name);
|
|
6825
7209
|
if (entry.isDirectory()) {
|
|
6826
7210
|
out.push(full);
|
|
6827
7211
|
continue;
|
|
6828
7212
|
}
|
|
6829
7213
|
if (entry.isSymbolicLink()) {
|
|
6830
7214
|
try {
|
|
6831
|
-
if (
|
|
7215
|
+
if (statSync10(full).isDirectory()) out.push(full);
|
|
6832
7216
|
} catch {
|
|
6833
7217
|
}
|
|
6834
7218
|
}
|
|
@@ -6839,14 +7223,14 @@ function findSkillDirsUnderRoot2(root) {
|
|
|
6839
7223
|
const found = /* @__PURE__ */ new Set();
|
|
6840
7224
|
const topDirs = listSubdirs2(root);
|
|
6841
7225
|
for (const dir of topDirs) {
|
|
6842
|
-
if (
|
|
7226
|
+
if (existsSync24(join30(dir, "SKILL.md"))) found.add(dir);
|
|
6843
7227
|
}
|
|
6844
|
-
for (const dir of listSubdirs2(
|
|
6845
|
-
if (
|
|
7228
|
+
for (const dir of listSubdirs2(join30(root, "skills"))) {
|
|
7229
|
+
if (existsSync24(join30(dir, "SKILL.md"))) found.add(dir);
|
|
6846
7230
|
}
|
|
6847
7231
|
for (const topDir of topDirs) {
|
|
6848
|
-
for (const dir of listSubdirs2(
|
|
6849
|
-
if (
|
|
7232
|
+
for (const dir of listSubdirs2(join30(topDir, "skills"))) {
|
|
7233
|
+
if (existsSync24(join30(dir, "SKILL.md"))) found.add(dir);
|
|
6850
7234
|
}
|
|
6851
7235
|
}
|
|
6852
7236
|
return Array.from(found).filter((p) => !p.includes("/.git/"));
|
|
@@ -7042,17 +7426,17 @@ function buildRegistryEntries(candidates, warnings) {
|
|
|
7042
7426
|
for (const candidate of candidates) {
|
|
7043
7427
|
let realPath;
|
|
7044
7428
|
try {
|
|
7045
|
-
realPath =
|
|
7429
|
+
realPath = realpathSync7(candidate.dirPath);
|
|
7046
7430
|
} catch (err) {
|
|
7047
7431
|
warnings.push(`could not resolve skill directory ${candidate.dirPath}: ${err.message}`);
|
|
7048
7432
|
continue;
|
|
7049
7433
|
}
|
|
7050
7434
|
if (seenRealPaths.has(realPath)) continue;
|
|
7051
7435
|
seenRealPaths.add(realPath);
|
|
7052
|
-
const skillMdPath =
|
|
7436
|
+
const skillMdPath = join30(realPath, "SKILL.md");
|
|
7053
7437
|
let text;
|
|
7054
7438
|
try {
|
|
7055
|
-
text =
|
|
7439
|
+
text = readFileSync22(skillMdPath, "utf-8");
|
|
7056
7440
|
} catch (err) {
|
|
7057
7441
|
warnings.push(`could not read ${skillMdPath}: ${err.message}`);
|
|
7058
7442
|
continue;
|
|
@@ -7458,13 +7842,13 @@ async function find(query, options = {}, deps = {}) {
|
|
|
7458
7842
|
}
|
|
7459
7843
|
|
|
7460
7844
|
// src/commands/watch.ts
|
|
7461
|
-
import { realpathSync as
|
|
7462
|
-
import { join as
|
|
7845
|
+
import { realpathSync as realpathSync8 } from "fs";
|
|
7846
|
+
import { join as join31, resolve as resolvePath10 } from "path";
|
|
7463
7847
|
|
|
7464
7848
|
// src/gate/skillMapDrift.ts
|
|
7465
7849
|
import { createHash as createHash4 } from "crypto";
|
|
7466
|
-
import { readFileSync as
|
|
7467
|
-
import { dirname as dirname10, isAbsolute as
|
|
7850
|
+
import { readFileSync as readFileSync23, existsSync as existsSync25 } from "fs";
|
|
7851
|
+
import { dirname as dirname10, isAbsolute as isAbsolute6, resolve as resolve8 } from "path";
|
|
7468
7852
|
function sha256(text) {
|
|
7469
7853
|
return createHash4("sha256").update(text, "utf-8").digest("hex");
|
|
7470
7854
|
}
|
|
@@ -7497,22 +7881,22 @@ function threeWayVerdict(localNormalized, upstreamHash, baselineHash) {
|
|
|
7497
7881
|
}
|
|
7498
7882
|
function resolveLocalPath(path, mapDir) {
|
|
7499
7883
|
const expanded = expandHome(path);
|
|
7500
|
-
if (
|
|
7501
|
-
const candidates = [
|
|
7884
|
+
if (isAbsolute6(expanded)) return resolve8(expanded);
|
|
7885
|
+
const candidates = [resolve8(expanded)];
|
|
7502
7886
|
if (mapDir) {
|
|
7503
|
-
candidates.push(
|
|
7504
|
-
candidates.push(
|
|
7887
|
+
candidates.push(resolve8(mapDir, expanded));
|
|
7888
|
+
candidates.push(resolve8(mapDir, "..", expanded));
|
|
7505
7889
|
}
|
|
7506
7890
|
for (const candidate of candidates) {
|
|
7507
|
-
if (
|
|
7891
|
+
if (existsSync25(candidate)) return candidate;
|
|
7508
7892
|
}
|
|
7509
7893
|
return candidates[0];
|
|
7510
7894
|
}
|
|
7511
7895
|
function defaultReadLocalFile(path, mapDir) {
|
|
7512
7896
|
const resolved = resolveLocalPath(path, mapDir);
|
|
7513
|
-
if (!
|
|
7897
|
+
if (!existsSync25(resolved)) return void 0;
|
|
7514
7898
|
try {
|
|
7515
|
-
return
|
|
7899
|
+
return readFileSync23(resolved, "utf-8");
|
|
7516
7900
|
} catch {
|
|
7517
7901
|
return void 0;
|
|
7518
7902
|
}
|
|
@@ -7533,7 +7917,7 @@ function upstreamLabel(node, edge) {
|
|
|
7533
7917
|
return node?.url ?? node?.path ?? node?.name ?? node?.id ?? edge.to;
|
|
7534
7918
|
}
|
|
7535
7919
|
async function computeMapDrift(map, deps = {}) {
|
|
7536
|
-
const mapDir = deps.mapPath ? dirname10(
|
|
7920
|
+
const mapDir = deps.mapPath ? dirname10(resolve8(expandHome(deps.mapPath))) : void 0;
|
|
7537
7921
|
const readLocalFile = deps.readLocalFile ?? ((path) => defaultReadLocalFile(path, mapDir));
|
|
7538
7922
|
const fetchUpstream = deps.fetchUpstream ?? defaultFetchUpstream;
|
|
7539
7923
|
const rows = [];
|
|
@@ -7553,7 +7937,7 @@ async function computeMapDrift(map, deps = {}) {
|
|
|
7553
7937
|
continue;
|
|
7554
7938
|
}
|
|
7555
7939
|
const localResolved = resolveLocalPath(localNode.path, mapDir);
|
|
7556
|
-
if (!
|
|
7940
|
+
if (!existsSync25(localResolved)) {
|
|
7557
7941
|
rows.push({
|
|
7558
7942
|
skill: skillLabel,
|
|
7559
7943
|
upstream,
|
|
@@ -7664,7 +8048,7 @@ async function runWatch(config, options = {}, deps = {}) {
|
|
|
7664
8048
|
let realPath = resolvedRoot;
|
|
7665
8049
|
if (exists) {
|
|
7666
8050
|
try {
|
|
7667
|
-
realPath =
|
|
8051
|
+
realPath = realpathSync8(resolvedRoot);
|
|
7668
8052
|
} catch {
|
|
7669
8053
|
realPath = resolvedRoot;
|
|
7670
8054
|
}
|
|
@@ -7676,7 +8060,7 @@ async function runWatch(config, options = {}, deps = {}) {
|
|
|
7676
8060
|
ledger = readProvenanceLedger(resolvedRoot);
|
|
7677
8061
|
} catch (err) {
|
|
7678
8062
|
const message = err instanceof Error ? err.message : String(err);
|
|
7679
|
-
const ledgerPath =
|
|
8063
|
+
const ledgerPath = join31(resolvedRoot, "SOURCES.md");
|
|
7680
8064
|
roots.push({ root, realPath, exists, ledgerPath, ledgerExists: false });
|
|
7681
8065
|
warnings.push(`${ledgerPath}: could not read ledger: ${message}`);
|
|
7682
8066
|
continue;
|
|
@@ -7786,8 +8170,8 @@ async function watchCommand(options = {}) {
|
|
|
7786
8170
|
}
|
|
7787
8171
|
|
|
7788
8172
|
// src/commands/routine.ts
|
|
7789
|
-
import { existsSync as
|
|
7790
|
-
import { join as
|
|
8173
|
+
import { existsSync as existsSync26, rmSync as rmSync7, statSync as statSync11 } from "fs";
|
|
8174
|
+
import { join as join32 } from "path";
|
|
7791
8175
|
var PRO_FEATURE_LABEL6 = "the scheduled maintenance routine";
|
|
7792
8176
|
var SEVERITY_ORDER = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
7793
8177
|
function parseFailOnFloor(failOn) {
|
|
@@ -7795,20 +8179,20 @@ function parseFailOnFloor(failOn) {
|
|
|
7795
8179
|
if (failOn === "any") return "LOW";
|
|
7796
8180
|
return failOn.toUpperCase();
|
|
7797
8181
|
}
|
|
7798
|
-
function exceedsFloor(
|
|
8182
|
+
function exceedsFloor(findings, failOn) {
|
|
7799
8183
|
const floor = parseFailOnFloor(failOn);
|
|
7800
8184
|
if (!floor) return void 0;
|
|
7801
8185
|
const floorIndex = SEVERITY_ORDER.indexOf(floor);
|
|
7802
8186
|
for (let i = SEVERITY_ORDER.length - 1; i >= floorIndex; i--) {
|
|
7803
8187
|
const severity = SEVERITY_ORDER[i];
|
|
7804
|
-
const count =
|
|
8188
|
+
const count = findings[severity] ?? 0;
|
|
7805
8189
|
if (count > 0) return `${count} ${severity} finding(s) at or above --fail-on ${failOn}`;
|
|
7806
8190
|
}
|
|
7807
8191
|
return void 0;
|
|
7808
8192
|
}
|
|
7809
8193
|
function resolveReportPath2(override, generatedAt) {
|
|
7810
8194
|
if (override) return override;
|
|
7811
|
-
return
|
|
8195
|
+
return join32(getHomeDir(), "reports", `routine-${generatedAt.replace(/[:.]/g, "-")}.md`);
|
|
7812
8196
|
}
|
|
7813
8197
|
function runHousekeeping(config, options) {
|
|
7814
8198
|
const actions = [];
|
|
@@ -7817,10 +8201,10 @@ function runHousekeeping(config, options) {
|
|
|
7817
8201
|
const pruneDays = options.pruneDays ?? 30;
|
|
7818
8202
|
const cutoff = Date.now() - pruneDays * 864e5;
|
|
7819
8203
|
for (const id of listQuarantineIds(config.quarantineDir)) {
|
|
7820
|
-
const path =
|
|
8204
|
+
const path = join32(config.quarantineDir, id);
|
|
7821
8205
|
let mtime;
|
|
7822
8206
|
try {
|
|
7823
|
-
mtime =
|
|
8207
|
+
mtime = statSync11(path).mtimeMs;
|
|
7824
8208
|
} catch {
|
|
7825
8209
|
continue;
|
|
7826
8210
|
}
|
|
@@ -7855,7 +8239,7 @@ function runHousekeeping(config, options) {
|
|
|
7855
8239
|
}
|
|
7856
8240
|
for (const entry of entries) {
|
|
7857
8241
|
if (entry.status !== "pending") continue;
|
|
7858
|
-
if (!entry.installedPath ||
|
|
8242
|
+
if (!entry.installedPath || existsSync26(entry.installedPath)) continue;
|
|
7859
8243
|
if (apply) {
|
|
7860
8244
|
try {
|
|
7861
8245
|
updateQueueEntry(entry.id, { status: "dismissed" });
|
|
@@ -7888,6 +8272,7 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
|
|
|
7888
8272
|
"",
|
|
7889
8273
|
`- Skills: ${result.audit.skillCount} across ${audit2.summary.rootCount} configured root(s)`,
|
|
7890
8274
|
`- Findings: ${audit2.summary.findingCounts.CRITICAL ?? 0} critical, ${audit2.summary.findingCounts.HIGH ?? 0} high, ${audit2.summary.findingCounts.MEDIUM ?? 0} medium, ${audit2.summary.findingCounts.LOW ?? 0} low`,
|
|
8275
|
+
`- Failure scope: ${result.failScope} (configured excludes plugin-origin findings; all includes them)`,
|
|
7891
8276
|
`- Accepted: ${audit2.summary.acceptedCount} (suppressed from the counts above; see Accepted findings)`,
|
|
7892
8277
|
`- Overlap clusters: ${result.audit.overlapClusters.length}${result.audit.overlapLocked ? ' (overlap analysis was locked \u2014 not "none found")' : ""}`,
|
|
7893
8278
|
`- Pending queue entries: ${result.pending.queue}`,
|
|
@@ -7957,15 +8342,23 @@ async function routineCommand(options = {}, deps = {}) {
|
|
|
7957
8342
|
if (options.dryRun && !options.housekeeping) {
|
|
7958
8343
|
throw new UserError("--dry-run only applies to --housekeeping; without it `routine` already writes nothing but its own report");
|
|
7959
8344
|
}
|
|
8345
|
+
const failScope = options.failScope ?? "configured";
|
|
8346
|
+
if (failScope !== "configured" && failScope !== "all") throw new UserError("--fail-scope must be configured or all");
|
|
7960
8347
|
const config = loadConfig();
|
|
7961
8348
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7962
|
-
const { pluginRoots, usage, notices: pluginUsageNotices } = resolvePluginAndUsageContext(config, options, deps);
|
|
8349
|
+
const { pluginRoots, usage, importedInventory, notices: pluginUsageNotices } = resolvePluginAndUsageContext(config, options, deps);
|
|
7963
8350
|
const { records: accepted, notices: storeNotices } = readAcceptedFindings();
|
|
7964
8351
|
const notices = [...storeNotices, ...pluginUsageNotices];
|
|
7965
8352
|
for (const notice of notices) log(`[skill-forge] ${notice}`);
|
|
7966
8353
|
const audit2 = await auditConfiguredSet(config, { accepted, pluginRoots, usage });
|
|
8354
|
+
if (importedInventory) {
|
|
8355
|
+
audit2.hostInventory = { host: importedInventory.host, complete: importedInventory.complete, generatedAt: importedInventory.generatedAt };
|
|
8356
|
+
for (const plugin of audit2.plugins ?? []) plugin.host = importedInventory.host;
|
|
8357
|
+
}
|
|
8358
|
+
applyPluginDecisions(audit2, deps.cwd);
|
|
8359
|
+
notices.push(...audit2.notices);
|
|
7967
8360
|
const auditState = summarizeAudit(audit2);
|
|
7968
|
-
if (audit2.plugins) {
|
|
8361
|
+
if (audit2.plugins && failScope === "configured") {
|
|
7969
8362
|
const critical = audit2.plugins.reduce((n, p) => n + p.findingCounts.CRITICAL, 0);
|
|
7970
8363
|
const high = audit2.plugins.reduce((n, p) => n + p.findingCounts.HIGH, 0);
|
|
7971
8364
|
if (critical + high > 0) {
|
|
@@ -8001,7 +8394,11 @@ async function routineCommand(options = {}, deps = {}) {
|
|
|
8001
8394
|
}
|
|
8002
8395
|
const quarantine2 = heldCount(config.quarantineDir);
|
|
8003
8396
|
if (!quarantine2.readable) degraded.push("quarantine dir could not be listed \u2014 held count is not trustworthy");
|
|
8397
|
+
const failureCounts = failScope === "all" ? audit2.summary.findingCounts : auditState.findings;
|
|
8004
8398
|
const result = {
|
|
8399
|
+
schemaVersion: 1,
|
|
8400
|
+
failScope,
|
|
8401
|
+
failureCounts,
|
|
8005
8402
|
generatedAt,
|
|
8006
8403
|
reportPath: resolveReportPath2(options.report, generatedAt),
|
|
8007
8404
|
audit: auditState,
|
|
@@ -8019,7 +8416,7 @@ async function routineCommand(options = {}, deps = {}) {
|
|
|
8019
8416
|
notices,
|
|
8020
8417
|
degraded
|
|
8021
8418
|
};
|
|
8022
|
-
const failure = degraded.length > 0 ? `${degraded.length} step(s) did not complete: ${degraded.join("; ")}` : exceedsFloor(
|
|
8419
|
+
const failure = degraded.length > 0 ? `${degraded.length} step(s) did not complete: ${degraded.join("; ")}` : exceedsFloor(failureCounts, options.failOn ?? "none");
|
|
8023
8420
|
if (failure) result.failure = failure;
|
|
8024
8421
|
writeNewFile(result.reportPath, renderRoutineMarkdown(result, audit2, watch, organize), "--report");
|
|
8025
8422
|
try {
|
|
@@ -8031,6 +8428,7 @@ async function routineCommand(options = {}, deps = {}) {
|
|
|
8031
8428
|
console.log(JSON.stringify(result, null, 2));
|
|
8032
8429
|
} else {
|
|
8033
8430
|
console.log("[skill-forge] routine");
|
|
8431
|
+
console.log(` failure scope: ${failScope}`);
|
|
8034
8432
|
console.log(` skills: ${auditState.skillCount}`);
|
|
8035
8433
|
console.log(
|
|
8036
8434
|
` findings: ${auditState.findings.CRITICAL ?? 0} critical / ${auditState.findings.HIGH ?? 0} high / ${auditState.findings.MEDIUM ?? 0} medium / ${auditState.findings.LOW ?? 0} low`
|
|
@@ -8060,8 +8458,8 @@ async function routineCommand(options = {}, deps = {}) {
|
|
|
8060
8458
|
}
|
|
8061
8459
|
|
|
8062
8460
|
// src/commands/ingest.ts
|
|
8063
|
-
import { realpathSync as
|
|
8064
|
-
import { dirname as dirname12, isAbsolute as
|
|
8461
|
+
import { realpathSync as realpathSync9 } from "fs";
|
|
8462
|
+
import { dirname as dirname12, isAbsolute as isAbsolute7, relative as relative6, resolve as resolvePath11 } from "path";
|
|
8065
8463
|
var PRO_FEATURE_LABEL7 = "the pending-ingestion queue";
|
|
8066
8464
|
var VALID_STATUSES = ["ingested", "dismissed"];
|
|
8067
8465
|
function requireProFeature() {
|
|
@@ -8102,14 +8500,14 @@ function printPendingList(pending, json) {
|
|
|
8102
8500
|
}
|
|
8103
8501
|
function realpathOrNull(p) {
|
|
8104
8502
|
try {
|
|
8105
|
-
return
|
|
8503
|
+
return realpathSync9(p);
|
|
8106
8504
|
} catch {
|
|
8107
8505
|
return null;
|
|
8108
8506
|
}
|
|
8109
8507
|
}
|
|
8110
8508
|
function isUnderRoot(candidate, root) {
|
|
8111
|
-
const rel =
|
|
8112
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
8509
|
+
const rel = relative6(root, candidate);
|
|
8510
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute7(rel);
|
|
8113
8511
|
}
|
|
8114
8512
|
function validatePendingEntries(entries, config) {
|
|
8115
8513
|
const quarantineRoot = realpathOrNull(resolvePath11(config.quarantineDir));
|
|
@@ -8191,15 +8589,15 @@ import {
|
|
|
8191
8589
|
chmodSync as chmodSync3,
|
|
8192
8590
|
lstatSync as lstatSync6,
|
|
8193
8591
|
mkdirSync as mkdirSync9,
|
|
8194
|
-
readFileSync as
|
|
8592
|
+
readFileSync as readFileSync27,
|
|
8195
8593
|
readSync as readSync3,
|
|
8196
8594
|
writeFileSync as writeFileSync8
|
|
8197
8595
|
} from "fs";
|
|
8198
|
-
import { dirname as dirname13, isAbsolute as
|
|
8596
|
+
import { dirname as dirname13, isAbsolute as isAbsolute9, join as join37, relative as relative8, resolve as resolvePath12 } from "path";
|
|
8199
8597
|
|
|
8200
8598
|
// src/insights/inventory.ts
|
|
8201
|
-
import { readFileSync as
|
|
8202
|
-
import { join as
|
|
8599
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
8600
|
+
import { join as join33 } from "path";
|
|
8203
8601
|
|
|
8204
8602
|
// src/insights/models.ts
|
|
8205
8603
|
import { createHash as createHash5 } from "crypto";
|
|
@@ -8313,7 +8711,7 @@ function declaredMapCoverage(path, claim) {
|
|
|
8313
8711
|
}
|
|
8314
8712
|
function fingerprintInstalledSkill(path) {
|
|
8315
8713
|
try {
|
|
8316
|
-
return { fingerprint: stableHash(
|
|
8714
|
+
return { fingerprint: stableHash(readFileSync24(join33(path, "SKILL.md"), "utf-8")) };
|
|
8317
8715
|
} catch (error) {
|
|
8318
8716
|
return { reason: `could not fingerprint SKILL.md: ${error.message}` };
|
|
8319
8717
|
}
|
|
@@ -8463,12 +8861,12 @@ function buildInsightInventory(config, options = {}) {
|
|
|
8463
8861
|
}
|
|
8464
8862
|
|
|
8465
8863
|
// src/insights/jira.ts
|
|
8466
|
-
import { existsSync as
|
|
8467
|
-
import { join as
|
|
8864
|
+
import { existsSync as existsSync28, readFileSync as readFileSync26 } from "fs";
|
|
8865
|
+
import { join as join36 } from "path";
|
|
8468
8866
|
|
|
8469
8867
|
// src/insights/store.ts
|
|
8470
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
8471
|
-
import { join as
|
|
8868
|
+
import { chmodSync as chmodSync2, existsSync as existsSync27, mkdirSync as mkdirSync8, readFileSync as readFileSync25 } from "fs";
|
|
8869
|
+
import { join as join35 } from "path";
|
|
8472
8870
|
|
|
8473
8871
|
// src/insights/sources.ts
|
|
8474
8872
|
import { createHash as createHash6 } from "crypto";
|
|
@@ -8480,10 +8878,10 @@ import {
|
|
|
8480
8878
|
openSync as openSync3,
|
|
8481
8879
|
readSync as readSync2,
|
|
8482
8880
|
readdirSync as readdirSync11,
|
|
8483
|
-
realpathSync as
|
|
8484
|
-
statSync as
|
|
8881
|
+
realpathSync as realpathSync10,
|
|
8882
|
+
statSync as statSync12
|
|
8485
8883
|
} from "fs";
|
|
8486
|
-
import { basename as basename12, extname as extname8, isAbsolute as
|
|
8884
|
+
import { basename as basename12, extname as extname8, isAbsolute as isAbsolute8, join as join34, relative as relative7, resolve as resolve9, sep as sep9 } from "path";
|
|
8487
8885
|
var DEFAULT_MAX_SOURCE_FILE_BYTES = 5 * 1024 * 1024;
|
|
8488
8886
|
var DEFAULT_MAX_SOURCE_TOTAL_BYTES = 25 * 1024 * 1024;
|
|
8489
8887
|
var SKIP_DIRS4 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "coverage", ".next", "out", "__pycache__", ".venv", "venv"]);
|
|
@@ -8554,8 +8952,8 @@ function canonicalPublicUrl(raw, label2) {
|
|
|
8554
8952
|
return url.toString();
|
|
8555
8953
|
}
|
|
8556
8954
|
function isInside3(child, root) {
|
|
8557
|
-
const rel =
|
|
8558
|
-
return rel === "" || !rel.startsWith(`..${
|
|
8955
|
+
const rel = relative7(root, child);
|
|
8956
|
+
return rel === "" || !rel.startsWith(`..${sep9}`) && rel !== ".." && !isAbsolute8(rel);
|
|
8559
8957
|
}
|
|
8560
8958
|
function isHiddenName(name) {
|
|
8561
8959
|
return name.startsWith(".") && name !== "." && name !== "..";
|
|
@@ -8619,8 +9017,8 @@ function observeFile(path, relativePath, declaredMediaType, maxFileBytes, remain
|
|
|
8619
9017
|
if (!bytes || !descriptorStat) throw new UserError(`source file could not be observed: ${relativePath}`);
|
|
8620
9018
|
let realPath;
|
|
8621
9019
|
try {
|
|
8622
|
-
realPath =
|
|
8623
|
-
const pathStat =
|
|
9020
|
+
realPath = realpathSync10(path);
|
|
9021
|
+
const pathStat = statSync12(realPath);
|
|
8624
9022
|
if (pathStat.dev !== descriptorStat.dev || pathStat.ino !== descriptorStat.ino) {
|
|
8625
9023
|
throw new Error("path identity changed");
|
|
8626
9024
|
}
|
|
@@ -8741,7 +9139,7 @@ function normalizeStdin(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8741
9139
|
};
|
|
8742
9140
|
}
|
|
8743
9141
|
function normalizeFile(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
8744
|
-
const resolved =
|
|
9142
|
+
const resolved = resolve9(input.path);
|
|
8745
9143
|
let lexicalStat;
|
|
8746
9144
|
try {
|
|
8747
9145
|
lexicalStat = lstatSync5(resolved);
|
|
@@ -8792,7 +9190,7 @@ function normalizeFile(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8792
9190
|
};
|
|
8793
9191
|
}
|
|
8794
9192
|
function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
8795
|
-
const resolved =
|
|
9193
|
+
const resolved = resolve9(input.path);
|
|
8796
9194
|
let lexicalStat;
|
|
8797
9195
|
try {
|
|
8798
9196
|
lexicalStat = lstatSync5(resolved);
|
|
@@ -8801,7 +9199,7 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8801
9199
|
}
|
|
8802
9200
|
if (lexicalStat.isSymbolicLink()) throw new UserError(`source directory must not be a symlink: ${resolved}`);
|
|
8803
9201
|
if (!lexicalStat.isDirectory()) throw new UserError(`source directory is not a directory: ${resolved}`);
|
|
8804
|
-
const realRoot =
|
|
9202
|
+
const realRoot = realpathSync10(resolved);
|
|
8805
9203
|
const observations = [];
|
|
8806
9204
|
const skipped = [];
|
|
8807
9205
|
const seenFiles = /* @__PURE__ */ new Set();
|
|
@@ -8815,7 +9213,7 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8815
9213
|
throw new UserError(`unable to enumerate source directory ${dir}: ${err.message}`);
|
|
8816
9214
|
}
|
|
8817
9215
|
for (const entry of entries) {
|
|
8818
|
-
const full =
|
|
9216
|
+
const full = join34(dir, entry.name);
|
|
8819
9217
|
const logicalPath = logicalDir ? `${logicalDir}/${entry.name}` : entry.name;
|
|
8820
9218
|
if (entry.isDirectory() && SKIP_DIRS4.has(entry.name)) {
|
|
8821
9219
|
skipped.push({ path: logicalPath, reason: "ignored-directory" });
|
|
@@ -8832,14 +9230,14 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8832
9230
|
if (entry.isSymbolicLink()) {
|
|
8833
9231
|
let realTarget;
|
|
8834
9232
|
try {
|
|
8835
|
-
realTarget =
|
|
9233
|
+
realTarget = realpathSync10(full);
|
|
8836
9234
|
} catch {
|
|
8837
9235
|
throw new UserError(`source directory contains an unreadable symlink: ${logicalPath}`);
|
|
8838
9236
|
}
|
|
8839
9237
|
if (!isInside3(realTarget, realRoot)) {
|
|
8840
9238
|
throw new UserError(`source directory symlink escapes its root: ${logicalPath}`);
|
|
8841
9239
|
}
|
|
8842
|
-
const targetStat =
|
|
9240
|
+
const targetStat = statSync12(realTarget);
|
|
8843
9241
|
if (targetStat.isDirectory()) {
|
|
8844
9242
|
if (seenDirs.has(realTarget)) continue;
|
|
8845
9243
|
seenDirs.add(realTarget);
|
|
@@ -8851,7 +9249,7 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8851
9249
|
continue;
|
|
8852
9250
|
}
|
|
8853
9251
|
} else if (entry.isDirectory()) {
|
|
8854
|
-
const realDir =
|
|
9252
|
+
const realDir = realpathSync10(full);
|
|
8855
9253
|
if (seenDirs.has(realDir)) continue;
|
|
8856
9254
|
seenDirs.add(realDir);
|
|
8857
9255
|
walk2(full, logicalPath);
|
|
@@ -8860,7 +9258,7 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8860
9258
|
skipped.push({ path: logicalPath, reason: "unsupported-media-type" });
|
|
8861
9259
|
continue;
|
|
8862
9260
|
}
|
|
8863
|
-
const realFile =
|
|
9261
|
+
const realFile = realpathSync10(full);
|
|
8864
9262
|
if (!isInside3(realFile, realRoot)) {
|
|
8865
9263
|
throw new UserError(`source directory entry escaped its root while being observed: ${logicalPath}`);
|
|
8866
9264
|
}
|
|
@@ -8967,17 +9365,17 @@ function normalizeSources(inputs, options = {}) {
|
|
|
8967
9365
|
// src/insights/store.ts
|
|
8968
9366
|
var STUDY_ID = /^STUDY-[0-9a-f]{16}$/;
|
|
8969
9367
|
function getInsightsDir() {
|
|
8970
|
-
return
|
|
9368
|
+
return join35(getHomeDir(), "insights");
|
|
8971
9369
|
}
|
|
8972
9370
|
function getStudyDir(studyId) {
|
|
8973
9371
|
assertStudyId(studyId);
|
|
8974
|
-
return
|
|
9372
|
+
return join35(getInsightsDir(), studyId);
|
|
8975
9373
|
}
|
|
8976
9374
|
function getStudyPath(studyId) {
|
|
8977
|
-
return
|
|
9375
|
+
return join35(getStudyDir(studyId), "study.json");
|
|
8978
9376
|
}
|
|
8979
9377
|
function getStudyEventsPath(studyId) {
|
|
8980
|
-
return
|
|
9378
|
+
return join35(getStudyDir(studyId), "events.jsonl");
|
|
8981
9379
|
}
|
|
8982
9380
|
function createStudyId(sourceInventoryHash, createdAt) {
|
|
8983
9381
|
return stableId("STUDY", { sourceInventoryHash, createdAt });
|
|
@@ -9108,7 +9506,7 @@ function validateStudyShape(value, path) {
|
|
|
9108
9506
|
function parseStudy(path) {
|
|
9109
9507
|
let raw;
|
|
9110
9508
|
try {
|
|
9111
|
-
raw =
|
|
9509
|
+
raw = readFileSync25(path, "utf-8");
|
|
9112
9510
|
} catch (err) {
|
|
9113
9511
|
throw new UserError(`unable to read insight study at ${path}: ${err.message}`);
|
|
9114
9512
|
}
|
|
@@ -9142,7 +9540,7 @@ function createInsightStudy(input, options = {}) {
|
|
|
9142
9540
|
const studyId = options.studyId ?? createStudyId(input.sourceInventoryHash, createdAt);
|
|
9143
9541
|
assertStudyId(studyId);
|
|
9144
9542
|
const path = getStudyPath(studyId);
|
|
9145
|
-
if (
|
|
9543
|
+
if (existsSync27(path)) throw new UserError(`insight study already exists: ${studyId}`);
|
|
9146
9544
|
const study = {
|
|
9147
9545
|
schemaVersion: INSIGHT_SCHEMA_VERSION,
|
|
9148
9546
|
studyId,
|
|
@@ -9163,7 +9561,7 @@ function createInsightStudy(input, options = {}) {
|
|
|
9163
9561
|
}
|
|
9164
9562
|
function readInsightStudy(studyId) {
|
|
9165
9563
|
const path = getStudyPath(studyId);
|
|
9166
|
-
if (!
|
|
9564
|
+
if (!existsSync27(path)) throw new UserError(`no insight study with id: ${studyId}`);
|
|
9167
9565
|
const study = parseStudy(path);
|
|
9168
9566
|
if (study.studyId !== studyId) {
|
|
9169
9567
|
throw new UserError(`malformed insight study at ${path}: stored studyId does not match its directory`);
|
|
@@ -9173,7 +9571,7 @@ function readInsightStudy(studyId) {
|
|
|
9173
9571
|
function writeInsightStudy(study, now = /* @__PURE__ */ new Date()) {
|
|
9174
9572
|
assertStudyId(study.studyId);
|
|
9175
9573
|
const path = getStudyPath(study.studyId);
|
|
9176
|
-
if (!
|
|
9574
|
+
if (!existsSync27(path)) throw new UserError(`no insight study with id: ${study.studyId}`);
|
|
9177
9575
|
const existing = parseStudy(path);
|
|
9178
9576
|
let next = { ...study, updatedAt: now.toISOString() };
|
|
9179
9577
|
if (next.status === "dismissed" || next.status === "archived") {
|
|
@@ -9910,7 +10308,7 @@ function buildJiraManifest(study, projectKey, options = {}) {
|
|
|
9910
10308
|
return manifest;
|
|
9911
10309
|
}
|
|
9912
10310
|
function getJiraReceiptsPath(studyId) {
|
|
9913
|
-
return
|
|
10311
|
+
return join36(getStudyDir(studyId), "jira-receipts.jsonl");
|
|
9914
10312
|
}
|
|
9915
10313
|
function assertCurrentJiraManifest(study, value) {
|
|
9916
10314
|
if (study.status !== "jira-manifested" || !study.jira) {
|
|
@@ -10042,10 +10440,10 @@ function appendJiraReceipt(studyId, manifest, input) {
|
|
|
10042
10440
|
function readJiraReceipts(studyId, manifest) {
|
|
10043
10441
|
const currentManifest = assertCurrentJiraManifest(readInsightStudy(studyId), manifest);
|
|
10044
10442
|
const path = getJiraReceiptsPath(studyId);
|
|
10045
|
-
if (!
|
|
10443
|
+
if (!existsSync28(path)) return [];
|
|
10046
10444
|
let text;
|
|
10047
10445
|
try {
|
|
10048
|
-
text =
|
|
10446
|
+
text = readFileSync26(path, "utf-8");
|
|
10049
10447
|
} catch (error) {
|
|
10050
10448
|
throw new UserError(`unable to read Jira receipts at ${path}: ${error.message}`);
|
|
10051
10449
|
}
|
|
@@ -10187,7 +10585,7 @@ function parseJsonFile(path, label2) {
|
|
|
10187
10585
|
const resolved = resolvePath12(path);
|
|
10188
10586
|
let raw;
|
|
10189
10587
|
try {
|
|
10190
|
-
raw =
|
|
10588
|
+
raw = readFileSync27(resolved, "utf-8");
|
|
10191
10589
|
} catch (error) {
|
|
10192
10590
|
throw new UserError(`unable to read ${label2} at ${resolved}: ${error.message}`);
|
|
10193
10591
|
}
|
|
@@ -10242,7 +10640,7 @@ function assertCommonDescriptorFields(value, label2) {
|
|
|
10242
10640
|
}
|
|
10243
10641
|
function pathRelativeTo(baseDir, raw, label2) {
|
|
10244
10642
|
const path = nonEmptyString(raw, label2);
|
|
10245
|
-
return
|
|
10643
|
+
return isAbsolute9(path) ? path : resolvePath12(baseDir, path);
|
|
10246
10644
|
}
|
|
10247
10645
|
function parseManifestDescriptor(value, index, baseDir) {
|
|
10248
10646
|
const label2 = `source manifest entry ${index + 1}`;
|
|
@@ -10332,10 +10730,10 @@ function collectSourceInputs(sourceArgs, options) {
|
|
|
10332
10730
|
return inputs;
|
|
10333
10731
|
}
|
|
10334
10732
|
function contextPath(studyId) {
|
|
10335
|
-
return
|
|
10733
|
+
return join37(getStudyDir(studyId), CONTEXT_FILENAME);
|
|
10336
10734
|
}
|
|
10337
10735
|
function inventoryPath(studyId) {
|
|
10338
|
-
return
|
|
10736
|
+
return join37(getStudyDir(studyId), INVENTORY_FILENAME);
|
|
10339
10737
|
}
|
|
10340
10738
|
function writeCommandContext(studyId, skillMapPath, skillMapCoverage, inventory) {
|
|
10341
10739
|
const context = {
|
|
@@ -10349,7 +10747,7 @@ function writeCommandContext(studyId, skillMapPath, skillMapCoverage, inventory)
|
|
|
10349
10747
|
function readCommandContext(studyId) {
|
|
10350
10748
|
const path = contextPath(studyId);
|
|
10351
10749
|
const parsed = parseJsonFile(path, "insight command context");
|
|
10352
|
-
if (!isRecord5(parsed) || parsed.schemaVersion !== 1 || parsed.skillMapPath !== void 0 && (typeof parsed.skillMapPath !== "string" || !
|
|
10750
|
+
if (!isRecord5(parsed) || parsed.schemaVersion !== 1 || parsed.skillMapPath !== void 0 && (typeof parsed.skillMapPath !== "string" || !isAbsolute9(parsed.skillMapPath)) || parsed.skillMapCoverage !== void 0 && parsed.skillMapCoverage !== "static" && parsed.skillMapCoverage !== "resolved" || parsed.skillMapCoverage !== void 0 && parsed.skillMapPath === void 0 || Object.keys(parsed).some((key) => !["schemaVersion", "skillMapPath", "skillMapCoverage"].includes(key))) {
|
|
10353
10751
|
throw new UserError(`malformed insight command context at ${path}`);
|
|
10354
10752
|
}
|
|
10355
10753
|
return parsed;
|
|
@@ -10373,18 +10771,18 @@ function readCurrentStudy(studyId) {
|
|
|
10373
10771
|
}
|
|
10374
10772
|
function retainedPath(root, relativePath) {
|
|
10375
10773
|
const candidate = resolvePath12(root, relativePath);
|
|
10376
|
-
const rel =
|
|
10377
|
-
if (rel.startsWith("..") ||
|
|
10774
|
+
const rel = relative8(root, candidate);
|
|
10775
|
+
if (rel.startsWith("..") || isAbsolute9(rel)) throw new UserError(`refusing retained source path outside the study`);
|
|
10378
10776
|
return candidate;
|
|
10379
10777
|
}
|
|
10380
10778
|
function retainObservedSources(study, observedFiles) {
|
|
10381
10779
|
if (observedFiles.length === 0) return study;
|
|
10382
|
-
const root =
|
|
10780
|
+
const root = join37(getStudyDir(study.studyId), "retained-sources");
|
|
10383
10781
|
mkdirSync9(root, { recursive: true, mode: 448 });
|
|
10384
10782
|
chmodSync3(root, 448);
|
|
10385
10783
|
const retainedSourceIds = /* @__PURE__ */ new Set();
|
|
10386
10784
|
for (const file of observedFiles) {
|
|
10387
|
-
const sourceRoot =
|
|
10785
|
+
const sourceRoot = join37(root, file.sourceId);
|
|
10388
10786
|
mkdirSync9(sourceRoot, { recursive: true, mode: 448 });
|
|
10389
10787
|
chmodSync3(sourceRoot, 448);
|
|
10390
10788
|
const destination = retainedPath(sourceRoot, file.relativePath);
|
|
@@ -10622,7 +11020,7 @@ async function insightJira(studyId, options) {
|
|
|
10622
11020
|
throw new UserError(`study ${studyId} cannot generate a Jira manifest: ${validation.blockers.map((issue) => issue.message).join("; ")}`);
|
|
10623
11021
|
}
|
|
10624
11022
|
if (localMode) {
|
|
10625
|
-
const privatePath2 =
|
|
11023
|
+
const privatePath2 = join37(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
|
|
10626
11024
|
const manifest2 = assertCurrentJiraManifest(study, parseJsonFile(privatePath2, "stored Jira manifest"));
|
|
10627
11025
|
const receipt = options.receipt === void 0 ? void 0 : appendJiraReceipt(studyId, manifest2, parseJiraReceiptInput(parseJsonFile(options.receipt, "Jira receipt import")));
|
|
10628
11026
|
const resume = buildJiraResumeState(manifest2, readJiraReceipts(studyId, manifest2));
|
|
@@ -10634,7 +11032,7 @@ async function insightJira(studyId, options) {
|
|
|
10634
11032
|
return;
|
|
10635
11033
|
}
|
|
10636
11034
|
const manifest = buildJiraManifest(study, projectKey);
|
|
10637
|
-
const privatePath =
|
|
11035
|
+
const privatePath = join37(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
|
|
10638
11036
|
writeJsonAtomic(privatePath, manifest);
|
|
10639
11037
|
if (options.out) {
|
|
10640
11038
|
writeNewFile(resolvePath12(options.out), `${JSON.stringify(manifest, null, 2)}
|
|
@@ -10705,14 +11103,11 @@ async function insightClose(studyId, options) {
|
|
|
10705
11103
|
}
|
|
10706
11104
|
|
|
10707
11105
|
// src/commands/refine.ts
|
|
10708
|
-
import {
|
|
10709
|
-
import { chmodSync as chmodSync4, existsSync as existsSync30, lstatSync as lstatSync7, mkdirSync as mkdirSync11, readFileSync as readFileSync26, realpathSync as realpathSync11, renameSync as renameSync6, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "fs";
|
|
10710
|
-
import { dirname as dirname15, isAbsolute as isAbsolute9, join as join38, relative as relative8, resolve as resolvePath15, sep as sep8 } from "path";
|
|
10711
|
-
import { createInterface as createInterface7 } from "readline/promises";
|
|
11106
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
10712
11107
|
|
|
10713
|
-
// src/refine/
|
|
10714
|
-
import {
|
|
10715
|
-
import {
|
|
11108
|
+
// src/refine/activation.ts
|
|
11109
|
+
import { lstatSync as lstatSync7, readFileSync as readFileSync28, readdirSync as readdirSync12, realpathSync as realpathSync11 } from "fs";
|
|
11110
|
+
import { join as join38 } from "path";
|
|
10716
11111
|
|
|
10717
11112
|
// src/refine/models.ts
|
|
10718
11113
|
import { randomBytes } from "crypto";
|
|
@@ -10780,52 +11175,413 @@ function contentSimilarity(a, b) {
|
|
|
10780
11175
|
return unionSize3 === 0 ? 0 : intersectionSize / unionSize3;
|
|
10781
11176
|
}
|
|
10782
11177
|
|
|
10783
|
-
// src/refine/
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
var
|
|
10788
|
-
var
|
|
10789
|
-
function
|
|
10790
|
-
|
|
11178
|
+
// src/refine/patch.ts
|
|
11179
|
+
var FENCE_RE = /^\s*(```|~~~)/;
|
|
11180
|
+
var HEADING_RE = /^(#{1,6})\s+(.*)$/;
|
|
11181
|
+
var ACTION_RE = /^<!--\s*ACTION:\s*([a-zA-Z-]+)(?:\s+"([^"]*)")?\s*-->\s*$/;
|
|
11182
|
+
var TARGET_RE = /^<!--\s*TARGET:\s*(.+?)\s*-->\s*$/;
|
|
11183
|
+
var ACTIONS_REQUIRING_MARKER = /* @__PURE__ */ new Set(["replace-section", "insert-after", "insert-before", "delete-section"]);
|
|
11184
|
+
function parsePatchFile(source) {
|
|
11185
|
+
const lines = source.split("\n");
|
|
11186
|
+
const blocks = [];
|
|
11187
|
+
let inFence = false;
|
|
11188
|
+
let current = null;
|
|
11189
|
+
const finalizeCurrent = () => {
|
|
11190
|
+
if (!current) return;
|
|
11191
|
+
const contentLines = current.contentLines;
|
|
11192
|
+
while (contentLines.length > 0 && contentLines[0].trim() === "") contentLines.shift();
|
|
11193
|
+
while (contentLines.length > 0 && contentLines[contentLines.length - 1].trim() === "") contentLines.pop();
|
|
11194
|
+
blocks.push({
|
|
11195
|
+
action: current.action,
|
|
11196
|
+
marker: current.marker,
|
|
11197
|
+
target: current.target,
|
|
11198
|
+
content: contentLines.join("\n")
|
|
11199
|
+
});
|
|
11200
|
+
current = null;
|
|
11201
|
+
};
|
|
11202
|
+
let i = 0;
|
|
11203
|
+
while (i < lines.length) {
|
|
11204
|
+
const line = lines[i];
|
|
11205
|
+
if (FENCE_RE.test(line)) {
|
|
11206
|
+
inFence = !inFence;
|
|
11207
|
+
if (current) current.contentLines.push(line);
|
|
11208
|
+
i++;
|
|
11209
|
+
continue;
|
|
11210
|
+
}
|
|
11211
|
+
if (!inFence) {
|
|
11212
|
+
const trimmed = line.trim();
|
|
11213
|
+
const actionMatch = ACTION_RE.exec(trimmed);
|
|
11214
|
+
if (actionMatch) {
|
|
11215
|
+
finalizeCurrent();
|
|
11216
|
+
const action = actionMatch[1];
|
|
11217
|
+
if (!isPatchAction(action)) {
|
|
11218
|
+
throw new UserError(`Unknown patch action "${action}" (valid: append, prepend, replace-section, insert-after, insert-before, delete-section)`);
|
|
11219
|
+
}
|
|
11220
|
+
const marker = actionMatch[2];
|
|
11221
|
+
if (ACTIONS_REQUIRING_MARKER.has(action) && (!marker || marker.trim().length === 0)) {
|
|
11222
|
+
throw new UserError(`Action "${action}" requires a quoted marker argument, e.g. <!-- ACTION: ${action} "marker text" -->`);
|
|
11223
|
+
}
|
|
11224
|
+
current = { action, marker: marker || void 0, contentLines: [] };
|
|
11225
|
+
i++;
|
|
11226
|
+
let peek = i;
|
|
11227
|
+
while (peek < lines.length && lines[peek].trim() === "") peek++;
|
|
11228
|
+
if (peek < lines.length) {
|
|
11229
|
+
const targetMatch = TARGET_RE.exec(lines[peek].trim());
|
|
11230
|
+
if (targetMatch) {
|
|
11231
|
+
current.target = targetMatch[1];
|
|
11232
|
+
i = peek + 1;
|
|
11233
|
+
}
|
|
11234
|
+
}
|
|
11235
|
+
continue;
|
|
11236
|
+
}
|
|
11237
|
+
}
|
|
11238
|
+
if (current) current.contentLines.push(line);
|
|
11239
|
+
i++;
|
|
11240
|
+
}
|
|
11241
|
+
finalizeCurrent();
|
|
11242
|
+
if (blocks.length === 0) {
|
|
11243
|
+
throw new UserError("No ACTION blocks found in patch source");
|
|
11244
|
+
}
|
|
11245
|
+
return blocks;
|
|
10791
11246
|
}
|
|
10792
|
-
function
|
|
10793
|
-
|
|
11247
|
+
function frontmatterOffset(lines) {
|
|
11248
|
+
if (lines[0] !== "---") return 0;
|
|
11249
|
+
for (let i = 1; i < lines.length; i++) {
|
|
11250
|
+
if (lines[i] === "---") return i + 1;
|
|
11251
|
+
}
|
|
11252
|
+
return 0;
|
|
10794
11253
|
}
|
|
10795
|
-
function
|
|
10796
|
-
|
|
11254
|
+
function buildHeadingTree(lines, bodyOffset) {
|
|
11255
|
+
const flat = [];
|
|
11256
|
+
let inFence = false;
|
|
11257
|
+
for (let i = bodyOffset; i < lines.length; i++) {
|
|
11258
|
+
const line = lines[i];
|
|
11259
|
+
if (FENCE_RE.test(line)) {
|
|
11260
|
+
inFence = !inFence;
|
|
11261
|
+
continue;
|
|
11262
|
+
}
|
|
11263
|
+
if (inFence) continue;
|
|
11264
|
+
const m = HEADING_RE.exec(line);
|
|
11265
|
+
if (m) flat.push({ level: m[1].length, text: m[2].trim(), line: i });
|
|
11266
|
+
}
|
|
11267
|
+
const root = { level: 0, heading: "", headingLine: bodyOffset - 1, bodyStart: bodyOffset, bodyEnd: lines.length, children: [] };
|
|
11268
|
+
const stack = [root];
|
|
11269
|
+
for (const h of flat) {
|
|
11270
|
+
while (stack.length > 1 && stack[stack.length - 1].level >= h.level) {
|
|
11271
|
+
const done = stack.pop();
|
|
11272
|
+
if (done) done.bodyEnd = h.line;
|
|
11273
|
+
}
|
|
11274
|
+
const node = { level: h.level, heading: h.text, headingLine: h.line, bodyStart: h.line + 1, bodyEnd: lines.length, children: [] };
|
|
11275
|
+
stack[stack.length - 1].children.push(node);
|
|
11276
|
+
stack.push(node);
|
|
11277
|
+
}
|
|
11278
|
+
return root;
|
|
10797
11279
|
}
|
|
10798
|
-
function
|
|
10799
|
-
return
|
|
11280
|
+
function normalizeText(s) {
|
|
11281
|
+
return s.trim().replace(/\s+/g, " ");
|
|
10800
11282
|
}
|
|
10801
|
-
function
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
|
|
10806
|
-
|
|
11283
|
+
function splitHeadingPath(target) {
|
|
11284
|
+
return target.split(">").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
|
|
11285
|
+
}
|
|
11286
|
+
function collectDescendants(node, acc) {
|
|
11287
|
+
acc.push(node);
|
|
11288
|
+
for (const child of node.children) collectDescendants(child, acc);
|
|
11289
|
+
}
|
|
11290
|
+
function resolveHeadingPath(root, segments) {
|
|
11291
|
+
if (segments.length === 0) return root;
|
|
11292
|
+
const allNodes = [];
|
|
11293
|
+
collectDescendants(root, allNodes);
|
|
11294
|
+
const headingNodes = allNodes.filter((n) => n.level > 0);
|
|
11295
|
+
const firstNorm = normalizeText(segments[0]);
|
|
11296
|
+
let candidates = headingNodes.filter((n) => normalizeText(n.heading) === firstNorm);
|
|
11297
|
+
if (candidates.length === 0) {
|
|
11298
|
+
const available = headingNodes.map((n) => n.heading);
|
|
11299
|
+
throw new UserError(`No section matching "${segments[0]}" found (available headings: ${available.length > 0 ? available.join(", ") : "none"})`);
|
|
10807
11300
|
}
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
10811
|
-
|
|
11301
|
+
for (let i = 1; i < segments.length; i++) {
|
|
11302
|
+
const segment = segments[i];
|
|
11303
|
+
const norm = normalizeText(segment);
|
|
11304
|
+
const next = [];
|
|
11305
|
+
for (const candidate of candidates) {
|
|
11306
|
+
for (const child of candidate.children) {
|
|
11307
|
+
if (normalizeText(child.heading) === norm) next.push(child);
|
|
11308
|
+
}
|
|
11309
|
+
}
|
|
11310
|
+
if (next.length === 0) {
|
|
11311
|
+
const available = [...new Set(candidates.flatMap((c) => c.children.map((child) => child.heading)))];
|
|
11312
|
+
throw new UserError(`No section matching "${segment}" found (available headings: ${available.length > 0 ? available.join(", ") : "none"})`);
|
|
11313
|
+
}
|
|
11314
|
+
candidates = next;
|
|
11315
|
+
}
|
|
11316
|
+
if (candidates.length > 1) {
|
|
11317
|
+
throw new UserError(`Target selector "${segments.join(" > ")}" matched ${candidates.length} sections; be more specific`);
|
|
10812
11318
|
}
|
|
11319
|
+
return candidates[0];
|
|
10813
11320
|
}
|
|
10814
|
-
function
|
|
10815
|
-
|
|
10816
|
-
|
|
11321
|
+
function findMarkerLine(lines, start, end, marker) {
|
|
11322
|
+
const norm = normalizeText(marker);
|
|
11323
|
+
const matches = [];
|
|
11324
|
+
for (let i = start; i < end; i++) {
|
|
11325
|
+
if (normalizeText(lines[i]).includes(norm)) matches.push(i);
|
|
10817
11326
|
}
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
throw new UserError(`malformed ${label2} at ${path}: expected an object with a "records" array`);
|
|
11327
|
+
if (matches.length === 0) {
|
|
11328
|
+
throw new UserError(`No line matching marker "${marker}" found in target section`);
|
|
10821
11329
|
}
|
|
10822
|
-
|
|
10823
|
-
|
|
10824
|
-
|
|
11330
|
+
if (matches.length > 1) {
|
|
11331
|
+
const preview = matches.map((i) => `line ${i + 1}: "${lines[i].trim()}"`).join("; ");
|
|
11332
|
+
throw new UserError(`Marker "${marker}" matched ${matches.length} lines, expected exactly 1 (${preview})`);
|
|
10825
11333
|
}
|
|
10826
|
-
return
|
|
11334
|
+
return matches[0];
|
|
10827
11335
|
}
|
|
10828
|
-
function
|
|
11336
|
+
function findChildByMarker(target, marker) {
|
|
11337
|
+
const norm = normalizeText(marker);
|
|
11338
|
+
const matches = target.children.filter((c) => normalizeText(c.heading).includes(norm));
|
|
11339
|
+
if (matches.length === 0) {
|
|
11340
|
+
const available = target.children.map((c) => c.heading).join(", ") || "none";
|
|
11341
|
+
throw new UserError(`No subsection matching "${marker}" found under target section (available: ${available})`);
|
|
11342
|
+
}
|
|
11343
|
+
if (matches.length > 1) {
|
|
11344
|
+
throw new UserError(`Marker "${marker}" matched ${matches.length} subsections: ${matches.map((c) => c.heading).join(", ")}`);
|
|
11345
|
+
}
|
|
11346
|
+
return matches[0];
|
|
11347
|
+
}
|
|
11348
|
+
function requireMarker(block) {
|
|
11349
|
+
if (!block.marker || block.marker.trim().length === 0) {
|
|
11350
|
+
throw new UserError(`Action "${block.action}" requires a marker`);
|
|
11351
|
+
}
|
|
11352
|
+
return block.marker;
|
|
11353
|
+
}
|
|
11354
|
+
function applyOneAction(lines, target, block) {
|
|
11355
|
+
const contentLines = block.content.length === 0 ? [""] : block.content.split("\n");
|
|
11356
|
+
switch (block.action) {
|
|
11357
|
+
case "append": {
|
|
11358
|
+
lines.splice(target.bodyEnd, 0, ...contentLines);
|
|
11359
|
+
return;
|
|
11360
|
+
}
|
|
11361
|
+
case "prepend": {
|
|
11362
|
+
lines.splice(target.bodyStart, 0, ...contentLines);
|
|
11363
|
+
return;
|
|
11364
|
+
}
|
|
11365
|
+
case "insert-after":
|
|
11366
|
+
case "insert-before": {
|
|
11367
|
+
const idx = findMarkerLine(lines, target.bodyStart, target.bodyEnd, requireMarker(block));
|
|
11368
|
+
lines.splice(block.action === "insert-after" ? idx + 1 : idx, 0, ...contentLines);
|
|
11369
|
+
return;
|
|
11370
|
+
}
|
|
11371
|
+
case "replace-section":
|
|
11372
|
+
case "delete-section": {
|
|
11373
|
+
const child = findChildByMarker(target, requireMarker(block));
|
|
11374
|
+
const replacement = block.action === "replace-section" ? contentLines : [];
|
|
11375
|
+
lines.splice(child.headingLine, child.bodyEnd - child.headingLine, ...replacement);
|
|
11376
|
+
return;
|
|
11377
|
+
}
|
|
11378
|
+
default: {
|
|
11379
|
+
const exhaustive = block.action;
|
|
11380
|
+
throw new UserError(`Unknown patch action "${exhaustive}"`);
|
|
11381
|
+
}
|
|
11382
|
+
}
|
|
11383
|
+
}
|
|
11384
|
+
function applyPatchBlocks(skillMd, blocks) {
|
|
11385
|
+
const lines = skillMd.split("\n");
|
|
11386
|
+
for (const block of blocks) {
|
|
11387
|
+
const bodyOffset = frontmatterOffset(lines);
|
|
11388
|
+
const root = buildHeadingTree(lines, bodyOffset);
|
|
11389
|
+
const segments = block.target ? splitHeadingPath(block.target) : [];
|
|
11390
|
+
const target = resolveHeadingPath(root, segments);
|
|
11391
|
+
applyOneAction(lines, target, block);
|
|
11392
|
+
}
|
|
11393
|
+
return lines.join("\n");
|
|
11394
|
+
}
|
|
11395
|
+
function renderExtendFile(meta) {
|
|
11396
|
+
return [
|
|
11397
|
+
"# SKILL.extend.md",
|
|
11398
|
+
`# Extensions for: ${meta.skillName}`,
|
|
11399
|
+
`# Generated: ${meta.generatedAt}`,
|
|
11400
|
+
`# Refinement: ${meta.refinementId}`,
|
|
11401
|
+
"",
|
|
11402
|
+
`## EXTEND: ${meta.targetSection}`,
|
|
11403
|
+
"<!-- ACTION: append -->",
|
|
11404
|
+
meta.content,
|
|
11405
|
+
""
|
|
11406
|
+
].join("\n");
|
|
11407
|
+
}
|
|
11408
|
+
function renderConfigOverride(meta) {
|
|
11409
|
+
const payload = {
|
|
11410
|
+
skill: meta.skillName,
|
|
11411
|
+
version: "1.0.0",
|
|
11412
|
+
generated: meta.generatedAt,
|
|
11413
|
+
config: meta.config
|
|
11414
|
+
};
|
|
11415
|
+
return `${JSON.stringify(payload, null, 2)}
|
|
11416
|
+
`;
|
|
11417
|
+
}
|
|
11418
|
+
function computeLcsDiff(a, b) {
|
|
11419
|
+
const n = a.length;
|
|
11420
|
+
const m = b.length;
|
|
11421
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
11422
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
11423
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
11424
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
11425
|
+
}
|
|
11426
|
+
}
|
|
11427
|
+
const ops = [];
|
|
11428
|
+
let i = 0;
|
|
11429
|
+
let j = 0;
|
|
11430
|
+
while (i < n && j < m) {
|
|
11431
|
+
if (a[i] === b[j]) {
|
|
11432
|
+
ops.push({ type: "context", line: a[i] });
|
|
11433
|
+
i++;
|
|
11434
|
+
j++;
|
|
11435
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
11436
|
+
ops.push({ type: "remove", line: a[i] });
|
|
11437
|
+
i++;
|
|
11438
|
+
} else {
|
|
11439
|
+
ops.push({ type: "add", line: b[j] });
|
|
11440
|
+
j++;
|
|
11441
|
+
}
|
|
11442
|
+
}
|
|
11443
|
+
while (i < n) {
|
|
11444
|
+
ops.push({ type: "remove", line: a[i] });
|
|
11445
|
+
i++;
|
|
11446
|
+
}
|
|
11447
|
+
while (j < m) {
|
|
11448
|
+
ops.push({ type: "add", line: b[j] });
|
|
11449
|
+
j++;
|
|
11450
|
+
}
|
|
11451
|
+
return ops;
|
|
11452
|
+
}
|
|
11453
|
+
function previewDiff(before, after, path) {
|
|
11454
|
+
const a = before.split("\n");
|
|
11455
|
+
const b = after.split("\n");
|
|
11456
|
+
const ops = computeLcsDiff(a, b);
|
|
11457
|
+
const lines = [`--- ${path}`, `+++ ${path}`];
|
|
11458
|
+
for (const op of ops) {
|
|
11459
|
+
const prefix = op.type === "add" ? "+" : op.type === "remove" ? "-" : " ";
|
|
11460
|
+
lines.push(`${prefix}${op.line}`);
|
|
11461
|
+
}
|
|
11462
|
+
return lines.join("\n");
|
|
11463
|
+
}
|
|
11464
|
+
|
|
11465
|
+
// src/refine/activation.ts
|
|
11466
|
+
var OUTSIDE_RESOURCE = /\$\{?CLAUDE_PLUGIN_ROOT\b|(?:\]\(|["'\s])(?:\.\.\/)/;
|
|
11467
|
+
function activationFiles(baseDir, overrideDir, name) {
|
|
11468
|
+
const files = [];
|
|
11469
|
+
let total = 0;
|
|
11470
|
+
function walk2(dir, prefix = "") {
|
|
11471
|
+
for (const entry of readdirSync12(dir).sort()) {
|
|
11472
|
+
if (entry === ".git" || entry === "node_modules") continue;
|
|
11473
|
+
const path = join38(dir, entry), relativePath = prefix ? `${prefix}/${entry}` : entry;
|
|
11474
|
+
const stat = lstatSync7(path);
|
|
11475
|
+
if (stat.isSymbolicLink()) throw new UserError(`Activation refuses symlinks: ${path}`);
|
|
11476
|
+
if (stat.isDirectory()) {
|
|
11477
|
+
walk2(path, relativePath);
|
|
11478
|
+
continue;
|
|
11479
|
+
}
|
|
11480
|
+
if (!stat.isFile() || stat.size > 2 * 1024 * 1024 || files.length >= 1e3 || (total += stat.size) > 16 * 1024 * 1024) {
|
|
11481
|
+
throw new UserError("Activation source exceeds the bounded file limits (1000 files, 2 MiB/file, 16 MiB total) or contains a special file.");
|
|
11482
|
+
}
|
|
11483
|
+
const bytes = readFileSync28(path);
|
|
11484
|
+
if (OUTSIDE_RESOURCE.test(bytes.toString("utf8"))) {
|
|
11485
|
+
throw new UserError(`Activation needs a self-contained skill; ${relativePath} references its parent/plugin tree. Vendor the required resources first.`);
|
|
11486
|
+
}
|
|
11487
|
+
files.push({ relativePath, bytes, mode: stat.mode & 511 });
|
|
11488
|
+
}
|
|
11489
|
+
}
|
|
11490
|
+
walk2(realpathSync11(baseDir));
|
|
11491
|
+
const skill = files.find((f) => f.relativePath === "SKILL.md");
|
|
11492
|
+
if (!skill) throw new UserError("The explicit base directory must contain SKILL.md.");
|
|
11493
|
+
const artifacts = readdirSync12(overrideDir).sort();
|
|
11494
|
+
if (!artifacts.length || artifacts.some((p) => !["SKILL.patch.md", "SKILL.extend.md"].includes(p))) {
|
|
11495
|
+
throw new UserError("Activation supports a capture directory containing only SKILL.patch.md and/or SKILL.extend.md. Full/config/hook/script overrides require separate review.");
|
|
11496
|
+
}
|
|
11497
|
+
let content = skill.bytes.toString("utf8");
|
|
11498
|
+
for (const artifact of artifacts) {
|
|
11499
|
+
const path = join38(overrideDir, artifact);
|
|
11500
|
+
const stat = lstatSync7(path);
|
|
11501
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new UserError("Override artifact must be a regular file, not a symlink.");
|
|
11502
|
+
if (stat.size > 2 * 1024 * 1024) throw new UserError("Override artifact exceeds 2 MiB.");
|
|
11503
|
+
const text = readFileSync28(path, "utf8");
|
|
11504
|
+
if (artifact === "SKILL.patch.md") content = applyPatchBlocks(content, parsePatchFile(text));
|
|
11505
|
+
else {
|
|
11506
|
+
const chunks = text.split(/^# SKILL\.extend\.md\r?$/m).filter((c) => c.trim());
|
|
11507
|
+
for (const chunk of chunks) {
|
|
11508
|
+
const match = /^\n# Extensions for: [^\n]+\n# Generated: [^\n]+\n# Refinement: [^\n]+\n\n## EXTEND: ([^\n]+)\n<!-- ACTION: append -->\n([\s\S]*)$/.exec(chunk);
|
|
11509
|
+
if (!match) throw new UserError("Unrecognized extend artifact; re-capture it or use an explicit patch.");
|
|
11510
|
+
content = applyPatchBlocks(content, [{ action: "append", target: match[1], content: match[2].trim() }]);
|
|
11511
|
+
}
|
|
11512
|
+
}
|
|
11513
|
+
}
|
|
11514
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content);
|
|
11515
|
+
if (!frontmatter) throw new UserError("Host activation requires SKILL.md YAML frontmatter.");
|
|
11516
|
+
const header = frontmatter[1].replace(/^name:.*(?:\r?\n|$)/m, "");
|
|
11517
|
+
content = `---
|
|
11518
|
+
name: ${name}
|
|
11519
|
+
${header}
|
|
11520
|
+
---
|
|
11521
|
+
${content.slice(frontmatter[0].length)}`;
|
|
11522
|
+
if (OUTSIDE_RESOURCE.test(content)) {
|
|
11523
|
+
throw new UserError("Activation needs a self-contained skill; captured content references its parent/plugin tree.");
|
|
11524
|
+
}
|
|
11525
|
+
skill.bytes = Buffer.from(content);
|
|
11526
|
+
return files;
|
|
11527
|
+
}
|
|
11528
|
+
|
|
11529
|
+
// src/commands/refine.ts
|
|
11530
|
+
import { createHash as createHash8, randomUUID as randomUUID5 } from "crypto";
|
|
11531
|
+
import { chmodSync as chmodSync4, existsSync as existsSync31, lstatSync as lstatSync8, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync4, rmdirSync, readFileSync as readFileSync30, realpathSync as realpathSync14, renameSync as renameSync6, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "fs";
|
|
11532
|
+
import { dirname as dirname15, isAbsolute as isAbsolute11, join as join41, relative as relative10, resolve as resolvePath15, sep as sep10 } from "path";
|
|
11533
|
+
import { createInterface as createInterface7 } from "readline/promises";
|
|
11534
|
+
|
|
11535
|
+
// src/refine/context.ts
|
|
11536
|
+
import { existsSync as existsSync30, realpathSync as realpathSync13, statSync as statSync14 } from "fs";
|
|
11537
|
+
import { isAbsolute as isAbsolute10, join as join40, relative as relative9, resolve as resolvePath14 } from "path";
|
|
11538
|
+
|
|
11539
|
+
// src/refine/store.ts
|
|
11540
|
+
import { createHash as createHash7 } from "crypto";
|
|
11541
|
+
import { existsSync as existsSync29, readFileSync as readFileSync29, statSync as statSync13, realpathSync as realpathSync12 } from "fs";
|
|
11542
|
+
import { join as join39, resolve as resolvePath13 } from "path";
|
|
11543
|
+
var SIMILARITY_JOIN_THRESHOLD = 0.7;
|
|
11544
|
+
var READY_THRESHOLD = 2;
|
|
11545
|
+
function getRefinementsDir() {
|
|
11546
|
+
return join39(getHomeDir(), "refinements");
|
|
11547
|
+
}
|
|
11548
|
+
function getRefinementsPath() {
|
|
11549
|
+
return join39(getRefinementsDir(), "refinements.json");
|
|
11550
|
+
}
|
|
11551
|
+
function getPatternsPath() {
|
|
11552
|
+
return join39(getRefinementsDir(), "patterns.json");
|
|
11553
|
+
}
|
|
11554
|
+
function getBackupsDir() {
|
|
11555
|
+
return join39(getRefinementsDir(), "backups");
|
|
11556
|
+
}
|
|
11557
|
+
function readJsonFile(path, label2) {
|
|
11558
|
+
let raw;
|
|
11559
|
+
try {
|
|
11560
|
+
raw = readFileSync29(path, "utf-8");
|
|
11561
|
+
} catch (err) {
|
|
11562
|
+
throw new UserError(`unable to read ${label2} at ${path}: ${err.message}`);
|
|
11563
|
+
}
|
|
11564
|
+
try {
|
|
11565
|
+
return JSON.parse(raw);
|
|
11566
|
+
} catch {
|
|
11567
|
+
throw new UserError(`malformed ${label2} at ${path}: not valid JSON`);
|
|
11568
|
+
}
|
|
11569
|
+
}
|
|
11570
|
+
function readStore(path, label2, validateRecord) {
|
|
11571
|
+
if (!existsSync29(path)) {
|
|
11572
|
+
return { schemaVersion: 1, records: [] };
|
|
11573
|
+
}
|
|
11574
|
+
const parsed = readJsonFile(path, label2);
|
|
11575
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.records)) {
|
|
11576
|
+
throw new UserError(`malformed ${label2} at ${path}: expected an object with a "records" array`);
|
|
11577
|
+
}
|
|
11578
|
+
const records = parsed.records;
|
|
11579
|
+
if (validateRecord) {
|
|
11580
|
+
records.forEach((record, index) => validateRecord(record, index));
|
|
11581
|
+
}
|
|
11582
|
+
return { schemaVersion: 1, records };
|
|
11583
|
+
}
|
|
11584
|
+
function validateRefinementRecordShape(record, index, path) {
|
|
10829
11585
|
if (typeof record !== "object" || record === null) {
|
|
10830
11586
|
throw new UserError(`malformed refinements.json at ${path}: record[${index}] is not an object`);
|
|
10831
11587
|
}
|
|
@@ -10904,7 +11660,7 @@ function writePatternsStore(store) {
|
|
|
10904
11660
|
function resolveProjectRealpath(projectRoot) {
|
|
10905
11661
|
const resolved = resolvePath13(projectRoot);
|
|
10906
11662
|
try {
|
|
10907
|
-
return
|
|
11663
|
+
return realpathSync12(resolved);
|
|
10908
11664
|
} catch {
|
|
10909
11665
|
return resolved;
|
|
10910
11666
|
}
|
|
@@ -11036,16 +11792,16 @@ function createBackup(filePaths, deps = {}) {
|
|
|
11036
11792
|
const randomHex = deps.randomHex ?? defaultRandomHex;
|
|
11037
11793
|
const id = `${formatBackupTimestamp(now)}-${randomHex()}`;
|
|
11038
11794
|
const files = filePaths.map((path) => {
|
|
11039
|
-
if (!
|
|
11795
|
+
if (!existsSync29(path)) {
|
|
11040
11796
|
return { path, existed: false, sha256: null, mode: null, contentBase64: null };
|
|
11041
11797
|
}
|
|
11042
|
-
const bytes =
|
|
11043
|
-
const mode =
|
|
11798
|
+
const bytes = readFileSync29(path);
|
|
11799
|
+
const mode = statSync13(path).mode & 511;
|
|
11044
11800
|
const sha2562 = createHash7("sha256").update(bytes).digest("hex");
|
|
11045
11801
|
return { path, existed: true, sha256: sha2562, mode, contentBase64: bytes.toString("base64") };
|
|
11046
11802
|
});
|
|
11047
11803
|
const manifest = { id, createdAt: now.toISOString(), files };
|
|
11048
|
-
writeJsonAtomic(
|
|
11804
|
+
writeJsonAtomic(join39(getBackupsDir(), id, "manifest.json"), manifest);
|
|
11049
11805
|
return manifest;
|
|
11050
11806
|
}
|
|
11051
11807
|
function validateBackupManifestShape(value, path) {
|
|
@@ -11094,8 +11850,8 @@ function readBackupManifest(id) {
|
|
|
11094
11850
|
if (!isValidBackupId(id)) {
|
|
11095
11851
|
throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
|
|
11096
11852
|
}
|
|
11097
|
-
const manifestPath =
|
|
11098
|
-
if (!
|
|
11853
|
+
const manifestPath = join39(getBackupsDir(), id, "manifest.json");
|
|
11854
|
+
if (!existsSync29(manifestPath)) {
|
|
11099
11855
|
throw new UserError(`no backup with id: ${id}`);
|
|
11100
11856
|
}
|
|
11101
11857
|
const parsed = readJsonFile(manifestPath, "backup manifest");
|
|
@@ -11117,32 +11873,32 @@ function assertPlainSkillName(name) {
|
|
|
11117
11873
|
}
|
|
11118
11874
|
function realpathOrNull2(p) {
|
|
11119
11875
|
try {
|
|
11120
|
-
return
|
|
11876
|
+
return realpathSync13(p);
|
|
11121
11877
|
} catch {
|
|
11122
11878
|
return null;
|
|
11123
11879
|
}
|
|
11124
11880
|
}
|
|
11125
11881
|
function isUnderRoot2(candidate, root) {
|
|
11126
|
-
const rel =
|
|
11127
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
11882
|
+
const rel = relative9(root, candidate);
|
|
11883
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute10(rel);
|
|
11128
11884
|
}
|
|
11129
11885
|
function resolveScopes(config, cwd, skill, userRootFlag) {
|
|
11130
11886
|
assertPlainSkillName(skill);
|
|
11131
11887
|
const resolvedCwd = resolvePath14(cwd);
|
|
11132
|
-
const localDir =
|
|
11133
|
-
const sharedDir =
|
|
11134
|
-
const local = { scope: "local", dir: localDir, exists:
|
|
11135
|
-
const shared = { scope: "shared", dir: sharedDir, exists:
|
|
11888
|
+
const localDir = join40(resolvedCwd, ".claude", "skills", skill);
|
|
11889
|
+
const sharedDir = join40(resolvedCwd, "skills", skill);
|
|
11890
|
+
const local = { scope: "local", dir: localDir, exists: existsSync30(localDir) };
|
|
11891
|
+
const shared = { scope: "shared", dir: sharedDir, exists: existsSync30(sharedDir) };
|
|
11136
11892
|
const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
|
|
11137
11893
|
return { local, shared, user };
|
|
11138
11894
|
}
|
|
11139
11895
|
function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
|
|
11140
11896
|
if (userRootFlag) {
|
|
11141
11897
|
const root = resolvePath14(userRootFlag);
|
|
11142
|
-
return { resolved: true, dir:
|
|
11898
|
+
return { resolved: true, dir: join40(root, skill), root: userRootFlag, reason: "flag" };
|
|
11143
11899
|
}
|
|
11144
11900
|
const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
|
|
11145
|
-
const candidates = config.skillsRoots.map((root) => ({ root, dir:
|
|
11901
|
+
const candidates = config.skillsRoots.map((root) => ({ root, dir: join40(resolveConfiguredRoot2(root), skill) }));
|
|
11146
11902
|
for (const candidate of candidates) {
|
|
11147
11903
|
const skillReal = realpathOrNull2(candidate.dir);
|
|
11148
11904
|
if (skillReal) {
|
|
@@ -11160,29 +11916,29 @@ function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
|
|
|
11160
11916
|
}
|
|
11161
11917
|
function isFile2(p) {
|
|
11162
11918
|
try {
|
|
11163
|
-
return
|
|
11919
|
+
return statSync14(p).isFile();
|
|
11164
11920
|
} catch {
|
|
11165
11921
|
return false;
|
|
11166
11922
|
}
|
|
11167
11923
|
}
|
|
11168
11924
|
function isDirectory(p) {
|
|
11169
11925
|
try {
|
|
11170
|
-
return
|
|
11926
|
+
return statSync14(p).isDirectory();
|
|
11171
11927
|
} catch {
|
|
11172
11928
|
return false;
|
|
11173
11929
|
}
|
|
11174
11930
|
}
|
|
11175
|
-
function inventoryScopeDir(
|
|
11931
|
+
function inventoryScopeDir(scope2, dir) {
|
|
11176
11932
|
return {
|
|
11177
|
-
scope,
|
|
11933
|
+
scope: scope2,
|
|
11178
11934
|
dir,
|
|
11179
11935
|
dirExists: isDirectory(dir),
|
|
11180
|
-
skillMd: isFile2(
|
|
11181
|
-
patchMd: isFile2(
|
|
11182
|
-
extendMd: isFile2(
|
|
11183
|
-
configJson: isFile2(
|
|
11184
|
-
hooksDir: isDirectory(
|
|
11185
|
-
scriptsDir: isDirectory(
|
|
11936
|
+
skillMd: isFile2(join40(dir, "SKILL.md")),
|
|
11937
|
+
patchMd: isFile2(join40(dir, "SKILL.patch.md")),
|
|
11938
|
+
extendMd: isFile2(join40(dir, "SKILL.extend.md")),
|
|
11939
|
+
configJson: isFile2(join40(dir, "skill-config.json")),
|
|
11940
|
+
hooksDir: isDirectory(join40(dir, "hooks")),
|
|
11941
|
+
scriptsDir: isDirectory(join40(dir, "scripts"))
|
|
11186
11942
|
};
|
|
11187
11943
|
}
|
|
11188
11944
|
function inventoryAllScopes(resolution) {
|
|
@@ -11193,230 +11949,10 @@ function inventoryAllScopes(resolution) {
|
|
|
11193
11949
|
return rows;
|
|
11194
11950
|
}
|
|
11195
11951
|
|
|
11196
|
-
// src/refine/patch.ts
|
|
11197
|
-
var FENCE_RE = /^\s*(```|~~~)/;
|
|
11198
|
-
var HEADING_RE = /^(#{1,6})\s+(.*)$/;
|
|
11199
|
-
function frontmatterOffset(lines) {
|
|
11200
|
-
if (lines[0] !== "---") return 0;
|
|
11201
|
-
for (let i = 1; i < lines.length; i++) {
|
|
11202
|
-
if (lines[i] === "---") return i + 1;
|
|
11203
|
-
}
|
|
11204
|
-
return 0;
|
|
11205
|
-
}
|
|
11206
|
-
function buildHeadingTree(lines, bodyOffset) {
|
|
11207
|
-
const flat = [];
|
|
11208
|
-
let inFence = false;
|
|
11209
|
-
for (let i = bodyOffset; i < lines.length; i++) {
|
|
11210
|
-
const line = lines[i];
|
|
11211
|
-
if (FENCE_RE.test(line)) {
|
|
11212
|
-
inFence = !inFence;
|
|
11213
|
-
continue;
|
|
11214
|
-
}
|
|
11215
|
-
if (inFence) continue;
|
|
11216
|
-
const m = HEADING_RE.exec(line);
|
|
11217
|
-
if (m) flat.push({ level: m[1].length, text: m[2].trim(), line: i });
|
|
11218
|
-
}
|
|
11219
|
-
const root = { level: 0, heading: "", headingLine: bodyOffset - 1, bodyStart: bodyOffset, bodyEnd: lines.length, children: [] };
|
|
11220
|
-
const stack = [root];
|
|
11221
|
-
for (const h of flat) {
|
|
11222
|
-
while (stack.length > 1 && stack[stack.length - 1].level >= h.level) {
|
|
11223
|
-
const done = stack.pop();
|
|
11224
|
-
if (done) done.bodyEnd = h.line;
|
|
11225
|
-
}
|
|
11226
|
-
const node = { level: h.level, heading: h.text, headingLine: h.line, bodyStart: h.line + 1, bodyEnd: lines.length, children: [] };
|
|
11227
|
-
stack[stack.length - 1].children.push(node);
|
|
11228
|
-
stack.push(node);
|
|
11229
|
-
}
|
|
11230
|
-
return root;
|
|
11231
|
-
}
|
|
11232
|
-
function normalizeText(s) {
|
|
11233
|
-
return s.trim().replace(/\s+/g, " ");
|
|
11234
|
-
}
|
|
11235
|
-
function splitHeadingPath(target) {
|
|
11236
|
-
return target.split(">").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
|
|
11237
|
-
}
|
|
11238
|
-
function collectDescendants(node, acc) {
|
|
11239
|
-
acc.push(node);
|
|
11240
|
-
for (const child of node.children) collectDescendants(child, acc);
|
|
11241
|
-
}
|
|
11242
|
-
function resolveHeadingPath(root, segments) {
|
|
11243
|
-
if (segments.length === 0) return root;
|
|
11244
|
-
const allNodes = [];
|
|
11245
|
-
collectDescendants(root, allNodes);
|
|
11246
|
-
const headingNodes = allNodes.filter((n) => n.level > 0);
|
|
11247
|
-
const firstNorm = normalizeText(segments[0]);
|
|
11248
|
-
let candidates = headingNodes.filter((n) => normalizeText(n.heading) === firstNorm);
|
|
11249
|
-
if (candidates.length === 0) {
|
|
11250
|
-
const available = headingNodes.map((n) => n.heading);
|
|
11251
|
-
throw new UserError(`No section matching "${segments[0]}" found (available headings: ${available.length > 0 ? available.join(", ") : "none"})`);
|
|
11252
|
-
}
|
|
11253
|
-
for (let i = 1; i < segments.length; i++) {
|
|
11254
|
-
const segment = segments[i];
|
|
11255
|
-
const norm = normalizeText(segment);
|
|
11256
|
-
const next = [];
|
|
11257
|
-
for (const candidate of candidates) {
|
|
11258
|
-
for (const child of candidate.children) {
|
|
11259
|
-
if (normalizeText(child.heading) === norm) next.push(child);
|
|
11260
|
-
}
|
|
11261
|
-
}
|
|
11262
|
-
if (next.length === 0) {
|
|
11263
|
-
const available = [...new Set(candidates.flatMap((c) => c.children.map((child) => child.heading)))];
|
|
11264
|
-
throw new UserError(`No section matching "${segment}" found (available headings: ${available.length > 0 ? available.join(", ") : "none"})`);
|
|
11265
|
-
}
|
|
11266
|
-
candidates = next;
|
|
11267
|
-
}
|
|
11268
|
-
if (candidates.length > 1) {
|
|
11269
|
-
throw new UserError(`Target selector "${segments.join(" > ")}" matched ${candidates.length} sections; be more specific`);
|
|
11270
|
-
}
|
|
11271
|
-
return candidates[0];
|
|
11272
|
-
}
|
|
11273
|
-
function findMarkerLine(lines, start, end, marker) {
|
|
11274
|
-
const norm = normalizeText(marker);
|
|
11275
|
-
const matches = [];
|
|
11276
|
-
for (let i = start; i < end; i++) {
|
|
11277
|
-
if (normalizeText(lines[i]).includes(norm)) matches.push(i);
|
|
11278
|
-
}
|
|
11279
|
-
if (matches.length === 0) {
|
|
11280
|
-
throw new UserError(`No line matching marker "${marker}" found in target section`);
|
|
11281
|
-
}
|
|
11282
|
-
if (matches.length > 1) {
|
|
11283
|
-
const preview = matches.map((i) => `line ${i + 1}: "${lines[i].trim()}"`).join("; ");
|
|
11284
|
-
throw new UserError(`Marker "${marker}" matched ${matches.length} lines, expected exactly 1 (${preview})`);
|
|
11285
|
-
}
|
|
11286
|
-
return matches[0];
|
|
11287
|
-
}
|
|
11288
|
-
function findChildByMarker(target, marker) {
|
|
11289
|
-
const norm = normalizeText(marker);
|
|
11290
|
-
const matches = target.children.filter((c) => normalizeText(c.heading).includes(norm));
|
|
11291
|
-
if (matches.length === 0) {
|
|
11292
|
-
const available = target.children.map((c) => c.heading).join(", ") || "none";
|
|
11293
|
-
throw new UserError(`No subsection matching "${marker}" found under target section (available: ${available})`);
|
|
11294
|
-
}
|
|
11295
|
-
if (matches.length > 1) {
|
|
11296
|
-
throw new UserError(`Marker "${marker}" matched ${matches.length} subsections: ${matches.map((c) => c.heading).join(", ")}`);
|
|
11297
|
-
}
|
|
11298
|
-
return matches[0];
|
|
11299
|
-
}
|
|
11300
|
-
function requireMarker(block) {
|
|
11301
|
-
if (!block.marker || block.marker.trim().length === 0) {
|
|
11302
|
-
throw new UserError(`Action "${block.action}" requires a marker`);
|
|
11303
|
-
}
|
|
11304
|
-
return block.marker;
|
|
11305
|
-
}
|
|
11306
|
-
function applyOneAction(lines, target, block) {
|
|
11307
|
-
const contentLines = block.content.length === 0 ? [""] : block.content.split("\n");
|
|
11308
|
-
switch (block.action) {
|
|
11309
|
-
case "append": {
|
|
11310
|
-
lines.splice(target.bodyEnd, 0, ...contentLines);
|
|
11311
|
-
return;
|
|
11312
|
-
}
|
|
11313
|
-
case "prepend": {
|
|
11314
|
-
lines.splice(target.bodyStart, 0, ...contentLines);
|
|
11315
|
-
return;
|
|
11316
|
-
}
|
|
11317
|
-
case "insert-after":
|
|
11318
|
-
case "insert-before": {
|
|
11319
|
-
const idx = findMarkerLine(lines, target.bodyStart, target.bodyEnd, requireMarker(block));
|
|
11320
|
-
lines.splice(block.action === "insert-after" ? idx + 1 : idx, 0, ...contentLines);
|
|
11321
|
-
return;
|
|
11322
|
-
}
|
|
11323
|
-
case "replace-section":
|
|
11324
|
-
case "delete-section": {
|
|
11325
|
-
const child = findChildByMarker(target, requireMarker(block));
|
|
11326
|
-
const replacement = block.action === "replace-section" ? contentLines : [];
|
|
11327
|
-
lines.splice(child.headingLine, child.bodyEnd - child.headingLine, ...replacement);
|
|
11328
|
-
return;
|
|
11329
|
-
}
|
|
11330
|
-
default: {
|
|
11331
|
-
const exhaustive = block.action;
|
|
11332
|
-
throw new UserError(`Unknown patch action "${exhaustive}"`);
|
|
11333
|
-
}
|
|
11334
|
-
}
|
|
11335
|
-
}
|
|
11336
|
-
function applyPatchBlocks(skillMd, blocks) {
|
|
11337
|
-
const lines = skillMd.split("\n");
|
|
11338
|
-
for (const block of blocks) {
|
|
11339
|
-
const bodyOffset = frontmatterOffset(lines);
|
|
11340
|
-
const root = buildHeadingTree(lines, bodyOffset);
|
|
11341
|
-
const segments = block.target ? splitHeadingPath(block.target) : [];
|
|
11342
|
-
const target = resolveHeadingPath(root, segments);
|
|
11343
|
-
applyOneAction(lines, target, block);
|
|
11344
|
-
}
|
|
11345
|
-
return lines.join("\n");
|
|
11346
|
-
}
|
|
11347
|
-
function renderExtendFile(meta) {
|
|
11348
|
-
return [
|
|
11349
|
-
"# SKILL.extend.md",
|
|
11350
|
-
`# Extensions for: ${meta.skillName}`,
|
|
11351
|
-
`# Generated: ${meta.generatedAt}`,
|
|
11352
|
-
`# Refinement: ${meta.refinementId}`,
|
|
11353
|
-
"",
|
|
11354
|
-
`## EXTEND: ${meta.targetSection}`,
|
|
11355
|
-
"<!-- ACTION: append -->",
|
|
11356
|
-
meta.content,
|
|
11357
|
-
""
|
|
11358
|
-
].join("\n");
|
|
11359
|
-
}
|
|
11360
|
-
function renderConfigOverride(meta) {
|
|
11361
|
-
const payload = {
|
|
11362
|
-
skill: meta.skillName,
|
|
11363
|
-
version: "1.0.0",
|
|
11364
|
-
generated: meta.generatedAt,
|
|
11365
|
-
config: meta.config
|
|
11366
|
-
};
|
|
11367
|
-
return `${JSON.stringify(payload, null, 2)}
|
|
11368
|
-
`;
|
|
11369
|
-
}
|
|
11370
|
-
function computeLcsDiff(a, b) {
|
|
11371
|
-
const n = a.length;
|
|
11372
|
-
const m = b.length;
|
|
11373
|
-
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
11374
|
-
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
11375
|
-
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
11376
|
-
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
11377
|
-
}
|
|
11378
|
-
}
|
|
11379
|
-
const ops = [];
|
|
11380
|
-
let i = 0;
|
|
11381
|
-
let j = 0;
|
|
11382
|
-
while (i < n && j < m) {
|
|
11383
|
-
if (a[i] === b[j]) {
|
|
11384
|
-
ops.push({ type: "context", line: a[i] });
|
|
11385
|
-
i++;
|
|
11386
|
-
j++;
|
|
11387
|
-
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
11388
|
-
ops.push({ type: "remove", line: a[i] });
|
|
11389
|
-
i++;
|
|
11390
|
-
} else {
|
|
11391
|
-
ops.push({ type: "add", line: b[j] });
|
|
11392
|
-
j++;
|
|
11393
|
-
}
|
|
11394
|
-
}
|
|
11395
|
-
while (i < n) {
|
|
11396
|
-
ops.push({ type: "remove", line: a[i] });
|
|
11397
|
-
i++;
|
|
11398
|
-
}
|
|
11399
|
-
while (j < m) {
|
|
11400
|
-
ops.push({ type: "add", line: b[j] });
|
|
11401
|
-
j++;
|
|
11402
|
-
}
|
|
11403
|
-
return ops;
|
|
11404
|
-
}
|
|
11405
|
-
function previewDiff(before, after, path) {
|
|
11406
|
-
const a = before.split("\n");
|
|
11407
|
-
const b = after.split("\n");
|
|
11408
|
-
const ops = computeLcsDiff(a, b);
|
|
11409
|
-
const lines = [`--- ${path}`, `+++ ${path}`];
|
|
11410
|
-
for (const op of ops) {
|
|
11411
|
-
const prefix = op.type === "add" ? "+" : op.type === "remove" ? "-" : " ";
|
|
11412
|
-
lines.push(`${prefix}${op.line}`);
|
|
11413
|
-
}
|
|
11414
|
-
return lines.join("\n");
|
|
11415
|
-
}
|
|
11416
|
-
|
|
11417
11952
|
// src/commands/refine.ts
|
|
11418
11953
|
var PRO_FEATURE_LABEL9 = "skill refinement";
|
|
11419
11954
|
var REFINE_PROMPT_ASSET = "refine-prompt.md";
|
|
11955
|
+
var PROJECT_SKILL_ROOTS = { claude: ".claude/skills", codex: ".agents/skills" };
|
|
11420
11956
|
function requireProFeature3() {
|
|
11421
11957
|
const status2 = proFeatureStatus(PRO_FEATURE_LABEL9);
|
|
11422
11958
|
if (!status2.enabled) {
|
|
@@ -11454,15 +11990,15 @@ function resolveConfiguredRoot3(root) {
|
|
|
11454
11990
|
return resolvePath15(expandHome(root));
|
|
11455
11991
|
}
|
|
11456
11992
|
function isUnderRoot3(candidate, root) {
|
|
11457
|
-
const rel =
|
|
11458
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
11993
|
+
const rel = relative10(root, candidate);
|
|
11994
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute11(rel);
|
|
11459
11995
|
}
|
|
11460
11996
|
function sanitizeDiffForConsole(diff) {
|
|
11461
11997
|
return diff.split("\n").map((line) => sanitizeForMarkdown(line)).join("\n");
|
|
11462
11998
|
}
|
|
11463
11999
|
function resolveContentOption(options) {
|
|
11464
12000
|
if (options.content !== void 0) return options.content;
|
|
11465
|
-
if (options.contentFile) return
|
|
12001
|
+
if (options.contentFile) return readFileSync30(resolvePath15(options.contentFile), "utf-8");
|
|
11466
12002
|
return void 0;
|
|
11467
12003
|
}
|
|
11468
12004
|
function validateCaptureOptions(options) {
|
|
@@ -11588,7 +12124,7 @@ ${rendered}` : rendered;
|
|
|
11588
12124
|
function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
11589
12125
|
let lst;
|
|
11590
12126
|
try {
|
|
11591
|
-
lst =
|
|
12127
|
+
lst = lstatSync8(scopeDir);
|
|
11592
12128
|
} catch {
|
|
11593
12129
|
lst = void 0;
|
|
11594
12130
|
}
|
|
@@ -11600,7 +12136,7 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
|
11600
12136
|
const lexical = resolvePath15(scopeDir);
|
|
11601
12137
|
let realResolved;
|
|
11602
12138
|
try {
|
|
11603
|
-
realResolved =
|
|
12139
|
+
realResolved = realpathSync14(scopeDir);
|
|
11604
12140
|
} catch {
|
|
11605
12141
|
realResolved = real;
|
|
11606
12142
|
}
|
|
@@ -11614,16 +12150,16 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
|
11614
12150
|
}
|
|
11615
12151
|
function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
|
|
11616
12152
|
const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
|
|
11617
|
-
const destPath =
|
|
12153
|
+
const destPath = join41(scopeDir, relPath);
|
|
11618
12154
|
const destDir = dirname15(destPath);
|
|
11619
12155
|
mkdirSync11(destDir, { recursive: true });
|
|
11620
|
-
const realDestDir =
|
|
12156
|
+
const realDestDir = realpathSync14(destDir);
|
|
11621
12157
|
if (!isUnderRoot3(realDestDir, realScopeDir)) {
|
|
11622
12158
|
throw new UserError(`refusing to write ${destPath}: resolves outside scope directory ${scopeDir}`);
|
|
11623
12159
|
}
|
|
11624
12160
|
let destLst;
|
|
11625
12161
|
try {
|
|
11626
|
-
destLst =
|
|
12162
|
+
destLst = lstatSync8(destPath);
|
|
11627
12163
|
} catch {
|
|
11628
12164
|
destLst = void 0;
|
|
11629
12165
|
}
|
|
@@ -11710,15 +12246,105 @@ Write ${preview.destPath}? [y/N] `);
|
|
|
11710
12246
|
}
|
|
11711
12247
|
function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
|
|
11712
12248
|
const probeRelPath = opts.overrideType === "patch" ? "SKILL.patch.md" : opts.overrideType === "extend" || opts.overrideType === "new" ? "SKILL.extend.md" : void 0;
|
|
11713
|
-
const probePath = probeRelPath ?
|
|
11714
|
-
const existingContent = probePath &&
|
|
12249
|
+
const probePath = probeRelPath ? join41(scopeDir, probeRelPath) : void 0;
|
|
12250
|
+
const existingContent = probePath && existsSync31(probePath) ? readFileSync30(probePath, "utf-8") : "";
|
|
11715
12251
|
const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
|
|
11716
12252
|
const relPath = rendered.relPath;
|
|
11717
|
-
const destPath =
|
|
11718
|
-
const before =
|
|
12253
|
+
const destPath = join41(scopeDir, relPath);
|
|
12254
|
+
const before = existsSync31(destPath) ? readFileSync30(destPath, "utf-8") : "";
|
|
11719
12255
|
const diff = previewDiff(before, rendered.content, destPath);
|
|
11720
12256
|
return { relPath, content: rendered.content, destPath, diff };
|
|
11721
12257
|
}
|
|
12258
|
+
async function refineActivateCommand(options) {
|
|
12259
|
+
requireProFeature3();
|
|
12260
|
+
if (!options.skill || !options.base || !options.as || !["claude", "codex"].includes(options.host ?? "")) {
|
|
12261
|
+
throw new UserError("activate requires --skill, --base, --as and --host claude|codex.");
|
|
12262
|
+
}
|
|
12263
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(options.as)) throw new UserError("--as must be a lowercase hyphenated skill name.");
|
|
12264
|
+
if (options.scope && !["local", "shared"].includes(options.scope)) throw new UserError("--scope must be local|shared.");
|
|
12265
|
+
const cwd = realpathSync14(process.cwd());
|
|
12266
|
+
const scopes = resolveScopes(loadConfig(), cwd, options.skill);
|
|
12267
|
+
const overrideDir = options.scope === "shared" ? scopes.shared.dir : scopes.local.dir;
|
|
12268
|
+
if (realpathSync14(overrideDir) !== overrideDir) throw new UserError("Activation refuses a symlinked override directory.");
|
|
12269
|
+
const hostRoot = join41(cwd, PROJECT_SKILL_ROOTS[options.host]);
|
|
12270
|
+
const destination = join41(hostRoot, options.as);
|
|
12271
|
+
if (existsSync31(destination) || resolveTargetRealPath(destination) !== destination) throw new UserError("Activation destination already exists or contains a symlink; choose a new --as name.");
|
|
12272
|
+
const files = activationFiles(options.base, overrideDir, options.as);
|
|
12273
|
+
const receiptName = ".skill-forge-activation.json";
|
|
12274
|
+
if (files.some((f) => f.relativePath === receiptName)) throw new UserError("Base is already an activation; use its original source and capture.");
|
|
12275
|
+
const provenance = {
|
|
12276
|
+
schemaVersion: 1,
|
|
12277
|
+
host: options.host,
|
|
12278
|
+
skill: options.as,
|
|
12279
|
+
capture: options.skill,
|
|
12280
|
+
source: realpathSync14(options.base),
|
|
12281
|
+
sourceSha256: createHash8("sha256").update(readFileSync30(join41(options.base, "SKILL.md"))).digest("hex"),
|
|
12282
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12283
|
+
runtimeVerified: false
|
|
12284
|
+
};
|
|
12285
|
+
files.push({ relativePath: receiptName, mode: 384, bytes: Buffer.from(JSON.stringify(provenance, null, 2) + "\n") });
|
|
12286
|
+
if (files.length > 1e3 || files.some((f) => f.bytes.length > 2 * 1024 * 1024) || files.reduce((n, f) => n + f.bytes.length, 0) > 16 * 1024 * 1024) {
|
|
12287
|
+
throw new UserError("Composed activation exceeds file or total size limits.");
|
|
12288
|
+
}
|
|
12289
|
+
const stage = mkdtempSync4(join41(tmpdir3(), "skill-forge-activate-"));
|
|
12290
|
+
try {
|
|
12291
|
+
for (const file of files) {
|
|
12292
|
+
const path = join41(stage, file.relativePath);
|
|
12293
|
+
mkdirSync11(dirname15(path), { recursive: true });
|
|
12294
|
+
writeFileSync10(path, file.bytes, { mode: file.mode });
|
|
12295
|
+
}
|
|
12296
|
+
const safety = await scanSafety(stage, "block-high", files.map((f) => ({ file: join41(stage, f.relativePath), content: f.bytes.toString("utf8") })));
|
|
12297
|
+
safety.findings = safety.findings.map((f) => ({ ...f, file: join41(destination, relative10(stage, f.file)) }));
|
|
12298
|
+
const preview = {
|
|
12299
|
+
destination,
|
|
12300
|
+
provenance,
|
|
12301
|
+
host: options.host,
|
|
12302
|
+
invocation: options.host === "claude" ? `/${options.as}` : `$${options.as}`,
|
|
12303
|
+
source: realpathSync14(options.base),
|
|
12304
|
+
files: files.map((f) => f.relativePath),
|
|
12305
|
+
safety,
|
|
12306
|
+
diff: previewDiff(readFileSync30(join41(options.base, "SKILL.md"), "utf8"), files.find((f) => f.relativePath === "SKILL.md").bytes.toString("utf8"), join41(destination, "SKILL.md"))
|
|
12307
|
+
};
|
|
12308
|
+
if (options.dryRun) {
|
|
12309
|
+
emit2(options.json, { ...preview, dryRun: true, activated: false }, () => console.log(JSON.stringify(preview, null, 2)));
|
|
12310
|
+
return;
|
|
12311
|
+
}
|
|
12312
|
+
if (safety.verdict === "block") throw new UserError(`Activation safety gate blocked the staged skill. Inspect --dry-run --json findings; nothing activated.`);
|
|
12313
|
+
if (!options.json) console.log(sanitizeDiffForConsole(preview.diff));
|
|
12314
|
+
if (!await confirmWrite(options.yes, options.json, `Activate ${destination}? [y/N] `)) {
|
|
12315
|
+
emit2(options.json, { activated: false, cancelled: true }, () => console.log("Activation cancelled."));
|
|
12316
|
+
return;
|
|
12317
|
+
}
|
|
12318
|
+
if (resolveTargetRealPath(destination) !== destination) throw new UserError("Activation destination became symlinked.");
|
|
12319
|
+
ensureScopeDirSafe(hostRoot, cwd);
|
|
12320
|
+
mkdirSync11(destination);
|
|
12321
|
+
const backup = createBackup(files.map((f) => join41(destination, f.relativePath)));
|
|
12322
|
+
const hashes = files.map((f) => ({ path: join41(destination, f.relativePath), sha256: createHash8("sha256").update(f.bytes).digest("hex") }));
|
|
12323
|
+
writePostWriteSidecar(backup.id, hashes);
|
|
12324
|
+
const projectConfig = { ...loadConfig(), skillsRoots: [hostRoot] };
|
|
12325
|
+
try {
|
|
12326
|
+
for (const file of files) safeRestoreWrite(join41(destination, file.relativePath), file.bytes, file.mode, projectConfig);
|
|
12327
|
+
for (const file of hashes) {
|
|
12328
|
+
if (createHash8("sha256").update(readFileSync30(file.path)).digest("hex") !== file.sha256) throw new UserError("Activation read-back hash mismatch.");
|
|
12329
|
+
}
|
|
12330
|
+
} catch (error) {
|
|
12331
|
+
throw new UserError(`Activation write failed; backup ${backup.id} records recovery paths. Inspect before using rollback --project --force: ${error.message}`);
|
|
12332
|
+
}
|
|
12333
|
+
emit2(options.json, {
|
|
12334
|
+
...preview,
|
|
12335
|
+
activated: true,
|
|
12336
|
+
backupId: backup.id,
|
|
12337
|
+
runtimeVerified: false,
|
|
12338
|
+
verification: `Open a fresh ${options.host} task in this project, invoke ${preview.invocation}, and verify it reads ${join41(destination, "SKILL.md")}.`,
|
|
12339
|
+
rollback: `skill-forge refine rollback ${backup.id} --project --yes`
|
|
12340
|
+
}, () => {
|
|
12341
|
+
console.log(`Activated ${destination}. Invoke ${preview.invocation} in a fresh ${options.host} task and verify the loaded file; runtime use is not yet observed.`);
|
|
12342
|
+
console.log(`Rollback from this project: skill-forge refine rollback ${backup.id} --project --yes`);
|
|
12343
|
+
});
|
|
12344
|
+
} finally {
|
|
12345
|
+
rmSync9(stage, { recursive: true, force: true });
|
|
12346
|
+
}
|
|
12347
|
+
}
|
|
11722
12348
|
async function refineListCommand(options = {}) {
|
|
11723
12349
|
requireProFeature3();
|
|
11724
12350
|
const { records } = readRefinementsStore();
|
|
@@ -11812,16 +12438,16 @@ async function refineWhichCommand(skill, options = {}) {
|
|
|
11812
12438
|
}
|
|
11813
12439
|
function writePostWriteSidecar(backupId, files) {
|
|
11814
12440
|
const sidecar = { schemaVersion: 1, files };
|
|
11815
|
-
const dir =
|
|
12441
|
+
const dir = join41(getBackupsDir(), backupId);
|
|
11816
12442
|
mkdirSync11(dir, { recursive: true });
|
|
11817
|
-
writeFileSync10(
|
|
12443
|
+
writeFileSync10(join41(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
|
|
11818
12444
|
}
|
|
11819
12445
|
function readPostWriteSidecar(backupId) {
|
|
11820
|
-
const path =
|
|
11821
|
-
if (!
|
|
12446
|
+
const path = join41(getBackupsDir(), backupId, "post.json");
|
|
12447
|
+
if (!existsSync31(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
|
|
11822
12448
|
let raw;
|
|
11823
12449
|
try {
|
|
11824
|
-
raw =
|
|
12450
|
+
raw = readFileSync30(path, "utf-8");
|
|
11825
12451
|
} catch {
|
|
11826
12452
|
return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
|
|
11827
12453
|
}
|
|
@@ -11866,21 +12492,21 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
11866
12492
|
const resolvedUserRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
11867
12493
|
let realUserRoot;
|
|
11868
12494
|
try {
|
|
11869
|
-
realUserRoot =
|
|
12495
|
+
realUserRoot = realpathSync14(resolvedUserRoot);
|
|
11870
12496
|
} catch {
|
|
11871
12497
|
realUserRoot = resolvedUserRoot;
|
|
11872
12498
|
}
|
|
11873
12499
|
let realSkillDir;
|
|
11874
12500
|
try {
|
|
11875
|
-
realSkillDir =
|
|
12501
|
+
realSkillDir = realpathSync14(skillDir);
|
|
11876
12502
|
} catch {
|
|
11877
12503
|
realSkillDir = resolvePath15(skillDir);
|
|
11878
12504
|
}
|
|
11879
12505
|
if (!isUnderRoot3(realSkillDir, realUserRoot)) {
|
|
11880
12506
|
throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
|
|
11881
12507
|
}
|
|
11882
|
-
const skillMdPath =
|
|
11883
|
-
if (!
|
|
12508
|
+
const skillMdPath = join41(skillDir, "SKILL.md");
|
|
12509
|
+
if (!existsSync31(skillMdPath)) {
|
|
11884
12510
|
throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
|
|
11885
12511
|
}
|
|
11886
12512
|
const refinementsStore = readRefinementsStore();
|
|
@@ -11897,7 +12523,7 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
11897
12523
|
`pattern ${patternId}'s representative refinement is a '${representative.overrideType}' override \u2014 promote currently only materializes patch/extend/new-type patterns against the user-scope SKILL.md; other override types must be merged manually.`
|
|
11898
12524
|
);
|
|
11899
12525
|
}
|
|
11900
|
-
const before =
|
|
12526
|
+
const before = readFileSync30(skillMdPath, "utf-8");
|
|
11901
12527
|
const after = applyPatchBlocks(before, [block]);
|
|
11902
12528
|
const diff = previewDiff(before, after, skillMdPath);
|
|
11903
12529
|
if (options.dryRun) {
|
|
@@ -11929,7 +12555,7 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
11929
12555
|
throw err;
|
|
11930
12556
|
}
|
|
11931
12557
|
try {
|
|
11932
|
-
const postSha256 = createHash8("sha256").update(
|
|
12558
|
+
const postSha256 = createHash8("sha256").update(readFileSync30(skillMdPath)).digest("hex");
|
|
11933
12559
|
writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
|
|
11934
12560
|
markGeneralized(patternId, backup.id);
|
|
11935
12561
|
const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
@@ -11955,11 +12581,11 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
11955
12581
|
}
|
|
11956
12582
|
function assertBackupDirContained(backupId) {
|
|
11957
12583
|
const backupsDir = getBackupsDir();
|
|
11958
|
-
if (!
|
|
11959
|
-
const realBackupsDir =
|
|
11960
|
-
const backupDir =
|
|
11961
|
-
if (!
|
|
11962
|
-
const realBackupDir =
|
|
12584
|
+
if (!existsSync31(backupsDir)) return;
|
|
12585
|
+
const realBackupsDir = realpathSync14(backupsDir);
|
|
12586
|
+
const backupDir = join41(backupsDir, backupId);
|
|
12587
|
+
if (!existsSync31(backupDir)) return;
|
|
12588
|
+
const realBackupDir = realpathSync14(backupDir);
|
|
11963
12589
|
if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
|
|
11964
12590
|
throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
|
|
11965
12591
|
}
|
|
@@ -11968,9 +12594,9 @@ function resolveTargetRealPath(path) {
|
|
|
11968
12594
|
let lexicalAncestor = resolvePath15(path);
|
|
11969
12595
|
while (true) {
|
|
11970
12596
|
try {
|
|
11971
|
-
const real =
|
|
11972
|
-
const remainder =
|
|
11973
|
-
return remainder === "" ? real :
|
|
12597
|
+
const real = realpathSync14(lexicalAncestor);
|
|
12598
|
+
const remainder = relative10(lexicalAncestor, resolvePath15(path));
|
|
12599
|
+
return remainder === "" ? real : join41(real, remainder);
|
|
11974
12600
|
} catch {
|
|
11975
12601
|
const parent = dirname15(lexicalAncestor);
|
|
11976
12602
|
if (parent === lexicalAncestor) return resolvePath15(path);
|
|
@@ -11985,7 +12611,7 @@ function assertManifestPathsWithinRoots(manifest, config) {
|
|
|
11985
12611
|
const roots = config.skillsRoots.map((root) => {
|
|
11986
12612
|
const resolved = resolveConfiguredRoot3(root);
|
|
11987
12613
|
try {
|
|
11988
|
-
return
|
|
12614
|
+
return realpathSync14(resolved);
|
|
11989
12615
|
} catch {
|
|
11990
12616
|
return resolved;
|
|
11991
12617
|
}
|
|
@@ -12002,7 +12628,7 @@ function assertNoSymlinkLeaves(manifest) {
|
|
|
12002
12628
|
for (const file of manifest.files) {
|
|
12003
12629
|
let lst;
|
|
12004
12630
|
try {
|
|
12005
|
-
lst =
|
|
12631
|
+
lst = lstatSync8(file.path);
|
|
12006
12632
|
} catch {
|
|
12007
12633
|
lst = void 0;
|
|
12008
12634
|
}
|
|
@@ -12015,11 +12641,11 @@ function findLexicalRoot(filePath, config) {
|
|
|
12015
12641
|
const lexicalTarget = resolvePath15(filePath);
|
|
12016
12642
|
for (const root of config.skillsRoots) {
|
|
12017
12643
|
const rootLexical = resolveConfiguredRoot3(root);
|
|
12018
|
-
const rel =
|
|
12019
|
-
if (rel !== "" && (rel === ".." || rel.startsWith(`..${
|
|
12644
|
+
const rel = relative10(rootLexical, lexicalTarget);
|
|
12645
|
+
if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep10}`) || isAbsolute11(rel))) continue;
|
|
12020
12646
|
let rootReal;
|
|
12021
12647
|
try {
|
|
12022
|
-
rootReal =
|
|
12648
|
+
rootReal = realpathSync14(rootLexical);
|
|
12023
12649
|
} catch {
|
|
12024
12650
|
rootReal = rootLexical;
|
|
12025
12651
|
}
|
|
@@ -12029,8 +12655,8 @@ function findLexicalRoot(filePath, config) {
|
|
|
12029
12655
|
}
|
|
12030
12656
|
function walkRealComponents(filePath, config, createMissing) {
|
|
12031
12657
|
const { rootLexical, rootReal } = findLexicalRoot(filePath, config);
|
|
12032
|
-
const relPath =
|
|
12033
|
-
const components = relPath.split(
|
|
12658
|
+
const relPath = relative10(rootLexical, resolvePath15(filePath));
|
|
12659
|
+
const components = relPath.split(sep10).filter((c) => c.length > 0);
|
|
12034
12660
|
if (components.length === 0) {
|
|
12035
12661
|
throw new UserError(`refusing to operate on ${filePath}: resolves to the skills root itself`);
|
|
12036
12662
|
}
|
|
@@ -12039,10 +12665,10 @@ function walkRealComponents(filePath, config, createMissing) {
|
|
|
12039
12665
|
let current = rootReal;
|
|
12040
12666
|
for (const component of dirComponents) {
|
|
12041
12667
|
if (current === void 0) break;
|
|
12042
|
-
const next =
|
|
12668
|
+
const next = join41(current, component);
|
|
12043
12669
|
let lst;
|
|
12044
12670
|
try {
|
|
12045
|
-
lst =
|
|
12671
|
+
lst = lstatSync8(next);
|
|
12046
12672
|
} catch {
|
|
12047
12673
|
lst = void 0;
|
|
12048
12674
|
}
|
|
@@ -12070,13 +12696,13 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
12070
12696
|
if (parentReal === void 0) {
|
|
12071
12697
|
throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
|
|
12072
12698
|
}
|
|
12073
|
-
const finalPath =
|
|
12699
|
+
const finalPath = join41(parentReal, leafComponent);
|
|
12074
12700
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
12075
12701
|
throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
|
|
12076
12702
|
}
|
|
12077
12703
|
let leafLst;
|
|
12078
12704
|
try {
|
|
12079
|
-
leafLst =
|
|
12705
|
+
leafLst = lstatSync8(finalPath);
|
|
12080
12706
|
} catch {
|
|
12081
12707
|
leafLst = void 0;
|
|
12082
12708
|
}
|
|
@@ -12096,13 +12722,13 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
12096
12722
|
function safeDeleteTombstone(filePath, config) {
|
|
12097
12723
|
const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
|
|
12098
12724
|
if (parentReal === void 0) return;
|
|
12099
|
-
const finalPath =
|
|
12725
|
+
const finalPath = join41(parentReal, leafComponent);
|
|
12100
12726
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
12101
12727
|
throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
|
|
12102
12728
|
}
|
|
12103
12729
|
let leafLst;
|
|
12104
12730
|
try {
|
|
12105
|
-
leafLst =
|
|
12731
|
+
leafLst = lstatSync8(finalPath);
|
|
12106
12732
|
} catch {
|
|
12107
12733
|
leafLst = void 0;
|
|
12108
12734
|
}
|
|
@@ -12121,6 +12747,13 @@ async function refineRollbackCommand(backupId, options = {}) {
|
|
|
12121
12747
|
assertBackupDirContained(backupId);
|
|
12122
12748
|
const manifest = readBackupManifest(backupId);
|
|
12123
12749
|
const config = loadConfig();
|
|
12750
|
+
if (options.project) config.skillsRoots = Object.values(PROJECT_SKILL_ROOTS).map((p) => join41(realpathSync14(process.cwd()), p));
|
|
12751
|
+
const assertProjectRoots = () => {
|
|
12752
|
+
if (options.project && config.skillsRoots.some((root) => resolveTargetRealPath(root) !== root)) {
|
|
12753
|
+
throw new UserError("refusing project rollback: a project skill root or its ancestor is symlinked.");
|
|
12754
|
+
}
|
|
12755
|
+
};
|
|
12756
|
+
assertProjectRoots();
|
|
12124
12757
|
assertManifestPathsWithinRoots(manifest, config);
|
|
12125
12758
|
assertNoSymlinkLeaves(manifest);
|
|
12126
12759
|
const warnings = [];
|
|
@@ -12134,11 +12767,14 @@ async function refineRollbackCommand(backupId, options = {}) {
|
|
|
12134
12767
|
}
|
|
12135
12768
|
warnings.push(`post-write verification data for backup ${backupId} is ${reason} \u2014 restoring without hash verification (--force).`);
|
|
12136
12769
|
}
|
|
12770
|
+
if (sidecar.status === "ok" && manifest.files.some((f) => !sidecar.hashes.has(f.path)) && !options.force) {
|
|
12771
|
+
throw new UserError("refusing rollback: post-write verification is incomplete for the backup manifest.");
|
|
12772
|
+
}
|
|
12137
12773
|
const mismatches = [];
|
|
12138
12774
|
for (const file of manifest.files) {
|
|
12139
12775
|
const expected = sidecar.hashes.get(file.path);
|
|
12140
12776
|
if (expected === void 0) continue;
|
|
12141
|
-
const currentSha256 =
|
|
12777
|
+
const currentSha256 = existsSync31(file.path) ? createHash8("sha256").update(readFileSync30(file.path)).digest("hex") : null;
|
|
12142
12778
|
if (currentSha256 !== expected) mismatches.push(file.path);
|
|
12143
12779
|
}
|
|
12144
12780
|
if (mismatches.length > 0 && !options.force) {
|
|
@@ -12165,6 +12801,7 @@ ${fileList}
|
|
|
12165
12801
|
const restored = [];
|
|
12166
12802
|
for (const file of manifest.files) {
|
|
12167
12803
|
try {
|
|
12804
|
+
assertProjectRoots();
|
|
12168
12805
|
if (!file.existed) {
|
|
12169
12806
|
safeDeleteTombstone(file.path, config);
|
|
12170
12807
|
} else {
|
|
@@ -12178,6 +12815,18 @@ ${fileList}
|
|
|
12178
12815
|
);
|
|
12179
12816
|
}
|
|
12180
12817
|
}
|
|
12818
|
+
if (options.project) {
|
|
12819
|
+
const roots = new Set(config.skillsRoots);
|
|
12820
|
+
for (const file of manifest.files) {
|
|
12821
|
+
for (let dir = dirname15(file.path); !roots.has(dir) && dirname15(dir) !== dir; dir = dirname15(dir)) {
|
|
12822
|
+
try {
|
|
12823
|
+
rmdirSync(dir);
|
|
12824
|
+
} catch {
|
|
12825
|
+
break;
|
|
12826
|
+
}
|
|
12827
|
+
}
|
|
12828
|
+
}
|
|
12829
|
+
}
|
|
12181
12830
|
const patternsStore = readPatternsStore();
|
|
12182
12831
|
const pattern = patternsStore.records.find((p) => p.generalizedBackupId === backupId);
|
|
12183
12832
|
if (pattern) {
|
|
@@ -12283,7 +12932,7 @@ configCmd.command("propose <key> <value>").description("Queue a preference for r
|
|
|
12283
12932
|
configCmd.command("review").description("Review pending agent-proposed preferences one at a time").option("--accept-all", "accept every pending proposal without prompting").option("--reject-all", "reject every pending proposal without prompting").action(configReview);
|
|
12284
12933
|
program.command("guide [topic]").description("What this tool does, what state you are in, and the next command to run").action(guide);
|
|
12285
12934
|
program.command("evolve <target-skill-dir>").description("Self-evolve a skill via SkillOpt-Sleep, re-gate the proposal, then promote/hold/reject (Pro)").option("--project <dir>", "project dir passed to SkillOpt-Sleep as --project (defaults to cwd)").option("--dry-run", "SkillOpt-Sleep dry-run: report only, nothing staged").option("--backend <name>", "SkillOpt-Sleep backend (default: mock, offline \u2014 a real backend requires --yes or interactive confirmation)").option("--lookback-hours <n>", "hours of session history for SkillOpt-Sleep to harvest", parseLookbackHours).option("-y, --yes", "skip interactive prompts; honor the re-gate verdict automatically").option("--json", "print the re-gate result as JSON instead of the terminal report").option("--force", "allow re-adopting a staging dir whose proposed content already matches the live skill").action(evolveCommand);
|
|
12286
|
-
program.command("audit").alias("doctor").description("Doctor-style health check over the configured skill/MCP set: inventory, hygiene findings, and consolidation/refinement opportunities").option("--json", "print the audit report as JSON instead of the terminal summary (non-interactive: skips the business-profile prompt)").option("-y, --yes", "skip interactive prompts (business profile, foundation, handoff); never implies --foundation or --handoff").option("--report <file>", "write the report to this path instead of the default ~/.skill-forge/reports/audit-<timestamp>.md (refuses an existing path)").option("--foundation", "authorizes exactly one skills-root write: scaffold a business-foundation skill from the captured business profile").option("--handoff", "authorizes launching the configured agent with the bundled curation prompt against the written report").option("--claude-plugins", "include enabled Claude Code plugins' declared skills in this audit (default: config.claudePlugins, else off)").option("--no-claude-plugins", "exclude enabled Claude Code plugins from this audit even when config.claudePlugins is on").option("--usage-snapshot <file>", "join per-skill usage counts from a skill-monitor snapshot JSON file or a directory of them (default: config.usageSnapshotPath)").option("--usage-window <days>", "restrict directory-input usage-snapshot selection to a snapshot with this window_days (integer)", parseUsageWindow).action(auditCommand);
|
|
12935
|
+
program.command("audit").option("--compare <file>", "compare with a prior full audit JSON; preserve all current findings").alias("doctor").description("Doctor-style health check over the configured skill/MCP set: inventory, hygiene findings, and consolidation/refinement opportunities").option("--json", "print the audit report as JSON instead of the terminal summary (non-interactive: skips the business-profile prompt)").option("-y, --yes", "skip interactive prompts (business profile, foundation, handoff); never implies --foundation or --handoff").option("--report <file>", "write the report to this path instead of the default ~/.skill-forge/reports/audit-<timestamp>.md (refuses an existing path)").option("--foundation", "authorizes exactly one skills-root write: scaffold a business-foundation skill from the captured business profile").option("--handoff", "authorizes launching the configured agent with the bundled curation prompt against the written report").option("--plugin-inventory <file>", "import a static host inventory JSON; audit one host at a time").option("--claude-plugins", "include enabled Claude Code plugins' declared skills in this audit (default: config.claudePlugins, else off)").option("--no-claude-plugins", "exclude enabled Claude Code plugins from this audit even when config.claudePlugins is on").option("--usage-snapshot <file>", "join per-skill usage counts from a skill-monitor snapshot JSON file or a directory of them (default: config.usageSnapshotPath)").option("--usage-window <days>", "restrict directory-input usage-snapshot selection to a snapshot with this window_days (integer)", parseUsageWindow).action(auditCommand);
|
|
12287
12936
|
var finding = program.command("finding").description("Review and acknowledge audit findings \u2014 human-only, never a gate input (see docs/accepted-findings-schema.md)");
|
|
12288
12937
|
finding.command("accept <fingerprint-prefix>").description("Accept a currently-active LOW/MEDIUM finding (no override for HIGH/CRITICAL); re-runs the audit engine to resolve it").requiredOption("--reason <text>", "why this finding is accepted (mandatory, 1-200 chars, no control characters)").option("--json", "print the accepted record as JSON").action(findingAcceptCommand);
|
|
12289
12938
|
finding.command("revoke <fingerprint-prefix>").description("Remove a stored acceptance so the finding reports again").option("--json", "print the removed record as JSON").action(findingRevokeCommand);
|
|
@@ -12294,7 +12943,7 @@ program.command("watch").description("Drift check across every SOURCES.md proven
|
|
|
12294
12943
|
"--skill-map <path>",
|
|
12295
12944
|
"also drift-check every fork-of edge in a generated rhize-plugins skill map (contentHash compare against upstream)"
|
|
12296
12945
|
).action(watchCommand);
|
|
12297
|
-
program.command("routine").description("One scheduled maintenance pass: audit + drift + registry, one report, cron-friendly exit codes (Pro)").option("--json", "print the routine result as JSON instead of the terminal summary").option("--offline", "skip every network check (drift entries are reported unchecked)").option("--skip-watch", "skip the drift check").option("--skip-organize", "skip the capability registry").option("--fail-on <level>", "exit 1 when a finding at or above this severity exists: none|low|medium|high|critical|any (default none)", parseFailOn).option("--housekeeping", "opt in to bounded writes: prune expired quarantine sandboxes, close orphaned queue entries").option("--prune-days <n>", "quarantine age threshold in days for --housekeeping (default 30)", parsePruneDays).option("--dry-run", "with --housekeeping: report what would change, change nothing").option("--handoff", "hand the written report to the configured agent (curation prompt)").option("--report <file>", "write the report here instead of ~/.skill-forge/reports/routine-<timestamp>.md (refuses an existing path)").option("--claude-plugins", "include enabled Claude Code plugins' declared skills in the audit step (default: config.claudePlugins, else off)").option("--no-claude-plugins", "exclude enabled Claude Code plugins from the audit step even when config.claudePlugins is on").option("--usage-snapshot <file>", "join per-skill usage counts from a skill-monitor snapshot JSON file or a directory of them (default: config.usageSnapshotPath)").option("--usage-window <days>", "restrict directory-input usage-snapshot selection to a snapshot with this window_days (integer)", parseUsageWindow).action(routineCommand);
|
|
12946
|
+
program.command("routine").description("One scheduled maintenance pass: audit + drift + registry, one report, cron-friendly exit codes (Pro)").option("--json", "print the routine result as JSON instead of the terminal summary").option("--offline", "skip every network check (drift entries are reported unchecked)").option("--skip-watch", "skip the drift check").option("--skip-organize", "skip the capability registry").option("--fail-on <level>", "exit 1 when a finding at or above this severity exists: none|low|medium|high|critical|any (default none)", parseFailOn).option("--fail-scope <scope>", "findings used by --fail-on: configured|all (default configured; all includes plugin findings)").option("--housekeeping", "opt in to bounded writes: prune expired quarantine sandboxes, close orphaned queue entries").option("--prune-days <n>", "quarantine age threshold in days for --housekeeping (default 30)", parsePruneDays).option("--dry-run", "with --housekeeping: report what would change, change nothing").option("--handoff", "hand the written report to the configured agent (curation prompt)").option("--report <file>", "write the report here instead of ~/.skill-forge/reports/routine-<timestamp>.md (refuses an existing path)").option("--plugin-inventory <file>", "import a static host inventory JSON; audit one host at a time").option("--claude-plugins", "include enabled Claude Code plugins' declared skills in the audit step (default: config.claudePlugins, else off)").option("--no-claude-plugins", "exclude enabled Claude Code plugins from the audit step even when config.claudePlugins is on").option("--usage-snapshot <file>", "join per-skill usage counts from a skill-monitor snapshot JSON file or a directory of them (default: config.usageSnapshotPath)").option("--usage-window <days>", "restrict directory-input usage-snapshot selection to a snapshot with this window_days (integer)", parseUsageWindow).action(routineCommand);
|
|
12298
12947
|
program.command("ingest").description(
|
|
12299
12948
|
"Hand the pending-ingestion queue to the configured coding agent for the decide/absorb pass (Pro). For a single new source, use `skill-forge add <source> --ingest` instead."
|
|
12300
12949
|
).option("--list", "print pending queue entries and exit \u2014 no handoff").option("--json", "with --list: print pending entries as JSON").action(ingest);
|
|
@@ -12308,13 +12957,18 @@ insight.command("approve <study-id>").description("Human-only TTY approval of th
|
|
|
12308
12957
|
insight.command("jira <study-id>").description("Generate a Jira manifest or manage its private local receipts; never calls Jira").option("--project <key>", "destination Jira project key recorded in a new manifest").option("--out <file>", "write an additional export-safe manifest here (refuses an existing file)").option("--handoff", "launch the configured agent to present and confirm the backlog before Jira writes").option("--receipt <file>", "append one validated local receipt for the current manifest").option("--resume", "show local resume state for the current manifest").option("--json", "print the selected mode result as one JSON document").action(insightJira);
|
|
12309
12958
|
insight.command("status <study-id>").description("Show private study lifecycle and evidence counts; read-only").option("--json", "print the status payload as one JSON document").action(insightStatus);
|
|
12310
12959
|
insight.command("close <study-id>").description("Close a study while preserving its append-only receipts").requiredOption("--status <status>", "dismissed|archived", parseInsightCloseStatus).action(insightClose);
|
|
12960
|
+
var plugins = program.command("plugins").description("Explicit host-specific plugin retention decisions; never suppresses safety findings");
|
|
12961
|
+
plugins.command("keep <plugin-id>").requiredOption("--host <host>", "claude|codex").requiredOption("--category <category>", "essential|project-specific|intentionally-infrequent").requiredOption("--reason <text>", "why this capability should be retained").option("--project <path>", "bind the decision to an existing project").option("--json", "print JSON").action(pluginsKeepCommand);
|
|
12962
|
+
plugins.command("list").option("--json", "print JSON").action(pluginsListCommand);
|
|
12963
|
+
plugins.command("forget <plugin-id>").requiredOption("--host <host>", "claude|codex").option("--project <path>", "remove the decision for this project only").option("--json", "print JSON").action(pluginsForgetCommand);
|
|
12311
12964
|
var refine = program.command("refine").description("Capture, apply, and generalize improvements to installed skills from usage feedback (Pro)");
|
|
12312
12965
|
refine.command("capture", { isDefault: true }).description("Record a refinement and write a project-scope override artifact (never touches the base skill)").option("--skill <name>", "target skill name").option("--category <cat>", "trigger|content|hook|tool|pattern|config|new").option("--target <section>", 'heading path or file the refinement targets, e.g. "Hooks > duplicate-check"').option("--override-type <type>", "patch|extend|config|full|hook|script|new").option("--action <action>", "patch action: append|prepend|replace-section|insert-after|insert-before|delete-section").option("--marker <text>", "marker text for insert-* / replace-section / delete-section (filename for full|hook|script)").option("--content <text>", "override content inline").option("--content-file <file>", "read override content from a file").option("--expected <text>", "expected behavior").option("--actual <text>", "actual observed behavior").option("--example <text>", "concrete example of the gap").option("--outcome <text>", "desired outcome").option("--root-cause <text>", "root cause per the gap analysis").option("--pattern-id <id>", "join an existing pattern (PAT-xxxx) instead of fingerprint matching").option("--scope <scope>", 'where to write the artifact: "local" (.claude/skills, default) or "shared" (./skills)').option("--dry-run", "preview the artifact diff; write nothing").option("--json", "print the result as JSON").option("-y, --yes", "skip the interactive confirm; never implies --handoff").option("--handoff", "authorizes launching the configured agent with the bundled refine prompt").action(refineCaptureCommand);
|
|
12313
12966
|
refine.command("list").description("Show refinement history from the user-scope store").option("--status <status>", "filter by refinement status").option("--skill <name>", "filter by skill").option("--project <name>", "filter by project").option("--json", "print records as JSON").action(refineListCommand);
|
|
12314
12967
|
refine.command("patterns").description("Review tracked patterns and the generalization queue").option("--status <status>", "filter: tracking|ready|generalized|dismissed").option("--skill <name>", "filter by affected skill").option("--project <name>", "filter by project").option("--json", "print patterns as JSON").action(refinePatternsCommand);
|
|
12968
|
+
refine.command("activate").description("Materialize a captured patch as a new project skill; never edits a plugin cache").requiredOption("--skill <name>", "captured skill directory name").requiredOption("--base <dir>", "explicit base skill directory, read-only").requiredOption("--as <name>", "unique project skill name").requiredOption("--host <host>", "claude|codex").option("--scope <scope>", "capture source local|shared (default local)").option("--dry-run", "compose and safety-scan without activating").option("--json", "print one JSON result").option("-y, --yes", "confirm activation").action(refineActivateCommand);
|
|
12315
12969
|
refine.command("which <skill>").description("Print override-scope resolution order and which override files exist at each scope").option("--user-root <dir>", "explicit user-scope skills root").option("--json", "print the resolution as JSON").action(refineWhichCommand);
|
|
12316
12970
|
refine.command("promote <pattern-id>").description("Materialize a READY pattern into the user-scope skill default (backup + manifest first)").option("--force", "promote a pattern that is not READY").option("--dry-run", "preview the user-scope diff; write nothing").option("--user-root <dir>", "explicit user-scope skills root").option("--json", "print the result as JSON").option("-y, --yes", "skip the interactive confirm").action(refinePromoteCommand);
|
|
12317
|
-
refine.command("rollback <backup-id>").description("Restore a promotion backup (refuses if files changed since promotion unless --force)").option("--force", "restore even if current files differ from the post-promotion state").option("--json", "print the result as JSON").option("-y, --yes", "skip the interactive confirm").action(refineRollbackCommand);
|
|
12971
|
+
refine.command("rollback <backup-id>").option("--project", "restrict rollback to this project\u2019s .claude/skills and .agents/skills roots").description("Restore a promotion backup (refuses if files changed since promotion unless --force)").option("--force", "restore even if current files differ from the post-promotion state").option("--json", "print the result as JSON").option("-y, --yes", "skip the interactive confirm").action(refineRollbackCommand);
|
|
12318
12972
|
program.command("init").description("Detect installed agents and set gate targets, default promotion target, and handoff agent").option("--defaults", "non-interactive: use all detected agents as gate targets, the first as the default target").option("--list", "print detected agent skill roots and exit \u2014 no writes").option("--all-agents", "pre-select every detected root, not just those of agents whose CLI is on PATH").action(init);
|
|
12319
12973
|
try {
|
|
12320
12974
|
await program.parseAsync();
|