@csark0812/skeleton 4.0.1 → 5.0.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/README.md +11 -15
- package/dist/audit/config/types.d.ts +8 -5
- package/dist/audit/core/context.d.ts +5 -0
- package/dist/audit/core/repo-files.d.ts +5 -0
- package/dist/audit/core/report.d.ts +1 -0
- package/dist/audit/core/review-coverage.d.ts +18 -0
- package/dist/audit/rules/review-coverage.d.ts +8 -0
- package/dist/cli.js +540 -686
- package/dist/plugin-types.js +6 -0
- package/package.json +2 -2
- package/schemas/config.schema.json +18 -17
- package/templates/skeleton-init/config.yaml +0 -5
- package/templates/skeleton-init/pre-commit-config.yaml +9 -0
- package/templates/skeleton-init/skeleton.toml +6 -7
- package/dist/hooks/customize-on-skill-read.js +0 -14813
- package/templates/skeleton-init/claude-settings.fragment.json +0 -24
- package/templates/skeleton-init/codex-hooks.fragment.json +0 -15
- package/templates/skeleton-init/cursor-hooks.fragment.json +0 -11
package/dist/cli.js
CHANGED
|
@@ -15968,8 +15968,7 @@ var require_extend = __commonJS((exports, module) => {
|
|
|
15968
15968
|
});
|
|
15969
15969
|
|
|
15970
15970
|
// src/cli.ts
|
|
15971
|
-
import
|
|
15972
|
-
import process7 from "node:process";
|
|
15971
|
+
import process5 from "node:process";
|
|
15973
15972
|
|
|
15974
15973
|
// src/audit/config/load.ts
|
|
15975
15974
|
var import_ajv = __toESM(require_ajv(), 1);
|
|
@@ -16903,7 +16902,6 @@ var $visitAsync = visit.visitAsync;
|
|
|
16903
16902
|
|
|
16904
16903
|
// src/audit/core/shared.ts
|
|
16905
16904
|
var CATALOG_REL_PATH = ".skeleton/catalog.md";
|
|
16906
|
-
var REGISTRY_DIR_REL = ".skeleton";
|
|
16907
16905
|
var EXTERNAL_LINK_RE = /^(https?:|mailto:|#)/;
|
|
16908
16906
|
var DOC_META_RE = /<!--\s*doc-meta:\s*owner=[^|]+\|\s*last-reviewed=\d{4}-\d{2}-\d{2}\s*-->/;
|
|
16909
16907
|
var DOC_META_LAST_REVIEWED_RE = /last-reviewed=(\d{4}-\d{2}-\d{2})/;
|
|
@@ -17002,8 +17000,7 @@ function isDraftPlacementAllowed(relPath, draftPathPrefixes) {
|
|
|
17002
17000
|
// src/audit/config/load.ts
|
|
17003
17001
|
var SCHEMA_CANDIDATES = [
|
|
17004
17002
|
join(dirname(fileURLToPath(import.meta.url)), "../../../schemas/config.schema.json"),
|
|
17005
|
-
join(dirname(fileURLToPath(import.meta.url)), "../schemas/config.schema.json")
|
|
17006
|
-
join(dirname(fileURLToPath(import.meta.url)), "../../schemas/config.schema.json")
|
|
17003
|
+
join(dirname(fileURLToPath(import.meta.url)), "../schemas/config.schema.json")
|
|
17007
17004
|
];
|
|
17008
17005
|
var ROOT_CONFIG_TOML = "skeleton.toml";
|
|
17009
17006
|
var LEGACY_CONFIG_YAML = join(".skeleton", "config.yaml");
|
|
@@ -17083,9 +17080,6 @@ function nonPublicSkills(config) {
|
|
|
17083
17080
|
return config.scan.nonPublicSkills ?? [];
|
|
17084
17081
|
}
|
|
17085
17082
|
|
|
17086
|
-
// src/audit/run.ts
|
|
17087
|
-
import process4 from "node:process";
|
|
17088
|
-
|
|
17089
17083
|
// src/catalog.ts
|
|
17090
17084
|
import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync5, writeFileSync } from "node:fs";
|
|
17091
17085
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
@@ -18229,7 +18223,6 @@ function listOwnedSkillSlugs(index) {
|
|
|
18229
18223
|
|
|
18230
18224
|
// src/audit/core/collect.ts
|
|
18231
18225
|
var MARKDOWN_GLOBS = ["**/*.md", "**/*.mdc"];
|
|
18232
|
-
var BUILTIN_INCLUDE_PATTERNS = [".skeleton/customize/**"];
|
|
18233
18226
|
function isMarkdownFile(absPath) {
|
|
18234
18227
|
return absPath.endsWith(".md") || absPath.endsWith(".mdc");
|
|
18235
18228
|
}
|
|
@@ -18271,7 +18264,7 @@ function expandPatterns(root, patterns, exclude) {
|
|
|
18271
18264
|
}
|
|
18272
18265
|
function collectScanFiles(config, root, skillIndex) {
|
|
18273
18266
|
const exclude = mergedExcludes(config);
|
|
18274
|
-
const includePatterns = [...
|
|
18267
|
+
const includePatterns = [...config.scan.include];
|
|
18275
18268
|
if (skillIndex) {
|
|
18276
18269
|
includePatterns.push(...skillCollectAugments(skillIndex));
|
|
18277
18270
|
}
|
|
@@ -18571,19 +18564,11 @@ function writeCatalog(root) {
|
|
|
18571
18564
|
writeFileSync(abs, content, "utf8");
|
|
18572
18565
|
return { path: normalizeRelPath(CATALOG_REL_PATH), entries };
|
|
18573
18566
|
}
|
|
18574
|
-
function
|
|
18567
|
+
function refreshLocalCatalog(root) {
|
|
18575
18568
|
if (process3.env.CI === "true")
|
|
18576
|
-
return
|
|
18577
|
-
|
|
18578
|
-
|
|
18579
|
-
return [
|
|
18580
|
-
`${CATALOG_REL_PATH} missing — run \`skeleton catalog\` so agents can skim SSOT summaries`
|
|
18581
|
-
];
|
|
18582
|
-
}
|
|
18583
|
-
if (result.stale) {
|
|
18584
|
-
return [`${CATALOG_REL_PATH} outdated — run \`skeleton catalog\` to refresh`];
|
|
18585
|
-
}
|
|
18586
|
-
return [];
|
|
18569
|
+
return "skipped-ci";
|
|
18570
|
+
writeCatalog(root);
|
|
18571
|
+
return "current";
|
|
18587
18572
|
}
|
|
18588
18573
|
function runCatalogCli(options = {}) {
|
|
18589
18574
|
const root = options.root ?? findRepoRoot();
|
|
@@ -18617,8 +18602,7 @@ import { basename as basename2, dirname as dirname4, extname, join as join6 } fr
|
|
|
18617
18602
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
18618
18603
|
var SCHEMA_CANDIDATES2 = [
|
|
18619
18604
|
join6(dirname4(fileURLToPath2(import.meta.url)), "../../../schemas/policy-file.schema.json"),
|
|
18620
|
-
join6(dirname4(fileURLToPath2(import.meta.url)), "../schemas/policy-file.schema.json")
|
|
18621
|
-
join6(dirname4(fileURLToPath2(import.meta.url)), "../../schemas/policy-file.schema.json")
|
|
18605
|
+
join6(dirname4(fileURLToPath2(import.meta.url)), "../schemas/policy-file.schema.json")
|
|
18622
18606
|
];
|
|
18623
18607
|
function resolvePolicySchemaPath() {
|
|
18624
18608
|
for (const candidate of SCHEMA_CANDIDATES2) {
|
|
@@ -18945,7 +18929,8 @@ function createContext(options = {}) {
|
|
|
18945
18929
|
registryHasTableHeader: false,
|
|
18946
18930
|
skillIndex,
|
|
18947
18931
|
lockedSkillSlugs: new Set(skillIndex.foreignSlugs),
|
|
18948
|
-
policies: options.policies ?? []
|
|
18932
|
+
policies: options.policies ?? [],
|
|
18933
|
+
fileSource: options.fileSource
|
|
18949
18934
|
};
|
|
18950
18935
|
}
|
|
18951
18936
|
|
|
@@ -30900,9 +30885,8 @@ function printWarnings(label, warnings) {
|
|
|
30900
30885
|
return;
|
|
30901
30886
|
console.log(`${label} warnings:
|
|
30902
30887
|
`);
|
|
30903
|
-
for (const
|
|
30904
|
-
|
|
30905
|
-
console.log(`- ${i.file}${linkPart}: ${i.message}`);
|
|
30888
|
+
for (const item of warnings) {
|
|
30889
|
+
console.log(`${item.file}: warning: ${item.message}`);
|
|
30906
30890
|
}
|
|
30907
30891
|
console.log("");
|
|
30908
30892
|
}
|
|
@@ -30912,12 +30896,65 @@ function printSuccess(label, options, warnings) {
|
|
|
30912
30896
|
console.log(`${label} passed${countNote}.`);
|
|
30913
30897
|
return 0;
|
|
30914
30898
|
}
|
|
30899
|
+
var REREAD_CODES = new Set([
|
|
30900
|
+
"review-dependency-changed",
|
|
30901
|
+
"impacted-document-review-required",
|
|
30902
|
+
"review-document-changed",
|
|
30903
|
+
"review-dependency-set-changed"
|
|
30904
|
+
]);
|
|
30905
|
+
function isRereadIssue(item) {
|
|
30906
|
+
return Boolean(item.code && REREAD_CODES.has(item.code));
|
|
30907
|
+
}
|
|
30908
|
+
function rereadTrigger(item) {
|
|
30909
|
+
if (item.code === "review-document-changed")
|
|
30910
|
+
return item.file;
|
|
30911
|
+
if (item.code === "review-dependency-changed" || item.code === "impacted-document-review-required") {
|
|
30912
|
+
return item.link ?? null;
|
|
30913
|
+
}
|
|
30914
|
+
return null;
|
|
30915
|
+
}
|
|
30916
|
+
function orderTriggers(file, triggers) {
|
|
30917
|
+
const rest = [...triggers].filter((path3) => path3 !== file).sort();
|
|
30918
|
+
return triggers.has(file) ? [file, ...rest] : rest;
|
|
30919
|
+
}
|
|
30920
|
+
function collectRereadTriggers(errors2) {
|
|
30921
|
+
const triggers = new Map;
|
|
30922
|
+
const rest = [];
|
|
30923
|
+
for (const item of errors2) {
|
|
30924
|
+
if (!isRereadIssue(item)) {
|
|
30925
|
+
rest.push(item);
|
|
30926
|
+
continue;
|
|
30927
|
+
}
|
|
30928
|
+
const current = triggers.get(item.file) ?? new Set;
|
|
30929
|
+
const trigger = rereadTrigger(item);
|
|
30930
|
+
if (trigger)
|
|
30931
|
+
current.add(trigger);
|
|
30932
|
+
triggers.set(item.file, current);
|
|
30933
|
+
}
|
|
30934
|
+
return { triggers, rest };
|
|
30935
|
+
}
|
|
30936
|
+
function printRereadDiagnostic(file, triggers) {
|
|
30937
|
+
console.log(`${file}: error: review required`);
|
|
30938
|
+
const changed = orderTriggers(file, triggers);
|
|
30939
|
+
if (changed.length > 0) {
|
|
30940
|
+
console.log(` changed: ${changed.join(", ")}`);
|
|
30941
|
+
return;
|
|
30942
|
+
}
|
|
30943
|
+
console.log(" changed: review dependency set");
|
|
30944
|
+
}
|
|
30945
|
+
function printRereadLists(errors2) {
|
|
30946
|
+
const { triggers, rest } = collectRereadTriggers(errors2);
|
|
30947
|
+
for (const file of [...triggers.keys()].sort()) {
|
|
30948
|
+
printRereadDiagnostic(file, triggers.get(file) ?? new Set);
|
|
30949
|
+
}
|
|
30950
|
+
return rest;
|
|
30951
|
+
}
|
|
30915
30952
|
function printErrors(label, errors2) {
|
|
30916
30953
|
console.log(`${label} failed:
|
|
30917
30954
|
`);
|
|
30918
|
-
|
|
30919
|
-
|
|
30920
|
-
console.log(
|
|
30955
|
+
const rest = printRereadLists(errors2);
|
|
30956
|
+
for (const item of rest) {
|
|
30957
|
+
console.log(`${item.file}: error: ${item.message}`);
|
|
30921
30958
|
}
|
|
30922
30959
|
return 1;
|
|
30923
30960
|
}
|
|
@@ -30931,11 +30968,43 @@ function printTextReport(ctx) {
|
|
|
30931
30968
|
|
|
30932
30969
|
// src/audit/core/review-proof.ts
|
|
30933
30970
|
import { createHash } from "node:crypto";
|
|
30934
|
-
import { existsSync as
|
|
30935
|
-
import { dirname as dirname8
|
|
30971
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync2, readFileSync as readFileSync12, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
30972
|
+
import { dirname as dirname8 } from "node:path";
|
|
30973
|
+
|
|
30974
|
+
// src/audit/core/repo-files.ts
|
|
30975
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
30976
|
+
import { existsSync as existsSync12, readFileSync as readFileSync11 } from "node:fs";
|
|
30977
|
+
import { join as join11 } from "node:path";
|
|
30978
|
+
function gitShow(root2, spec) {
|
|
30979
|
+
const proc = spawnSync2("git", ["show", spec], {
|
|
30980
|
+
cwd: root2,
|
|
30981
|
+
encoding: "utf8",
|
|
30982
|
+
maxBuffer: 20000000
|
|
30983
|
+
});
|
|
30984
|
+
if (proc.status !== 0)
|
|
30985
|
+
return null;
|
|
30986
|
+
return proc.stdout;
|
|
30987
|
+
}
|
|
30988
|
+
function readRepoText(root2, relPath2, source = "worktree") {
|
|
30989
|
+
const normalized = normalizeRelPath(relPath2);
|
|
30990
|
+
if (source === "index")
|
|
30991
|
+
return gitShow(root2, `:${normalized}`);
|
|
30992
|
+
const abs = join11(root2, normalized);
|
|
30993
|
+
if (!existsSync12(abs))
|
|
30994
|
+
return null;
|
|
30995
|
+
return readFileSync11(abs, "utf8");
|
|
30996
|
+
}
|
|
30997
|
+
function pathDiffersFromHead(root2, relPath2) {
|
|
30998
|
+
const normalized = normalizeRelPath(relPath2);
|
|
30999
|
+
const head = gitShow(root2, `HEAD:${normalized}`);
|
|
31000
|
+
const worktree = readRepoText(root2, normalized, "worktree");
|
|
31001
|
+
if (worktree === null)
|
|
31002
|
+
return head !== null;
|
|
31003
|
+
return worktree !== (head ?? "");
|
|
31004
|
+
}
|
|
30936
31005
|
|
|
30937
31006
|
// src/audit/core/review-deps.ts
|
|
30938
|
-
import { existsSync as
|
|
31007
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
30939
31008
|
var REVIEW_DEPS_RE = /<!--\s*review-deps:\s*([^>]*?)-->/gi;
|
|
30940
31009
|
var GLOB_MAGIC_RE = /[*?{[]/;
|
|
30941
31010
|
function parseReviewDepsMarkers(content3) {
|
|
@@ -30967,7 +31036,7 @@ function resolveReviewDependencies(root2, patterns) {
|
|
|
30967
31036
|
throw new Error(`Invalid review dependency path: ${pattern}`);
|
|
30968
31037
|
}
|
|
30969
31038
|
if (!isReviewDependencyGlob(pattern)) {
|
|
30970
|
-
if (!
|
|
31039
|
+
if (!existsSync13(`${root2}/${pattern}`)) {
|
|
30971
31040
|
throw new Error(`Review dependency path is missing: ${pattern}`);
|
|
30972
31041
|
}
|
|
30973
31042
|
targets.add(pattern);
|
|
@@ -31059,19 +31128,19 @@ function parseLock(content3) {
|
|
|
31059
31128
|
return null;
|
|
31060
31129
|
}
|
|
31061
31130
|
}
|
|
31062
|
-
function loadLock(root2, relPath2) {
|
|
31063
|
-
const
|
|
31064
|
-
if (
|
|
31131
|
+
function loadLock(root2, relPath2, source = "worktree") {
|
|
31132
|
+
const content3 = readRepoText(root2, relPath2, source);
|
|
31133
|
+
if (content3 === null)
|
|
31065
31134
|
return null;
|
|
31066
|
-
return parseLock(
|
|
31135
|
+
return parseLock(content3);
|
|
31067
31136
|
}
|
|
31068
31137
|
function hashDependencies(root2, targets) {
|
|
31069
31138
|
const out = {};
|
|
31070
31139
|
for (const target of targets) {
|
|
31071
31140
|
const abs = resolveWritePath(root2, target);
|
|
31072
|
-
if (!
|
|
31141
|
+
if (!existsSync14(abs))
|
|
31073
31142
|
throw new Error(`Cannot attest missing review dependency: ${target}`);
|
|
31074
|
-
out[target] = hash(
|
|
31143
|
+
out[target] = hash(readFileSync12(abs, "utf8"));
|
|
31075
31144
|
}
|
|
31076
31145
|
return out;
|
|
31077
31146
|
}
|
|
@@ -31100,7 +31169,7 @@ function commitWrites(root2, writes) {
|
|
|
31100
31169
|
try {
|
|
31101
31170
|
for (const [relPath2, content3] of writes) {
|
|
31102
31171
|
const abs = resolveWritePath(root2, relPath2);
|
|
31103
|
-
originals.set(relPath2,
|
|
31172
|
+
originals.set(relPath2, existsSync14(abs) ? readFileSync12(abs, "utf8") : null);
|
|
31104
31173
|
mkdirSync2(dirname8(abs), { recursive: true });
|
|
31105
31174
|
writeFileSync3(abs, content3, "utf8");
|
|
31106
31175
|
written.push(relPath2);
|
|
@@ -31131,7 +31200,7 @@ function attestDocuments(options) {
|
|
|
31131
31200
|
if (proofPath) {
|
|
31132
31201
|
const absProof = resolveWritePath(ctx.root, proofPath);
|
|
31133
31202
|
lock = loadLock(ctx.root, proofPath);
|
|
31134
|
-
if (
|
|
31203
|
+
if (existsSync14(absProof) && !lock) {
|
|
31135
31204
|
throw new Error(`Review proof lockfile is malformed: ${proofPath}`);
|
|
31136
31205
|
}
|
|
31137
31206
|
lock ??= emptyLock();
|
|
@@ -31139,9 +31208,9 @@ function attestDocuments(options) {
|
|
|
31139
31208
|
const writes = new Map;
|
|
31140
31209
|
for (const relPath2 of selected) {
|
|
31141
31210
|
const abs = resolveWritePath(ctx.root, relPath2);
|
|
31142
|
-
if (!
|
|
31211
|
+
if (!existsSync14(abs))
|
|
31143
31212
|
throw new Error(`Cannot attest missing document: ${relPath2}`);
|
|
31144
|
-
const original =
|
|
31213
|
+
const original = readFileSync12(abs, "utf8");
|
|
31145
31214
|
if (!DOC_META_RE.test(original)) {
|
|
31146
31215
|
throw new Error(`Cannot attest ${relPath2}: missing doc-meta comment`);
|
|
31147
31216
|
}
|
|
@@ -31191,7 +31260,13 @@ function validateEntry(input) {
|
|
|
31191
31260
|
}));
|
|
31192
31261
|
return issues;
|
|
31193
31262
|
}
|
|
31194
|
-
issues.push(...dependencyHashIssues({
|
|
31263
|
+
issues.push(...dependencyHashIssues({
|
|
31264
|
+
root: ctx.root,
|
|
31265
|
+
relPath: relPath2,
|
|
31266
|
+
targets: currentTargets,
|
|
31267
|
+
entry,
|
|
31268
|
+
fileSource: ctx.fileSource
|
|
31269
|
+
}));
|
|
31195
31270
|
return issues;
|
|
31196
31271
|
}
|
|
31197
31272
|
function documentProofIssues(relPath2, content3, entry) {
|
|
@@ -31225,16 +31300,17 @@ function currentDependencies(input) {
|
|
|
31225
31300
|
}
|
|
31226
31301
|
function dependencyHashIssues(input) {
|
|
31227
31302
|
const { root: root2, relPath: relPath2, targets, entry } = input;
|
|
31303
|
+
const source = input.fileSource ?? "worktree";
|
|
31228
31304
|
const issues = [];
|
|
31229
31305
|
for (const target of targets) {
|
|
31230
|
-
const
|
|
31231
|
-
if (
|
|
31306
|
+
const content3 = readRepoText(root2, target, source);
|
|
31307
|
+
if (content3 === null) {
|
|
31232
31308
|
issues.push(issue("review-proof", relPath2, {
|
|
31233
31309
|
code: "review-dependency-missing",
|
|
31234
31310
|
message: `recorded review dependency is missing: ${target}`,
|
|
31235
31311
|
link: target
|
|
31236
31312
|
}));
|
|
31237
|
-
} else if (entry.reviewDependencies[target] !== hash(
|
|
31313
|
+
} else if (entry.reviewDependencies[target] !== hash(content3)) {
|
|
31238
31314
|
issues.push(changedDependencyIssue(relPath2, target));
|
|
31239
31315
|
}
|
|
31240
31316
|
}
|
|
@@ -31244,8 +31320,9 @@ function runReviewProofRule(ctx) {
|
|
|
31244
31320
|
if (!ctx.config.reviewProof)
|
|
31245
31321
|
return [];
|
|
31246
31322
|
const relLock = lockPath(ctx);
|
|
31247
|
-
const
|
|
31248
|
-
|
|
31323
|
+
const source = ctx.fileSource ?? "worktree";
|
|
31324
|
+
const lockContent = readRepoText(ctx.root, relLock, source);
|
|
31325
|
+
if (lockContent === null) {
|
|
31249
31326
|
return [
|
|
31250
31327
|
issue("review-proof", relLock, {
|
|
31251
31328
|
code: "review-proof-lock-missing",
|
|
@@ -31253,7 +31330,7 @@ function runReviewProofRule(ctx) {
|
|
|
31253
31330
|
})
|
|
31254
31331
|
];
|
|
31255
31332
|
}
|
|
31256
|
-
const lock = parseLock(
|
|
31333
|
+
const lock = parseLock(lockContent);
|
|
31257
31334
|
if (!lock) {
|
|
31258
31335
|
return [
|
|
31259
31336
|
issue("review-proof", relLock, {
|
|
@@ -31264,11 +31341,10 @@ function runReviewProofRule(ctx) {
|
|
|
31264
31341
|
}
|
|
31265
31342
|
const issues = [];
|
|
31266
31343
|
for (const relPath2 of ctx.docMetaPaths) {
|
|
31267
|
-
const
|
|
31268
|
-
if (
|
|
31344
|
+
const content3 = readRepoText(ctx.root, relPath2, source);
|
|
31345
|
+
if (content3 === null)
|
|
31269
31346
|
continue;
|
|
31270
|
-
const
|
|
31271
|
-
const entry = lock.documents[normalizeRelPath(relative6(ctx.root, abs))];
|
|
31347
|
+
const entry = lock.documents[normalizeRelPath(relPath2)];
|
|
31272
31348
|
if (!entry) {
|
|
31273
31349
|
issues.push(issue("review-proof", relPath2, {
|
|
31274
31350
|
code: "review-proof-missing",
|
|
@@ -31295,8 +31371,8 @@ function runBannedRule(ctx) {
|
|
|
31295
31371
|
var bannedRule = { id: "banned", run: runBannedRule };
|
|
31296
31372
|
|
|
31297
31373
|
// src/audit/rules/doc-meta.ts
|
|
31298
|
-
import { existsSync as
|
|
31299
|
-
import { join as
|
|
31374
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
|
|
31375
|
+
import { join as join12 } from "node:path";
|
|
31300
31376
|
function checkDocMetaBanner(relPath2, content3) {
|
|
31301
31377
|
if (DOC_META_RE.test(content3))
|
|
31302
31378
|
return null;
|
|
@@ -31351,10 +31427,10 @@ function runDocMetaRule(ctx) {
|
|
|
31351
31427
|
const issues = [];
|
|
31352
31428
|
const today = new Date;
|
|
31353
31429
|
for (const relPath2 of ctx.docMetaPaths) {
|
|
31354
|
-
const abs =
|
|
31355
|
-
if (!
|
|
31430
|
+
const abs = join12(ctx.root, relPath2);
|
|
31431
|
+
if (!existsSync15(abs))
|
|
31356
31432
|
continue;
|
|
31357
|
-
const content3 =
|
|
31433
|
+
const content3 = readFileSync13(abs, "utf8");
|
|
31358
31434
|
const banner = checkDocMetaBanner(relPath2, content3);
|
|
31359
31435
|
if (banner) {
|
|
31360
31436
|
issues.push(banner);
|
|
@@ -31382,7 +31458,7 @@ function runDocMetaRule(ctx) {
|
|
|
31382
31458
|
var docMetaRule = { id: "doc-meta", run: runDocMetaRule };
|
|
31383
31459
|
|
|
31384
31460
|
// src/audit/rules/links.ts
|
|
31385
|
-
import { existsSync as
|
|
31461
|
+
import { existsSync as existsSync16, readFileSync as readFileSync14 } from "node:fs";
|
|
31386
31462
|
import { dirname as dirname9, resolve as resolve5 } from "node:path";
|
|
31387
31463
|
function resolveLink2(sourceFile, target) {
|
|
31388
31464
|
const withoutAnchor = target.split("#")[0]?.split("?")[0] ?? "";
|
|
@@ -31406,13 +31482,13 @@ function checkAgentFile(input, resolved, relSource) {
|
|
|
31406
31482
|
return null;
|
|
31407
31483
|
}
|
|
31408
31484
|
const agentPath = resolved.endsWith(".md") ? resolved : `${resolved}.md`;
|
|
31409
|
-
if (
|
|
31485
|
+
if (existsSync16(agentPath))
|
|
31410
31486
|
return null;
|
|
31411
31487
|
return issue("links", relSource, { message: "missing agent file", link: input.linkLabel });
|
|
31412
31488
|
}
|
|
31413
31489
|
function checkBrokenPath(ctx) {
|
|
31414
31490
|
const { input, pathPart, resolved, relSource, relTarget } = ctx;
|
|
31415
|
-
if (!(pathPart && !
|
|
31491
|
+
if (!(pathPart && !existsSync16(resolved)))
|
|
31416
31492
|
return null;
|
|
31417
31493
|
return issue("links", relSource, {
|
|
31418
31494
|
message: `broken link → ${relTarget}`,
|
|
@@ -31421,9 +31497,9 @@ function checkBrokenPath(ctx) {
|
|
|
31421
31497
|
}
|
|
31422
31498
|
function checkBrokenAnchor(ctx) {
|
|
31423
31499
|
const { input, anchor, resolved, relSource, relTarget } = ctx;
|
|
31424
|
-
if (!(anchor &&
|
|
31500
|
+
if (!(anchor && existsSync16(resolved)))
|
|
31425
31501
|
return null;
|
|
31426
|
-
const targetContent =
|
|
31502
|
+
const targetContent = readFileSync14(resolved, "utf8");
|
|
31427
31503
|
const slugs = extractHeadingSlugs(targetContent, resolved);
|
|
31428
31504
|
const anchorSlug = slugifyAnchor(anchor);
|
|
31429
31505
|
if (slugs.has(anchorSlug))
|
|
@@ -31475,8 +31551,8 @@ function runLinksRule(ctx) {
|
|
|
31475
31551
|
var linksRule = { id: "links", run: runLinksRule };
|
|
31476
31552
|
|
|
31477
31553
|
// src/audit/rules/near-duplicate.ts
|
|
31478
|
-
import { readFileSync as
|
|
31479
|
-
import { join as
|
|
31554
|
+
import { readFileSync as readFileSync15 } from "node:fs";
|
|
31555
|
+
import { join as join13 } from "node:path";
|
|
31480
31556
|
|
|
31481
31557
|
// src/audit/core/ssot-fit.ts
|
|
31482
31558
|
var DEFAULT_SSOT_OVERLAP_MIN = 0.35;
|
|
@@ -31764,7 +31840,7 @@ function runNearDuplicateRule(ctx) {
|
|
|
31764
31840
|
const ignored = ignoredPairSet(ctx);
|
|
31765
31841
|
const entries = eligibleEntries(ctx);
|
|
31766
31842
|
const fingerprints = entries.map((e) => {
|
|
31767
|
-
const content3 =
|
|
31843
|
+
const content3 = readFileSync15(join13(ctx.root, e.path), "utf8");
|
|
31768
31844
|
const tokens = tokenize2(bodyWithoutSsotNoise(content3));
|
|
31769
31845
|
return {
|
|
31770
31846
|
path: e.path,
|
|
@@ -31859,12 +31935,105 @@ function runProsePolicyRule(ctx) {
|
|
|
31859
31935
|
}
|
|
31860
31936
|
var prosePolicyRule = { id: "prose-policy", run: runProsePolicyRule };
|
|
31861
31937
|
|
|
31938
|
+
// src/audit/core/review-coverage.ts
|
|
31939
|
+
var DEFAULT_REVIEW_COVERAGE_INCLUDE = [
|
|
31940
|
+
"**/*.{ts,tsx,js,jsx,mjs,cjs,py}",
|
|
31941
|
+
"package.json",
|
|
31942
|
+
"project.json"
|
|
31943
|
+
];
|
|
31944
|
+
var DEFAULT_REVIEW_COVERAGE_EXCLUDE = [
|
|
31945
|
+
"**/__tests__/**",
|
|
31946
|
+
"**/*.test.*",
|
|
31947
|
+
"**/*.spec.*",
|
|
31948
|
+
"**/fixtures/**",
|
|
31949
|
+
"templates/**",
|
|
31950
|
+
"**/dist/**",
|
|
31951
|
+
"**/node_modules/**",
|
|
31952
|
+
"**/.venv/**",
|
|
31953
|
+
"**/.git/**",
|
|
31954
|
+
".skeleton/plugins/**"
|
|
31955
|
+
];
|
|
31956
|
+
function reviewCoveragePatterns(config) {
|
|
31957
|
+
const configured = config.reviewCoverage;
|
|
31958
|
+
if (configured?.include && configured.include.length === 0) {
|
|
31959
|
+
return { include: [], exclude: [] };
|
|
31960
|
+
}
|
|
31961
|
+
return {
|
|
31962
|
+
include: configured?.include && configured.include.length > 0 ? configured.include : DEFAULT_REVIEW_COVERAGE_INCLUDE,
|
|
31963
|
+
exclude: [...DEFAULT_REVIEW_COVERAGE_EXCLUDE, ...configured?.exclude ?? []]
|
|
31964
|
+
};
|
|
31965
|
+
}
|
|
31966
|
+
function pathRequiresReviewCoverage(relPath2, config) {
|
|
31967
|
+
const { include, exclude } = reviewCoveragePatterns(config);
|
|
31968
|
+
if (include.length === 0)
|
|
31969
|
+
return false;
|
|
31970
|
+
const path3 = normalizeRelPath(relPath2);
|
|
31971
|
+
if (exclude.some((pattern) => matchesGlobScope(path3, pattern)))
|
|
31972
|
+
return false;
|
|
31973
|
+
return include.some((pattern) => matchesGlobScope(path3, pattern));
|
|
31974
|
+
}
|
|
31975
|
+
function collectReviewDependencyPatterns(input) {
|
|
31976
|
+
const patterns = new Set;
|
|
31977
|
+
const source = input.fileSource ?? "worktree";
|
|
31978
|
+
for (const abs of collectScanFiles(input.config, input.root, input.skillIndex)) {
|
|
31979
|
+
const rel = relPath(abs, input.root);
|
|
31980
|
+
const content3 = readRepoText(input.root, rel, source);
|
|
31981
|
+
if (content3 === null)
|
|
31982
|
+
continue;
|
|
31983
|
+
for (const pattern of reviewDependencyPatterns(content3))
|
|
31984
|
+
patterns.add(pattern);
|
|
31985
|
+
}
|
|
31986
|
+
return [...patterns].sort();
|
|
31987
|
+
}
|
|
31988
|
+
function pathHasReviewOwner(relPath2, patterns) {
|
|
31989
|
+
return patterns.some((pattern) => reviewDependencyMatchesPath(pattern, relPath2));
|
|
31990
|
+
}
|
|
31991
|
+
function collectReviewCoverageFiles(root2, config) {
|
|
31992
|
+
const { include, exclude } = reviewCoveragePatterns(config);
|
|
31993
|
+
if (include.length === 0)
|
|
31994
|
+
return [];
|
|
31995
|
+
const files = new Set;
|
|
31996
|
+
for (const pattern of include) {
|
|
31997
|
+
for (const match of globSync(pattern, {
|
|
31998
|
+
cwd: root2,
|
|
31999
|
+
onlyFiles: true,
|
|
32000
|
+
dot: true,
|
|
32001
|
+
ignore: exclude
|
|
32002
|
+
})) {
|
|
32003
|
+
const rel = normalizeRelPath(match);
|
|
32004
|
+
if (exclude.some((item) => matchesGlobScope(rel, item)))
|
|
32005
|
+
continue;
|
|
32006
|
+
files.add(rel);
|
|
32007
|
+
}
|
|
32008
|
+
}
|
|
32009
|
+
return [...files].sort();
|
|
32010
|
+
}
|
|
32011
|
+
|
|
32012
|
+
// src/audit/rules/review-coverage.ts
|
|
32013
|
+
function runReviewCoverageRule(ctx) {
|
|
32014
|
+
const patterns = collectReviewDependencyPatterns({
|
|
32015
|
+
root: ctx.root,
|
|
32016
|
+
config: ctx.config,
|
|
32017
|
+
skillIndex: ctx.skillIndex,
|
|
32018
|
+
fileSource: ctx.fileSource
|
|
32019
|
+
});
|
|
32020
|
+
return collectReviewCoverageFiles(ctx.root, ctx.config).filter((path3) => !pathHasReviewOwner(path3, patterns)).map((path3) => issue("review-coverage", path3, {
|
|
32021
|
+
code: "review-coverage-gap",
|
|
32022
|
+
message: "file has no owning document; add a review-deps path or glob on the paper that describes it"
|
|
32023
|
+
}));
|
|
32024
|
+
}
|
|
32025
|
+
var reviewCoverageRule = {
|
|
32026
|
+
id: "review-coverage",
|
|
32027
|
+
global: true,
|
|
32028
|
+
run: runReviewCoverageRule
|
|
32029
|
+
};
|
|
32030
|
+
|
|
31862
32031
|
// src/audit/rules/review-deps.ts
|
|
31863
|
-
import { relative as
|
|
32032
|
+
import { relative as relative6 } from "node:path";
|
|
31864
32033
|
function runReviewDepsRule(ctx) {
|
|
31865
32034
|
const issues = [];
|
|
31866
32035
|
for (const abs of collectScanFiles(ctx.config, ctx.root, ctx.skillIndex)) {
|
|
31867
|
-
const path3 = normalizeRelPath(
|
|
32036
|
+
const path3 = normalizeRelPath(relative6(ctx.root, abs));
|
|
31868
32037
|
for (const marker of parseReviewDepsMarkers(readFileContent(abs))) {
|
|
31869
32038
|
issues.push(...validateMarker(ctx.root, path3, marker.paths));
|
|
31870
32039
|
}
|
|
@@ -31925,16 +32094,16 @@ function runScanRootsRule(ctx) {
|
|
|
31925
32094
|
var scanRootsRule = { id: "scan-roots", run: runScanRootsRule };
|
|
31926
32095
|
|
|
31927
32096
|
// src/audit/rules/skill-index.ts
|
|
31928
|
-
import { existsSync as
|
|
31929
|
-
import { join as
|
|
32097
|
+
import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync as readFileSync16 } from "node:fs";
|
|
32098
|
+
import { join as join14, relative as relative7 } from "node:path";
|
|
31930
32099
|
function walkSkillMarkdown(dir) {
|
|
31931
32100
|
const files = [];
|
|
31932
|
-
if (!
|
|
32101
|
+
if (!existsSync17(dir))
|
|
31933
32102
|
return files;
|
|
31934
32103
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
31935
32104
|
if (entry.name.startsWith("."))
|
|
31936
32105
|
continue;
|
|
31937
|
-
const fullPath =
|
|
32106
|
+
const fullPath = join14(dir, entry.name);
|
|
31938
32107
|
if (entry.isDirectory()) {
|
|
31939
32108
|
files.push(...walkSkillMarkdown(fullPath));
|
|
31940
32109
|
continue;
|
|
@@ -31960,8 +32129,8 @@ function parseReadmeTaxonomySlugs(content3) {
|
|
|
31960
32129
|
}
|
|
31961
32130
|
function scanFileForSkillLinks(ctx, filePath, index2) {
|
|
31962
32131
|
const issues = [];
|
|
31963
|
-
const rel =
|
|
31964
|
-
const content3 =
|
|
32132
|
+
const rel = relative7(ctx.root, filePath).replace(/\\/g, "/");
|
|
32133
|
+
const content3 = readFileSync16(filePath, "utf8");
|
|
31965
32134
|
for (const match of content3.matchAll(SKILL_LINK_RE)) {
|
|
31966
32135
|
const slug2 = match[1];
|
|
31967
32136
|
if (!slug2)
|
|
@@ -31974,14 +32143,14 @@ function scanFileForSkillLinks(ctx, filePath, index2) {
|
|
|
31974
32143
|
}
|
|
31975
32144
|
function taxonomyIssuesForReadme(input) {
|
|
31976
32145
|
const { ctx, index: index2, skillRoot, diskSlugs, nonPublic } = input;
|
|
31977
|
-
const readmePath =
|
|
31978
|
-
if (!
|
|
32146
|
+
const readmePath = join14(ctx.root, skillRoot.relPath, "README.md");
|
|
32147
|
+
if (!existsSync17(readmePath))
|
|
31979
32148
|
return [];
|
|
31980
|
-
const readme =
|
|
32149
|
+
const readme = readFileSync16(readmePath, "utf8");
|
|
31981
32150
|
if (!readme.includes("## Taxonomy"))
|
|
31982
32151
|
return [];
|
|
31983
32152
|
const taxonomySlugs = parseReadmeTaxonomySlugs(readme);
|
|
31984
|
-
const nestedSlugs = diskSlugs.filter((slug2) =>
|
|
32153
|
+
const nestedSlugs = diskSlugs.filter((slug2) => existsSync17(join14(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
|
|
31985
32154
|
const foreign = new Set(index2.foreignSlugs);
|
|
31986
32155
|
const publicSlugs = nestedSlugs.filter((slug2) => !(nonPublic.has(slug2) || foreign.has(slug2)));
|
|
31987
32156
|
const relReadme = `${skillRoot.relPath}/README.md`;
|
|
@@ -32014,10 +32183,10 @@ function slugsForRoot(skillRoot, index2, owned) {
|
|
|
32014
32183
|
function auditSkillRoot(input) {
|
|
32015
32184
|
const { ctx, index: index2, skillRoot, owned } = input;
|
|
32016
32185
|
const issues = [];
|
|
32017
|
-
const base = skillRoot.kind === "nested" ?
|
|
32186
|
+
const base = skillRoot.kind === "nested" ? join14(ctx.root, skillRoot.relPath) : ctx.root;
|
|
32018
32187
|
for (const slug2 of slugsForRoot(skillRoot, index2, owned)) {
|
|
32019
|
-
const skillDir =
|
|
32020
|
-
if (!
|
|
32188
|
+
const skillDir = join14(base, slug2);
|
|
32189
|
+
if (!existsSync17(skillDir))
|
|
32021
32190
|
continue;
|
|
32022
32191
|
for (const skillMd of walkSkillMarkdown(skillDir)) {
|
|
32023
32192
|
issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
|
|
@@ -32067,8 +32236,8 @@ function runSsotRule(ctx) {
|
|
|
32067
32236
|
var ssotRule = { id: "ssot", run: runSsotRule };
|
|
32068
32237
|
|
|
32069
32238
|
// src/audit/rules/ssot-summary.ts
|
|
32070
|
-
import { readFileSync as
|
|
32071
|
-
import { join as
|
|
32239
|
+
import { readFileSync as readFileSync17 } from "node:fs";
|
|
32240
|
+
import { join as join15 } from "node:path";
|
|
32072
32241
|
function runSsotSummaryRule(ctx) {
|
|
32073
32242
|
const overlapMin = ctx.config.docsLint?.ssotOverlapMin ?? DEFAULT_SSOT_OVERLAP_MIN;
|
|
32074
32243
|
const margin = ctx.config.docsLint?.ssotBetterMatchMargin ?? DEFAULT_BETTER_MATCH_MARGIN;
|
|
@@ -32076,7 +32245,7 @@ function runSsotSummaryRule(ctx) {
|
|
|
32076
32245
|
const files = ctx.ssotEntries.map((entry) => ({
|
|
32077
32246
|
path: entry.path,
|
|
32078
32247
|
summary: entry.summary,
|
|
32079
|
-
content:
|
|
32248
|
+
content: readFileSync17(join15(ctx.root, entry.path), "utf8")
|
|
32080
32249
|
}));
|
|
32081
32250
|
return evaluateSsotFit(files, {
|
|
32082
32251
|
overlapMin,
|
|
@@ -32097,6 +32266,7 @@ var docsRules = [
|
|
|
32097
32266
|
{ ...nearDuplicateRule, global: true },
|
|
32098
32267
|
{ ...ssotSummaryRule, global: true },
|
|
32099
32268
|
{ ...coverageGapsRule, global: true },
|
|
32269
|
+
{ ...reviewCoverageRule, global: true },
|
|
32100
32270
|
linksRule,
|
|
32101
32271
|
docMetaRule,
|
|
32102
32272
|
reviewProofRule,
|
|
@@ -32263,14 +32433,7 @@ function labelForSuite(suite) {
|
|
|
32263
32433
|
function catalogStatusFor(root2, suite) {
|
|
32264
32434
|
if (suite !== "docs" && suite !== "self")
|
|
32265
32435
|
return "not-applicable";
|
|
32266
|
-
|
|
32267
|
-
return "skipped-ci";
|
|
32268
|
-
const result = checkCatalog(root2);
|
|
32269
|
-
if (result.missing)
|
|
32270
|
-
return "missing";
|
|
32271
|
-
if (result.stale)
|
|
32272
|
-
return "stale";
|
|
32273
|
-
return "current";
|
|
32436
|
+
return refreshLocalCatalog(root2);
|
|
32274
32437
|
}
|
|
32275
32438
|
function buildAuditResult(input) {
|
|
32276
32439
|
const diagnostics = finalizeIssues(input.issues, input.options.strict);
|
|
@@ -32366,7 +32529,8 @@ async function evaluateAudit(options) {
|
|
|
32366
32529
|
const base = createContext({
|
|
32367
32530
|
root: options.root,
|
|
32368
32531
|
paths: pathScoped ? options.paths : undefined,
|
|
32369
|
-
includeExcludedSkillTrees: options.suite === "skills" && !pathScoped
|
|
32532
|
+
includeExcludedSkillTrees: options.suite === "skills" && !pathScoped,
|
|
32533
|
+
fileSource: options.fileSource
|
|
32370
32534
|
});
|
|
32371
32535
|
const loaded = await loadPlugins(base.root, base.config);
|
|
32372
32536
|
const ctx = { ...base, policies: loaded.policies };
|
|
@@ -32408,11 +32572,6 @@ async function evaluateAudit(options) {
|
|
|
32408
32572
|
for (const rule of executableRules) {
|
|
32409
32573
|
issues.push(...rule.run(ctx));
|
|
32410
32574
|
}
|
|
32411
|
-
if (options.suite === "docs" || options.suite === "self") {
|
|
32412
|
-
for (const message of catalogAuditWarnings(ctx.root)) {
|
|
32413
|
-
issues.push(issue("catalog", CATALOG_REL_PATH, { message, severity: "warning" }));
|
|
32414
|
-
}
|
|
32415
|
-
}
|
|
32416
32575
|
const executed = executableRules.map((rule) => rule.id);
|
|
32417
32576
|
return buildAuditResult({
|
|
32418
32577
|
options,
|
|
@@ -32441,175 +32600,15 @@ async function runAudit(options) {
|
|
|
32441
32600
|
return printAuditResult(await evaluateAudit(options), options.json);
|
|
32442
32601
|
}
|
|
32443
32602
|
|
|
32444
|
-
// src/customize/resolve.ts
|
|
32445
|
-
import { existsSync as existsSync17, readFileSync as readFileSync17 } from "node:fs";
|
|
32446
|
-
import { basename as basename3, join as join15, relative as relative9 } from "node:path";
|
|
32447
|
-
function customizeDir(root2) {
|
|
32448
|
-
return join15(root2, REGISTRY_DIR_REL, "customize");
|
|
32449
|
-
}
|
|
32450
|
-
function customizePathForSlug(root2, slug2) {
|
|
32451
|
-
return join15(customizeDir(root2), `${slug2}.md`);
|
|
32452
|
-
}
|
|
32453
|
-
function resolveSlugFile(root2, slug2) {
|
|
32454
|
-
const direct = customizePathForSlug(root2, slug2);
|
|
32455
|
-
if (existsSync17(direct)) {
|
|
32456
|
-
return {
|
|
32457
|
-
content: readFileSync17(direct, "utf8"),
|
|
32458
|
-
path: normalizeRelPath(relative9(root2, direct))
|
|
32459
|
-
};
|
|
32460
|
-
}
|
|
32461
|
-
return { content: null, path: null };
|
|
32462
|
-
}
|
|
32463
|
-
function alwaysIncludeBasenames(root2) {
|
|
32464
|
-
try {
|
|
32465
|
-
const config = loadConfig(root2);
|
|
32466
|
-
return config.customize?.alwaysInclude ?? [];
|
|
32467
|
-
} catch {
|
|
32468
|
-
return [];
|
|
32469
|
-
}
|
|
32470
|
-
}
|
|
32471
|
-
function readAlwaysInclude(root2, basenames, skipBasename) {
|
|
32472
|
-
const parts = [];
|
|
32473
|
-
const paths = [];
|
|
32474
|
-
const dir = customizeDir(root2);
|
|
32475
|
-
for (const name of basenames) {
|
|
32476
|
-
const file = basename3(name);
|
|
32477
|
-
if (skipBasename && file === skipBasename)
|
|
32478
|
-
continue;
|
|
32479
|
-
const abs = join15(dir, file);
|
|
32480
|
-
if (!existsSync17(abs))
|
|
32481
|
-
continue;
|
|
32482
|
-
parts.push(readFileSync17(abs, "utf8").trimEnd());
|
|
32483
|
-
paths.push(normalizeRelPath(relative9(root2, abs)));
|
|
32484
|
-
}
|
|
32485
|
-
return { parts, paths };
|
|
32486
|
-
}
|
|
32487
|
-
function resolveCustomize(root2, slug2) {
|
|
32488
|
-
const slugFile = resolveSlugFile(root2, slug2);
|
|
32489
|
-
const alwaysNames = alwaysIncludeBasenames(root2);
|
|
32490
|
-
const skip = slugFile.path !== null && slugFile.path !== undefined ? basename3(slugFile.path) : null;
|
|
32491
|
-
const always = readAlwaysInclude(root2, alwaysNames, skip);
|
|
32492
|
-
const parts = [];
|
|
32493
|
-
const included = [];
|
|
32494
|
-
if (slugFile.content !== null && slugFile.content !== undefined && slugFile.content.trim().length > 0) {
|
|
32495
|
-
parts.push(slugFile.content.trimEnd());
|
|
32496
|
-
if (slugFile.path)
|
|
32497
|
-
included.push(slugFile.path);
|
|
32498
|
-
}
|
|
32499
|
-
parts.push(...always.parts);
|
|
32500
|
-
included.push(...always.paths);
|
|
32501
|
-
if (parts.length === 0) {
|
|
32502
|
-
return { slug: slug2, content: null, path: slugFile.path, included: [] };
|
|
32503
|
-
}
|
|
32504
|
-
return {
|
|
32505
|
-
slug: slug2,
|
|
32506
|
-
content: `${parts.join(`
|
|
32507
|
-
|
|
32508
|
-
---
|
|
32509
|
-
|
|
32510
|
-
`)}
|
|
32511
|
-
`,
|
|
32512
|
-
path: slugFile.path ?? always.paths[0] ?? null,
|
|
32513
|
-
included
|
|
32514
|
-
};
|
|
32515
|
-
}
|
|
32516
|
-
function resolveCustomizeFromRoot(slug2, startDir) {
|
|
32517
|
-
const root2 = findRepoRoot(startDir);
|
|
32518
|
-
return resolveCustomize(root2, slug2);
|
|
32519
|
-
}
|
|
32520
|
-
|
|
32521
|
-
// src/hooks/run.ts
|
|
32522
|
-
import process5 from "node:process";
|
|
32523
|
-
function parsePayload(raw) {
|
|
32524
|
-
if (!raw.trim())
|
|
32525
|
-
return {};
|
|
32526
|
-
return JSON.parse(raw);
|
|
32527
|
-
}
|
|
32528
|
-
function extractPath(payload) {
|
|
32529
|
-
const input = payload.tool_input ?? payload.toolInput ?? {};
|
|
32530
|
-
const candidates = [
|
|
32531
|
-
input.path,
|
|
32532
|
-
input.file_path,
|
|
32533
|
-
input.filePath,
|
|
32534
|
-
input.target_file,
|
|
32535
|
-
input.targetFile
|
|
32536
|
-
];
|
|
32537
|
-
for (const value of candidates) {
|
|
32538
|
-
if (typeof value === "string" && value.trim())
|
|
32539
|
-
return normalizeRelPath(value.trim());
|
|
32540
|
-
}
|
|
32541
|
-
return null;
|
|
32542
|
-
}
|
|
32543
|
-
function extractSkillSlug(payload) {
|
|
32544
|
-
const tool = payload.tool_name ?? payload.toolName ?? "";
|
|
32545
|
-
if (tool === "Skill" || tool === "skill") {
|
|
32546
|
-
const input = payload.tool_input ?? payload.toolInput ?? {};
|
|
32547
|
-
const slug2 = input.skill ?? input.slug ?? input.name;
|
|
32548
|
-
if (typeof slug2 === "string" && slug2.trim())
|
|
32549
|
-
return slug2.trim();
|
|
32550
|
-
}
|
|
32551
|
-
const path3 = extractPath(payload);
|
|
32552
|
-
if (path3)
|
|
32553
|
-
return slugFromPath(path3, process5.cwd());
|
|
32554
|
-
return null;
|
|
32555
|
-
}
|
|
32556
|
-
function cursorResponse(content3) {
|
|
32557
|
-
return JSON.stringify({ additional_context: content3 });
|
|
32558
|
-
}
|
|
32559
|
-
function claudeResponse(content3) {
|
|
32560
|
-
return JSON.stringify({
|
|
32561
|
-
hookSpecificOutput: {
|
|
32562
|
-
additionalContext: content3
|
|
32563
|
-
}
|
|
32564
|
-
});
|
|
32565
|
-
}
|
|
32566
|
-
function codexResponse(content3) {
|
|
32567
|
-
return JSON.stringify({ additionalContext: content3 });
|
|
32568
|
-
}
|
|
32569
|
-
function formatResponse(payload, content3) {
|
|
32570
|
-
const tool = payload.tool_name ?? payload.toolName ?? "";
|
|
32571
|
-
if (tool === "read_file")
|
|
32572
|
-
return codexResponse(content3);
|
|
32573
|
-
if (payload.hook_event_name?.toLowerCase().includes("claude"))
|
|
32574
|
-
return claudeResponse(content3);
|
|
32575
|
-
if (tool === "Read" && payload.hook_event_name === "postToolUse")
|
|
32576
|
-
return cursorResponse(content3);
|
|
32577
|
-
if (tool === "Read" || tool === "Skill")
|
|
32578
|
-
return claudeResponse(content3);
|
|
32579
|
-
return cursorResponse(content3);
|
|
32580
|
-
}
|
|
32581
|
-
function runCustomizeHook(raw) {
|
|
32582
|
-
try {
|
|
32583
|
-
const payload = parsePayload(raw);
|
|
32584
|
-
const slug2 = extractSkillSlug(payload);
|
|
32585
|
-
if (!slug2)
|
|
32586
|
-
return "{}";
|
|
32587
|
-
const resolved = resolveCustomizeFromRoot(slug2);
|
|
32588
|
-
if (!resolved.content)
|
|
32589
|
-
return "{}";
|
|
32590
|
-
const from = resolved.included.length > 0 ? resolved.included.join(", ") : resolved.path ?? ".skeleton/customize";
|
|
32591
|
-
const prefix = `
|
|
32592
|
-
|
|
32593
|
-
---
|
|
32594
|
-
Customize override for /${slug2} (from ${from}):
|
|
32595
|
-
|
|
32596
|
-
`;
|
|
32597
|
-
return formatResponse(payload, prefix + resolved.content);
|
|
32598
|
-
} catch (error) {
|
|
32599
|
-
console.error(`customize hook error: ${error}`);
|
|
32600
|
-
return "{}";
|
|
32601
|
-
}
|
|
32602
|
-
}
|
|
32603
|
-
|
|
32604
32603
|
// src/init/init.ts
|
|
32605
|
-
import { spawnSync as
|
|
32606
|
-
import { copyFileSync, existsSync as existsSync21, mkdirSync as
|
|
32604
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
32605
|
+
import { copyFileSync, existsSync as existsSync21, mkdirSync as mkdirSync3, readFileSync as readFileSync20 } from "node:fs";
|
|
32607
32606
|
import { join as join19 } from "node:path";
|
|
32608
|
-
import
|
|
32607
|
+
import process4 from "node:process";
|
|
32609
32608
|
|
|
32610
|
-
// src/init/merge-
|
|
32611
|
-
import { existsSync as
|
|
32612
|
-
import {
|
|
32609
|
+
// src/init/merge-precommit.ts
|
|
32610
|
+
import { existsSync as existsSync19, readFileSync as readFileSync18, writeFileSync as writeFileSync4 } from "node:fs";
|
|
32611
|
+
import { join as join17 } from "node:path";
|
|
32613
32612
|
|
|
32614
32613
|
// src/init/package-paths.ts
|
|
32615
32614
|
import { existsSync as existsSync18 } from "node:fs";
|
|
@@ -32632,279 +32631,45 @@ function resolveTemplatesDir() {
|
|
|
32632
32631
|
return dir;
|
|
32633
32632
|
}
|
|
32634
32633
|
|
|
32635
|
-
// src/init/
|
|
32636
|
-
import { existsSync as existsSync19, realpathSync as realpathSync5 } from "node:fs";
|
|
32637
|
-
import { createRequire as createRequire3 } from "node:module";
|
|
32638
|
-
import { dirname as dirname11, join as join17, relative as relative10, resolve as resolve6 } from "node:path";
|
|
32639
|
-
var PACKAGE_NAME = "@csark0812/skeleton";
|
|
32640
|
-
var CLI_DIST = "dist/cli.js";
|
|
32641
|
-
var PACKAGE_ROOT = resolvePackageRoot();
|
|
32642
|
-
var DEV_HOOK_COMMAND = "bun src/cli.ts hook customize";
|
|
32643
|
-
var FALLBACK_HOOK_COMMAND = `node node_modules/${PACKAGE_NAME}/${CLI_DIST} hook customize`;
|
|
32644
|
-
function safeRealpath2(path3) {
|
|
32645
|
-
try {
|
|
32646
|
-
return realpathSync5(path3);
|
|
32647
|
-
} catch {
|
|
32648
|
-
return path3;
|
|
32649
|
-
}
|
|
32650
|
-
}
|
|
32651
|
-
function toRepoRelative(cwd, absPath) {
|
|
32652
|
-
const rel = relative10(safeRealpath2(cwd), safeRealpath2(absPath)).replace(/\\/g, "/");
|
|
32653
|
-
return rel.startsWith("..") ? absPath.replace(/\\/g, "/") : rel;
|
|
32654
|
-
}
|
|
32655
|
-
function tryResolvePublishedCli(cwd) {
|
|
32656
|
-
try {
|
|
32657
|
-
const req = createRequire3(join17(cwd, "package.json"));
|
|
32658
|
-
return req.resolve(`${PACKAGE_NAME}/${CLI_DIST}`);
|
|
32659
|
-
} catch {
|
|
32660
|
-
return null;
|
|
32661
|
-
}
|
|
32662
|
-
}
|
|
32663
|
-
function walkNodeModulesCli(cwd) {
|
|
32664
|
-
let dir = cwd;
|
|
32665
|
-
while (true) {
|
|
32666
|
-
const candidate = join17(dir, "node_modules", PACKAGE_NAME, CLI_DIST);
|
|
32667
|
-
if (existsSync19(candidate))
|
|
32668
|
-
return candidate;
|
|
32669
|
-
const parent = dirname11(dir);
|
|
32670
|
-
if (parent === dir)
|
|
32671
|
-
break;
|
|
32672
|
-
dir = parent;
|
|
32673
|
-
}
|
|
32674
|
-
return null;
|
|
32675
|
-
}
|
|
32676
|
-
function isInsidePackageRoot(cwd) {
|
|
32677
|
-
const rel = relative10(PACKAGE_ROOT, resolve6(cwd)).replace(/\\/g, "/");
|
|
32678
|
-
return rel === "" || !(rel.startsWith("..") || rel.startsWith("/"));
|
|
32679
|
-
}
|
|
32680
|
-
function nodeCliHookCommand(cliPath) {
|
|
32681
|
-
return `node ${cliPath} hook customize`;
|
|
32682
|
-
}
|
|
32683
|
-
function resolveHookCommand(cwd) {
|
|
32684
|
-
if (isInsidePackageRoot(cwd))
|
|
32685
|
-
return DEV_HOOK_COMMAND;
|
|
32686
|
-
const published = tryResolvePublishedCli(cwd);
|
|
32687
|
-
if (published)
|
|
32688
|
-
return nodeCliHookCommand(toRepoRelative(cwd, published));
|
|
32689
|
-
const hoisted = walkNodeModulesCli(cwd);
|
|
32690
|
-
if (hoisted)
|
|
32691
|
-
return nodeCliHookCommand(toRepoRelative(cwd, hoisted));
|
|
32692
|
-
return FALLBACK_HOOK_COMMAND;
|
|
32693
|
-
}
|
|
32694
|
-
function isSkeletonHookCommand(command) {
|
|
32695
|
-
if (!command)
|
|
32696
|
-
return false;
|
|
32697
|
-
if (/\bhook\s+customize\b/.test(command))
|
|
32698
|
-
return true;
|
|
32699
|
-
return /customize-on-skill-read\.(js|ts)\b/.test(command);
|
|
32700
|
-
}
|
|
32701
|
-
|
|
32702
|
-
// src/init/merge-hooks.ts
|
|
32634
|
+
// src/init/merge-precommit.ts
|
|
32703
32635
|
var TEMPLATES_DIR = resolveTemplatesDir();
|
|
32704
|
-
|
|
32705
|
-
|
|
32706
|
-
|
|
32707
|
-
|
|
32708
|
-
|
|
32709
|
-
|
|
32710
|
-
|
|
32711
|
-
|
|
32712
|
-
|
|
32713
|
-
|
|
32714
|
-
|
|
32715
|
-
|
|
32716
|
-
|
|
32717
|
-
|
|
32718
|
-
|
|
32719
|
-
|
|
32720
|
-
|
|
32721
|
-
|
|
32722
|
-
|
|
32723
|
-
|
|
32724
|
-
|
|
32725
|
-
|
|
32726
|
-
|
|
32727
|
-
}
|
|
32728
|
-
|
|
32729
|
-
const { postToolUse, skeletonIdx, canonical, opts } = input;
|
|
32730
|
-
const current = postToolUse[skeletonIdx];
|
|
32731
|
-
const userEdited = current && !opts.forceHooks && (current.matcher !== canonical.matcher || !isSkeletonHookCommand(current.command));
|
|
32732
|
-
if (userEdited) {
|
|
32733
|
-
return {
|
|
32734
|
-
platform: "cursor",
|
|
32735
|
-
action: "conflict",
|
|
32736
|
-
message: identityKey("cursor", "postToolUse", String(current.matcher ?? "Read"))
|
|
32737
|
-
};
|
|
32738
|
-
}
|
|
32739
|
-
const extras = Object.fromEntries(Object.entries(current ?? {}).filter(([key]) => !["command", "matcher"].includes(key)));
|
|
32740
|
-
const merged = { ...extras, ...canonical };
|
|
32741
|
-
if (deepEqual(current, merged))
|
|
32742
|
-
return { platform: "cursor", action: "skipped" };
|
|
32743
|
-
postToolUse[skeletonIdx] = merged;
|
|
32744
|
-
return null;
|
|
32745
|
-
}
|
|
32746
|
-
function persistCursorHooks(input) {
|
|
32747
|
-
const { targetPath, existing, hooks, postToolUse, skeletonIdx } = input;
|
|
32748
|
-
const next = {
|
|
32749
|
-
...existing,
|
|
32750
|
-
version: existing.version ?? 1,
|
|
32751
|
-
hooks: { ...hooks, postToolUse }
|
|
32752
|
-
};
|
|
32753
|
-
if (deepEqual(existing, next))
|
|
32754
|
-
return { platform: "cursor", action: "skipped" };
|
|
32755
|
-
writeJson(targetPath, next);
|
|
32756
|
-
return { platform: "cursor", action: skeletonIdx >= 0 ? "updated" : "added" };
|
|
32757
|
-
}
|
|
32758
|
-
function mergeCursorHooks(targetPath, fragment, opts) {
|
|
32759
|
-
const existing = readJson(targetPath) ?? {};
|
|
32760
|
-
const hooks = existing.hooks ?? {};
|
|
32761
|
-
const postToolUse = [...hooks.postToolUse ?? []];
|
|
32762
|
-
const incoming = fragment.hooks?.postToolUse?.[0];
|
|
32763
|
-
if (!incoming)
|
|
32764
|
-
return { platform: "cursor", action: "skipped" };
|
|
32765
|
-
const skeletonIdx = postToolUse.findIndex((entry) => isSkeletonHookCommand(entry.command));
|
|
32766
|
-
const canonical = { ...incoming, matcher: incoming.matcher ?? "Read" };
|
|
32767
|
-
if (skeletonIdx >= 0) {
|
|
32768
|
-
const conflict = mergeExistingCursorHook({ postToolUse, skeletonIdx, canonical, opts });
|
|
32769
|
-
if (conflict)
|
|
32770
|
-
return conflict;
|
|
32771
|
-
} else {
|
|
32772
|
-
postToolUse.push(canonical);
|
|
32773
|
-
}
|
|
32774
|
-
return persistCursorHooks({ targetPath, existing, hooks, postToolUse, skeletonIdx });
|
|
32775
|
-
}
|
|
32776
|
-
function mergeExistingNestedSkeletonHook(input) {
|
|
32777
|
-
const { hooks, skeletonIdx, incomingHook, opts, platform, eventName, matcher } = input;
|
|
32778
|
-
const current = hooks[skeletonIdx];
|
|
32779
|
-
const userEdited = current && !opts.forceHooks && (current.type !== incomingHook.type || !isSkeletonHookCommand(current.command));
|
|
32780
|
-
if (userEdited) {
|
|
32781
|
-
return {
|
|
32782
|
-
changed: false,
|
|
32783
|
-
conflict: {
|
|
32784
|
-
platform,
|
|
32785
|
-
action: "conflict",
|
|
32786
|
-
message: identityKey(platform, eventName, matcher)
|
|
32787
|
-
}
|
|
32788
|
-
};
|
|
32789
|
-
}
|
|
32790
|
-
const extras = Object.fromEntries(Object.entries(current ?? {}).filter(([key]) => !["type", "command"].includes(key)));
|
|
32791
|
-
const merged = { ...extras, ...incomingHook };
|
|
32792
|
-
if (!deepEqual(current, merged)) {
|
|
32793
|
-
hooks[skeletonIdx] = merged;
|
|
32794
|
-
return { changed: true };
|
|
32795
|
-
}
|
|
32796
|
-
return { changed: false };
|
|
32797
|
-
}
|
|
32798
|
-
function mergeIncomingHookGroup(args) {
|
|
32799
|
-
const { group, incomingHook, eventHooks, opts, platform, eventName } = args;
|
|
32800
|
-
const matcher = group.matcher ?? "";
|
|
32801
|
-
const groupIdx = eventHooks.findIndex((g) => g.matcher === matcher);
|
|
32802
|
-
if (groupIdx < 0) {
|
|
32803
|
-
eventHooks.push({ ...group, hooks: [{ ...incomingHook }] });
|
|
32804
|
-
return { changed: true };
|
|
32805
|
-
}
|
|
32806
|
-
const existingGroup = eventHooks[groupIdx];
|
|
32807
|
-
const hooks = [...existingGroup?.hooks ?? []];
|
|
32808
|
-
const skeletonIdx = hooks.findIndex((entry) => isSkeletonHookCommand(entry.command));
|
|
32809
|
-
if (skeletonIdx >= 0) {
|
|
32810
|
-
const result = mergeExistingNestedSkeletonHook({
|
|
32811
|
-
hooks,
|
|
32812
|
-
skeletonIdx,
|
|
32813
|
-
incomingHook,
|
|
32814
|
-
opts,
|
|
32815
|
-
platform,
|
|
32816
|
-
eventName,
|
|
32817
|
-
matcher
|
|
32818
|
-
});
|
|
32819
|
-
if (result.conflict)
|
|
32820
|
-
return { changed: false, conflict: result.conflict };
|
|
32821
|
-
eventHooks[groupIdx] = { ...existingGroup, matcher, hooks };
|
|
32822
|
-
return { changed: result.changed };
|
|
32823
|
-
}
|
|
32824
|
-
hooks.push({ ...incomingHook });
|
|
32825
|
-
eventHooks[groupIdx] = { ...existingGroup, matcher, hooks };
|
|
32826
|
-
return { changed: true };
|
|
32827
|
-
}
|
|
32828
|
-
function mergeIncomingGroups(args) {
|
|
32829
|
-
let changed = false;
|
|
32830
|
-
for (const incomingGroup of args.incomingGroups) {
|
|
32831
|
-
const incomingHook = incomingGroup.hooks?.[0];
|
|
32832
|
-
if (!incomingHook)
|
|
32833
|
-
continue;
|
|
32834
|
-
const result = mergeIncomingHookGroup({
|
|
32835
|
-
group: incomingGroup,
|
|
32836
|
-
incomingHook,
|
|
32837
|
-
eventHooks: args.eventHooks,
|
|
32838
|
-
opts: args.opts,
|
|
32839
|
-
platform: args.platform,
|
|
32840
|
-
eventName: args.eventName
|
|
32841
|
-
});
|
|
32842
|
-
if (result.conflict)
|
|
32843
|
-
return { changed: false, conflict: result.conflict };
|
|
32844
|
-
if (result.changed)
|
|
32845
|
-
changed = true;
|
|
32846
|
-
}
|
|
32847
|
-
return { changed };
|
|
32848
|
-
}
|
|
32849
|
-
function mergeNestedHooks(args) {
|
|
32850
|
-
const { platform, targetPath, eventName } = args;
|
|
32851
|
-
const existing = readJson(targetPath) ?? {};
|
|
32852
|
-
const rootHooks = existing.hooks ?? {};
|
|
32853
|
-
const eventHooks = [...rootHooks[eventName] ?? []];
|
|
32854
|
-
const incomingGroups = args.fragment.hooks?.[eventName] ?? [];
|
|
32855
|
-
const result = mergeIncomingGroups({
|
|
32856
|
-
incomingGroups,
|
|
32857
|
-
eventHooks,
|
|
32858
|
-
opts: args.opts,
|
|
32859
|
-
platform,
|
|
32860
|
-
eventName
|
|
32861
|
-
});
|
|
32862
|
-
if (result.conflict)
|
|
32863
|
-
return result.conflict;
|
|
32864
|
-
if (!result.changed)
|
|
32865
|
-
return { platform, action: "skipped" };
|
|
32866
|
-
const next = {
|
|
32867
|
-
...existing,
|
|
32868
|
-
hooks: { ...rootHooks, [eventName]: eventHooks }
|
|
32869
|
-
};
|
|
32870
|
-
writeJson(targetPath, next);
|
|
32871
|
-
return { platform, action: "updated" };
|
|
32872
|
-
}
|
|
32873
|
-
function mergeHookConfigs(opts) {
|
|
32874
|
-
const results = [];
|
|
32875
|
-
const cursorPath = join18(opts.cwd, ".cursor/hooks.json");
|
|
32876
|
-
const cursorFragment = loadFragment("cursor-hooks.fragment.json", opts.hookCommand);
|
|
32877
|
-
results.push(mergeCursorHooks(cursorPath, cursorFragment, opts));
|
|
32878
|
-
const claudePath = join18(opts.cwd, ".claude/settings.json");
|
|
32879
|
-
const claudeFragment = loadFragment("claude-settings.fragment.json", opts.hookCommand);
|
|
32880
|
-
results.push(mergeNestedHooks({
|
|
32881
|
-
platform: "claude",
|
|
32882
|
-
targetPath: claudePath,
|
|
32883
|
-
fragment: claudeFragment,
|
|
32884
|
-
eventName: "PostToolUse",
|
|
32885
|
-
opts
|
|
32886
|
-
}));
|
|
32887
|
-
const codexPath = join18(opts.cwd, ".codex/hooks.json");
|
|
32888
|
-
if (existsSync20(join18(opts.cwd, ".codex"))) {
|
|
32889
|
-
const codexFragment = loadFragment("codex-hooks.fragment.json", opts.hookCommand);
|
|
32890
|
-
results.push(mergeNestedHooks({
|
|
32891
|
-
platform: "codex",
|
|
32892
|
-
targetPath: codexPath,
|
|
32893
|
-
fragment: codexFragment,
|
|
32894
|
-
eventName: "PostToolUse",
|
|
32895
|
-
opts
|
|
32896
|
-
}));
|
|
32897
|
-
} else {
|
|
32898
|
-
results.push({ platform: "codex", action: "skipped", message: "missing .codex directory" });
|
|
32899
|
-
}
|
|
32900
|
-
return results;
|
|
32636
|
+
var PRECOMMIT_NAME = ".pre-commit-config.yaml";
|
|
32637
|
+
var HOOK_ID = "id: skeleton-validate-staged";
|
|
32638
|
+
var LOCAL_HOOK_BLOCK = ` - repo: local
|
|
32639
|
+
hooks:
|
|
32640
|
+
- id: skeleton-validate-staged
|
|
32641
|
+
name: skeleton validate changed (staged)
|
|
32642
|
+
entry: node node_modules/@csark0812/skeleton/dist/cli.js validate changed --staged
|
|
32643
|
+
language: system
|
|
32644
|
+
pass_filenames: false
|
|
32645
|
+
`;
|
|
32646
|
+
function mergePrecommitConfig(cwd) {
|
|
32647
|
+
const target = join17(cwd, PRECOMMIT_NAME);
|
|
32648
|
+
const template = readFileSync18(join17(TEMPLATES_DIR, "pre-commit-config.yaml"), "utf8");
|
|
32649
|
+
if (!existsSync19(target)) {
|
|
32650
|
+
writeFileSync4(target, template, "utf8");
|
|
32651
|
+
return "added";
|
|
32652
|
+
}
|
|
32653
|
+
const existing = readFileSync18(target, "utf8");
|
|
32654
|
+
if (existing.includes(HOOK_ID))
|
|
32655
|
+
return "skipped";
|
|
32656
|
+
const suffix = existing.includes("repos:") ? `
|
|
32657
|
+
${LOCAL_HOOK_BLOCK}` : `
|
|
32658
|
+
${template}`;
|
|
32659
|
+
writeFileSync4(target, `${existing.trimEnd()}${suffix}`, "utf8");
|
|
32660
|
+
return "updated";
|
|
32901
32661
|
}
|
|
32662
|
+
|
|
32663
|
+
// src/init/merge-scripts.ts
|
|
32664
|
+
import { existsSync as existsSync20, readFileSync as readFileSync19, writeFileSync as writeFileSync5 } from "node:fs";
|
|
32665
|
+
import { join as join18 } from "node:path";
|
|
32666
|
+
var TEMPLATES_DIR2 = resolveTemplatesDir();
|
|
32902
32667
|
function mergePackageJsonScripts(cwd) {
|
|
32903
32668
|
const pkgPath = join18(cwd, "package.json");
|
|
32904
32669
|
if (!existsSync20(pkgPath))
|
|
32905
32670
|
return "skipped";
|
|
32906
|
-
const fragment = JSON.parse(
|
|
32907
|
-
const pkg = JSON.parse(
|
|
32671
|
+
const fragment = JSON.parse(readFileSync19(join18(TEMPLATES_DIR2, "package.json.scripts.fragment.json"), "utf8"));
|
|
32672
|
+
const pkg = JSON.parse(readFileSync19(pkgPath, "utf8"));
|
|
32908
32673
|
pkg.scripts ??= {};
|
|
32909
32674
|
let changed = false;
|
|
32910
32675
|
for (const [key, value] of Object.entries(fragment)) {
|
|
@@ -32915,7 +32680,7 @@ function mergePackageJsonScripts(cwd) {
|
|
|
32915
32680
|
}
|
|
32916
32681
|
if (!changed)
|
|
32917
32682
|
return "skipped";
|
|
32918
|
-
|
|
32683
|
+
writeFileSync5(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
32919
32684
|
`, "utf8");
|
|
32920
32685
|
return "updated";
|
|
32921
32686
|
}
|
|
@@ -32958,25 +32723,24 @@ function skillsAddArgs(options = {}) {
|
|
|
32958
32723
|
}
|
|
32959
32724
|
|
|
32960
32725
|
// src/init/init.ts
|
|
32961
|
-
var
|
|
32726
|
+
var TEMPLATES_DIR3 = resolveTemplatesDir();
|
|
32962
32727
|
function writeScaffold(cwd) {
|
|
32963
32728
|
const skeletonDir2 = join19(cwd, ".skeleton");
|
|
32964
|
-
|
|
32729
|
+
mkdirSync3(skeletonDir2, { recursive: true });
|
|
32965
32730
|
let created = false;
|
|
32966
32731
|
const tomlPath = join19(cwd, "skeleton.toml");
|
|
32967
32732
|
const legacyYaml = join19(skeletonDir2, "config.yaml");
|
|
32968
32733
|
if (!(existsSync21(tomlPath) || existsSync21(legacyYaml))) {
|
|
32969
|
-
copyFileSync(join19(
|
|
32734
|
+
copyFileSync(join19(TEMPLATES_DIR3, "skeleton.toml"), tomlPath);
|
|
32970
32735
|
created = true;
|
|
32971
32736
|
}
|
|
32972
|
-
mkdirSync4(join19(skeletonDir2, "customize"), { recursive: true });
|
|
32973
32737
|
return created ? "created" : "skipped";
|
|
32974
32738
|
}
|
|
32975
32739
|
function assertPackageResolvable(cwd) {
|
|
32976
32740
|
const pkgPath = join19(cwd, "package.json");
|
|
32977
32741
|
if (!existsSync21(pkgPath))
|
|
32978
32742
|
return;
|
|
32979
|
-
const pkg = JSON.parse(
|
|
32743
|
+
const pkg = JSON.parse(readFileSync20(pkgPath, "utf8"));
|
|
32980
32744
|
const hasDep = pkg.devDependencies?.["@csark0812/skeleton"] || pkg.dependencies?.["@csark0812/skeleton"];
|
|
32981
32745
|
if (!hasDep) {
|
|
32982
32746
|
try {
|
|
@@ -32987,23 +32751,13 @@ function assertPackageResolvable(cwd) {
|
|
|
32987
32751
|
}
|
|
32988
32752
|
}
|
|
32989
32753
|
function runSkillsAdd(args, cwd) {
|
|
32990
|
-
const result =
|
|
32754
|
+
const result = spawnSync3("npx", args, {
|
|
32991
32755
|
cwd,
|
|
32992
32756
|
stdio: "inherit",
|
|
32993
32757
|
shell: false
|
|
32994
32758
|
});
|
|
32995
32759
|
return result.status ?? 1;
|
|
32996
32760
|
}
|
|
32997
|
-
function logHookMergeResult(result) {
|
|
32998
|
-
if (result.action === "conflict") {
|
|
32999
|
-
console.error(`init: skipped ${result.platform} hook (user-edited) — re-run with --force-hooks to restore`);
|
|
33000
|
-
return;
|
|
33001
|
-
}
|
|
33002
|
-
if (result.action === "added")
|
|
33003
|
-
console.log(`init: added ${result.platform} customize hook`);
|
|
33004
|
-
if (result.action === "updated")
|
|
33005
|
-
console.log(`init: updated ${result.platform} customize hook`);
|
|
33006
|
-
}
|
|
33007
32761
|
function installSkillsIfRequested(options, cwd) {
|
|
33008
32762
|
if (!(options.skills && !options.noSkills))
|
|
33009
32763
|
return "skipped";
|
|
@@ -33016,30 +32770,34 @@ function installSkillsIfRequested(options, cwd) {
|
|
|
33016
32770
|
return "installed";
|
|
33017
32771
|
}
|
|
33018
32772
|
function runInit(options = {}) {
|
|
33019
|
-
const cwd = options.cwd ??
|
|
32773
|
+
const cwd = options.cwd ?? process4.cwd();
|
|
33020
32774
|
assertPackageResolvable(cwd);
|
|
33021
32775
|
const scaffold = writeScaffold(cwd);
|
|
33022
|
-
const hookCommand = resolveHookCommand(cwd);
|
|
33023
|
-
const hooks = mergeHookConfigs({ cwd, hookCommand, forceHooks: options.forceHooks });
|
|
33024
32776
|
const scripts = mergePackageJsonScripts(cwd);
|
|
33025
|
-
|
|
33026
|
-
logHookMergeResult(result);
|
|
32777
|
+
const precommit = mergePrecommitConfig(cwd);
|
|
33027
32778
|
if (scaffold === "created") {
|
|
33028
|
-
console.log("init: wrote skeleton.toml
|
|
32779
|
+
console.log("init: wrote skeleton.toml");
|
|
33029
32780
|
} else {
|
|
33030
32781
|
console.log("init: skeleton.toml or .skeleton/ already present — skipped scaffold write");
|
|
33031
32782
|
}
|
|
33032
32783
|
if (scripts === "updated") {
|
|
33033
32784
|
console.log("init: merged validate/audit scripts into package.json");
|
|
33034
32785
|
}
|
|
32786
|
+
if (precommit === "added") {
|
|
32787
|
+
console.log("init: wrote .pre-commit-config.yaml (run pre-commit install once per machine)");
|
|
32788
|
+
} else if (precommit === "updated") {
|
|
32789
|
+
console.log("init: added skeleton validate hook to .pre-commit-config.yaml");
|
|
32790
|
+
}
|
|
33035
32791
|
const skills = installSkillsIfRequested(options, cwd);
|
|
33036
|
-
return { scaffold,
|
|
32792
|
+
return { scaffold, scripts, skills, precommit };
|
|
33037
32793
|
}
|
|
33038
32794
|
|
|
33039
32795
|
// src/init/parse-args.ts
|
|
33040
|
-
var SKELETON_INIT_FLAGS = new Set(["--
|
|
32796
|
+
var SKELETON_INIT_FLAGS = new Set(["--skills", "--no-skills"]);
|
|
33041
32797
|
function parseInitArgs(argv) {
|
|
33042
|
-
|
|
32798
|
+
if (argv.includes("--force-hooks")) {
|
|
32799
|
+
throw new Error("init: --force-hooks was removed with customize hooks");
|
|
32800
|
+
}
|
|
33043
32801
|
const noSkills = argv.includes("--no-skills");
|
|
33044
32802
|
const skills = argv.includes("--skills");
|
|
33045
32803
|
const skillsFlags = [];
|
|
@@ -33054,14 +32812,14 @@ function parseInitArgs(argv) {
|
|
|
33054
32812
|
if (skills)
|
|
33055
32813
|
skillsFlags.push(arg);
|
|
33056
32814
|
}
|
|
33057
|
-
return {
|
|
32815
|
+
return { skills, noSkills, skillsFlags };
|
|
33058
32816
|
}
|
|
33059
32817
|
|
|
33060
32818
|
// src/plugins/build.ts
|
|
33061
|
-
import { spawnSync as
|
|
32819
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
33062
32820
|
import { createHash as createHash2 } from "node:crypto";
|
|
33063
|
-
import { existsSync as existsSync22, readFileSync as
|
|
33064
|
-
import { basename as
|
|
32821
|
+
import { existsSync as existsSync22, readFileSync as readFileSync21, writeFileSync as writeFileSync6 } from "node:fs";
|
|
32822
|
+
import { basename as basename3, dirname as dirname11, resolve as resolve6 } from "node:path";
|
|
33065
32823
|
function parseBuildPluginArgs(argv) {
|
|
33066
32824
|
let check = false;
|
|
33067
32825
|
let entry;
|
|
@@ -33086,7 +32844,7 @@ function collectPluginEntries(root2, config, entry) {
|
|
|
33086
32844
|
return (config.plugins ?? []).map((e) => resolvePluginTsPath(root2, e));
|
|
33087
32845
|
}
|
|
33088
32846
|
function localImportPaths(tsAbs, content3) {
|
|
33089
|
-
const dir =
|
|
32847
|
+
const dir = dirname11(tsAbs);
|
|
33090
32848
|
const deps = [];
|
|
33091
32849
|
const re = /(?:from\s+|import\s*\(\s*|import\s+)["'](\.[^"']+)["']/g;
|
|
33092
32850
|
for (const match of content3.matchAll(re)) {
|
|
@@ -33096,9 +32854,9 @@ function localImportPaths(tsAbs, content3) {
|
|
|
33096
32854
|
const candidates = [];
|
|
33097
32855
|
if (spec.endsWith(".js")) {
|
|
33098
32856
|
const withoutJs = spec.slice(0, -".js".length);
|
|
33099
|
-
candidates.push(
|
|
32857
|
+
candidates.push(resolve6(dir, `${withoutJs}.ts`), resolve6(dir, spec), resolve6(dir, withoutJs, "index.ts"));
|
|
33100
32858
|
} else {
|
|
33101
|
-
candidates.push(
|
|
32859
|
+
candidates.push(resolve6(dir, spec), resolve6(dir, `${spec}.ts`), resolve6(dir, `${spec}.js`), resolve6(dir, spec, "index.ts"));
|
|
33102
32860
|
}
|
|
33103
32861
|
for (const candidate of candidates) {
|
|
33104
32862
|
if (existsSync22(candidate) && candidate.endsWith(".ts")) {
|
|
@@ -33118,8 +32876,8 @@ function sourceFingerprint(tsAbs, seen = new Set) {
|
|
|
33118
32876
|
if (seen.has(abs))
|
|
33119
32877
|
return;
|
|
33120
32878
|
seen.add(abs);
|
|
33121
|
-
const content3 =
|
|
33122
|
-
hash2.update(
|
|
32879
|
+
const content3 = readFileSync21(abs, "utf8");
|
|
32880
|
+
hash2.update(basename3(abs));
|
|
33123
32881
|
hash2.update("\x00");
|
|
33124
32882
|
hash2.update(content3);
|
|
33125
32883
|
hash2.update("\x00");
|
|
@@ -33131,7 +32889,7 @@ function sourceFingerprint(tsAbs, seen = new Set) {
|
|
|
33131
32889
|
return hash2.digest("hex");
|
|
33132
32890
|
}
|
|
33133
32891
|
function writeStamp(tsAbs, mjsAbs) {
|
|
33134
|
-
|
|
32892
|
+
writeFileSync6(stampPathForMjs(mjsAbs), `${sourceFingerprint(tsAbs)}
|
|
33135
32893
|
`, "utf8");
|
|
33136
32894
|
}
|
|
33137
32895
|
async function buildOne(tsAbs) {
|
|
@@ -33139,7 +32897,7 @@ async function buildOne(tsAbs) {
|
|
|
33139
32897
|
if (!existsSync22(tsAbs)) {
|
|
33140
32898
|
throw new Error(`Plugin source not found: ${tsAbs}`);
|
|
33141
32899
|
}
|
|
33142
|
-
const proc =
|
|
32900
|
+
const proc = spawnSync4("bun", ["build", tsAbs, "--target=node", "--format=esm", `--outfile=${mjsAbs}`, "--packages=external"], { encoding: "utf8" });
|
|
33143
32901
|
if (proc.error) {
|
|
33144
32902
|
const code3 = proc.error.code;
|
|
33145
32903
|
if (code3 === "ENOENT") {
|
|
@@ -33166,7 +32924,7 @@ function checkOne(tsAbs) {
|
|
|
33166
32924
|
if (!existsSync22(stampAbs)) {
|
|
33167
32925
|
throw new Error(`Plugin stale: ${mjsAbs} has no fingerprint stamp. Run: skeleton build-plugin`);
|
|
33168
32926
|
}
|
|
33169
|
-
const expected =
|
|
32927
|
+
const expected = readFileSync21(stampAbs, "utf8").trim();
|
|
33170
32928
|
const actual = sourceFingerprint(tsAbs);
|
|
33171
32929
|
if (expected !== actual) {
|
|
33172
32930
|
throw new Error(`Plugin stale: ${mjsAbs} does not match ${tsAbs} (or local imports). Run: skeleton build-plugin`);
|
|
@@ -33195,15 +32953,15 @@ async function runBuildPlugin(options = {}) {
|
|
|
33195
32953
|
}
|
|
33196
32954
|
|
|
33197
32955
|
// src/validate/changed.ts
|
|
33198
|
-
import { spawnSync as
|
|
33199
|
-
import { existsSync as existsSync23, readFileSync as
|
|
33200
|
-
import { basename as
|
|
32956
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
32957
|
+
import { existsSync as existsSync23, readFileSync as readFileSync22 } from "node:fs";
|
|
32958
|
+
import { basename as basename4, extname as extname2, join as join20 } from "node:path";
|
|
33201
32959
|
|
|
33202
32960
|
// src/validate/git-diff.ts
|
|
33203
|
-
import { spawnSync as
|
|
32961
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
33204
32962
|
function gitDiffChangedFiles(options = {}) {
|
|
33205
32963
|
const root2 = options.root ?? findRepoRoot();
|
|
33206
|
-
const proc =
|
|
32964
|
+
const proc = spawnSync5("git", gitDiffArgs(options), { cwd: root2, encoding: "utf8" });
|
|
33207
32965
|
if (proc.status !== 0) {
|
|
33208
32966
|
throw new Error(proc.stderr?.trim() || "git diff failed");
|
|
33209
32967
|
}
|
|
@@ -33239,6 +32997,32 @@ function addChangedPath(input) {
|
|
|
33239
32997
|
});
|
|
33240
32998
|
}
|
|
33241
32999
|
|
|
33000
|
+
// src/validate/staged.ts
|
|
33001
|
+
function stageRequiredIssue(file) {
|
|
33002
|
+
return issue("validate-changed", file, {
|
|
33003
|
+
code: "stage-required",
|
|
33004
|
+
message: "worktree differs from HEAD and is not staged; stage the attested document and review-lock.json when hash mode is on"
|
|
33005
|
+
});
|
|
33006
|
+
}
|
|
33007
|
+
function stageRequiredDiagnostics(input) {
|
|
33008
|
+
if (!input.staged)
|
|
33009
|
+
return [];
|
|
33010
|
+
const staged = new Set(input.stagedPaths.map(normalizeRelPath));
|
|
33011
|
+
const required = [...input.impactedDocuments];
|
|
33012
|
+
if (input.config.reviewProof) {
|
|
33013
|
+
required.push(input.config.reviewProof.lockfile ?? DEFAULT_REVIEW_LOCKFILE);
|
|
33014
|
+
}
|
|
33015
|
+
const issues = [];
|
|
33016
|
+
for (const path3 of [...new Set(required)].sort()) {
|
|
33017
|
+
if (staged.has(path3))
|
|
33018
|
+
continue;
|
|
33019
|
+
if (!pathDiffersFromHead(input.root, path3))
|
|
33020
|
+
continue;
|
|
33021
|
+
issues.push(stageRequiredIssue(path3));
|
|
33022
|
+
}
|
|
33023
|
+
return issues;
|
|
33024
|
+
}
|
|
33025
|
+
|
|
33242
33026
|
// src/validate/changed.ts
|
|
33243
33027
|
var DOC_EXTENSIONS = new Set([".md", ".mdc", ".yaml", ".yml"]);
|
|
33244
33028
|
var POLICY_EXTENSIONS = new Set([".yaml", ".yml"]);
|
|
@@ -33251,7 +33035,7 @@ function isSkeletonYamlCandidate(normalized, ext) {
|
|
|
33251
33035
|
if (!(normalized.startsWith(".skeleton/") || normalized.startsWith(".skeleton\\"))) {
|
|
33252
33036
|
return false;
|
|
33253
33037
|
}
|
|
33254
|
-
const name =
|
|
33038
|
+
const name = basename4(normalized).toLowerCase();
|
|
33255
33039
|
if (name === "config.yaml" || name === "config.yml")
|
|
33256
33040
|
return false;
|
|
33257
33041
|
return true;
|
|
@@ -33276,7 +33060,7 @@ function bucketForDocPath(normalized, ctx) {
|
|
|
33276
33060
|
function bucketFor(relPath2, ctx) {
|
|
33277
33061
|
const normalized = normalizeRelPath(relPath2);
|
|
33278
33062
|
const ext = extname2(normalized).toLowerCase();
|
|
33279
|
-
const name =
|
|
33063
|
+
const name = basename4(normalized);
|
|
33280
33064
|
if (CODE_EXTENSIONS.has(ext))
|
|
33281
33065
|
return "code";
|
|
33282
33066
|
if (COMMAND_CONFIG_NAMES.has(name))
|
|
@@ -33314,30 +33098,51 @@ function parseJsonContent(content3) {
|
|
|
33314
33098
|
function validationIssue(code3, file, message) {
|
|
33315
33099
|
return issue("validate-changed", file, { code: code3, message, severity: "error" });
|
|
33316
33100
|
}
|
|
33317
|
-
function
|
|
33318
|
-
|
|
33101
|
+
function rereadValidationIssue(file, target) {
|
|
33102
|
+
return issue("validate-changed", file, {
|
|
33103
|
+
code: "impacted-document-review-required",
|
|
33104
|
+
message: "a linked review dependency changed; re-read the entire document, then attest it with --fix=doc-meta --confirm-reviewed and include the document in validation",
|
|
33105
|
+
severity: "error",
|
|
33106
|
+
link: target
|
|
33107
|
+
});
|
|
33108
|
+
}
|
|
33109
|
+
function validateJson(relPath2, root2, fileSource) {
|
|
33110
|
+
const content3 = readRepoText(root2, relPath2, fileSource);
|
|
33111
|
+
if (content3 === null)
|
|
33112
|
+
return validationIssue("invalid-json", relPath2, "path not found");
|
|
33319
33113
|
try {
|
|
33320
|
-
parseJsonContent(
|
|
33114
|
+
parseJsonContent(content3);
|
|
33321
33115
|
return null;
|
|
33322
33116
|
} catch (error) {
|
|
33323
33117
|
return validationIssue("invalid-json", relPath2, `invalid JSON: ${error}`);
|
|
33324
33118
|
}
|
|
33325
33119
|
}
|
|
33326
|
-
function validatePolicy(relPath2, root2) {
|
|
33327
|
-
const
|
|
33120
|
+
function validatePolicy(relPath2, root2, fileSource) {
|
|
33121
|
+
const content3 = readRepoText(root2, relPath2, fileSource);
|
|
33122
|
+
if (content3 === null)
|
|
33123
|
+
return validationIssue("invalid-policy", relPath2, "path not found");
|
|
33328
33124
|
try {
|
|
33329
|
-
loadPolicyFile(
|
|
33125
|
+
loadPolicyFile(join20(root2, relPath2), content3);
|
|
33330
33126
|
return null;
|
|
33331
33127
|
} catch (error) {
|
|
33332
33128
|
return validationIssue("invalid-policy", relPath2, `invalid policy: ${error}`);
|
|
33333
33129
|
}
|
|
33334
33130
|
}
|
|
33335
|
-
function validateShell(relPath2, root2) {
|
|
33131
|
+
function validateShell(relPath2, root2, fileSource) {
|
|
33132
|
+
if (fileSource === "index") {
|
|
33133
|
+
const content3 = readRepoText(root2, relPath2, fileSource);
|
|
33134
|
+
if (content3 === null)
|
|
33135
|
+
return validationIssue("invalid-shell", relPath2, "path not found");
|
|
33136
|
+
const bash2 = spawnSync6("bash", ["-n"], { input: content3, encoding: "utf8" });
|
|
33137
|
+
if (bash2.status === 0)
|
|
33138
|
+
return null;
|
|
33139
|
+
return validationIssue("invalid-shell", relPath2, `shell syntax check failed: ${bash2.stderr}`);
|
|
33140
|
+
}
|
|
33336
33141
|
const abs = join20(root2, relPath2);
|
|
33337
|
-
const shellcheck =
|
|
33142
|
+
const shellcheck = spawnSync6("shellcheck", [abs], { encoding: "utf8" });
|
|
33338
33143
|
if (shellcheck.status === 0)
|
|
33339
33144
|
return null;
|
|
33340
|
-
const bash =
|
|
33145
|
+
const bash = spawnSync6("bash", ["-n", abs], { encoding: "utf8" });
|
|
33341
33146
|
if (bash.status === 0)
|
|
33342
33147
|
return null;
|
|
33343
33148
|
return validationIssue("invalid-shell", relPath2, `shell syntax check failed: ${bash.stderr || shellcheck.stderr}`);
|
|
@@ -33361,7 +33166,7 @@ function packageManagerFromPackageJson(root2) {
|
|
|
33361
33166
|
if (!existsSync23(pkgPath))
|
|
33362
33167
|
return null;
|
|
33363
33168
|
try {
|
|
33364
|
-
const pkg = JSON.parse(
|
|
33169
|
+
const pkg = JSON.parse(readFileSync22(pkgPath, "utf8"));
|
|
33365
33170
|
const raw = pkg.packageManager?.split("@")[0];
|
|
33366
33171
|
if (raw === "bun" || raw === "npm" || raw === "pnpm" || raw === "yarn")
|
|
33367
33172
|
return raw;
|
|
@@ -33440,20 +33245,20 @@ function classifyPaths(ctx) {
|
|
|
33440
33245
|
}
|
|
33441
33246
|
return state;
|
|
33442
33247
|
}
|
|
33443
|
-
function validateLocalBuckets(buckets, root2) {
|
|
33248
|
+
function validateLocalBuckets(buckets, root2, fileSource) {
|
|
33444
33249
|
const diagnostics = [];
|
|
33445
33250
|
for (const relPath2 of buckets.shell) {
|
|
33446
|
-
const found = validateShell(relPath2, root2);
|
|
33251
|
+
const found = validateShell(relPath2, root2, fileSource);
|
|
33447
33252
|
if (found)
|
|
33448
33253
|
diagnostics.push(found);
|
|
33449
33254
|
}
|
|
33450
33255
|
for (const relPath2 of buckets.json) {
|
|
33451
|
-
const found = validateJson(relPath2, root2);
|
|
33256
|
+
const found = validateJson(relPath2, root2, fileSource);
|
|
33452
33257
|
if (found)
|
|
33453
33258
|
diagnostics.push(found);
|
|
33454
33259
|
}
|
|
33455
33260
|
for (const relPath2 of buckets.policy) {
|
|
33456
|
-
const found = validatePolicy(relPath2, root2);
|
|
33261
|
+
const found = validatePolicy(relPath2, root2, fileSource);
|
|
33457
33262
|
if (found)
|
|
33458
33263
|
diagnostics.push(found);
|
|
33459
33264
|
}
|
|
@@ -33466,16 +33271,24 @@ function discoverImpactedDocuments(input) {
|
|
|
33466
33271
|
const changed = new Set(input.relPaths.map(normalizeRelPath));
|
|
33467
33272
|
const impacted = [];
|
|
33468
33273
|
for (const abs of collectScanFiles(input.config, input.root, input.skillIndex)) {
|
|
33469
|
-
const document4 = impactedDocumentForPath(
|
|
33274
|
+
const document4 = impactedDocumentForPath({
|
|
33275
|
+
abs,
|
|
33276
|
+
root: input.root,
|
|
33277
|
+
changed,
|
|
33278
|
+
fileSource: input.fileSource
|
|
33279
|
+
});
|
|
33470
33280
|
if (document4)
|
|
33471
33281
|
impacted.push(document4);
|
|
33472
33282
|
}
|
|
33473
33283
|
return impacted.sort((a, b) => a.path.localeCompare(b.path));
|
|
33474
33284
|
}
|
|
33475
|
-
function impactedDocumentForPath(
|
|
33476
|
-
const path3 = relPath(abs,
|
|
33477
|
-
const
|
|
33478
|
-
|
|
33285
|
+
function impactedDocumentForPath(input) {
|
|
33286
|
+
const path3 = relPath(input.abs, input.root);
|
|
33287
|
+
const content3 = readRepoText(input.root, path3, input.fileSource);
|
|
33288
|
+
if (content3 === null)
|
|
33289
|
+
return null;
|
|
33290
|
+
const reviewDependencies = reviewDependencyPatterns(content3);
|
|
33291
|
+
const reasons = impactReasons(path3, reviewDependencies, input.changed);
|
|
33479
33292
|
return reasons.length > 0 ? { path: path3, reviewDependencies, reasons } : null;
|
|
33480
33293
|
}
|
|
33481
33294
|
function impactReasons(path3, reviewDependencies, changed) {
|
|
@@ -33494,18 +33307,35 @@ function dateModeImpactDiagnostics(input) {
|
|
|
33494
33307
|
return [];
|
|
33495
33308
|
const changed = new Set(input.relPaths.map(normalizeRelPath));
|
|
33496
33309
|
const today = formatLocalReviewDate(new Date);
|
|
33497
|
-
|
|
33498
|
-
|
|
33499
|
-
|
|
33500
|
-
|
|
33501
|
-
|
|
33502
|
-
|
|
33310
|
+
return input.impactedDocuments.flatMap((impacted) => dateModeIssuesForDocument({
|
|
33311
|
+
impacted,
|
|
33312
|
+
changed,
|
|
33313
|
+
today,
|
|
33314
|
+
root: input.root,
|
|
33315
|
+
fileSource: input.fileSource
|
|
33316
|
+
}));
|
|
33317
|
+
}
|
|
33318
|
+
function dateModeIssuesForDocument(input) {
|
|
33319
|
+
const content3 = readRepoText(input.root, input.impacted.path, input.fileSource);
|
|
33320
|
+
if (!content3) {
|
|
33321
|
+
return input.impacted.reasons.filter((reason) => reason.kind === "changed-review-dependency" && reason.target).map((reason) => rereadValidationIssue(input.impacted.path, reason.target ?? ""));
|
|
33322
|
+
}
|
|
33323
|
+
if (input.changed.has(input.impacted.path) && docMetaLastReviewed(content3) === input.today) {
|
|
33324
|
+
return [];
|
|
33325
|
+
}
|
|
33326
|
+
const issues = [];
|
|
33327
|
+
for (const reason of input.impacted.reasons) {
|
|
33328
|
+
if (reason.kind !== "changed-review-dependency" || !reason.target)
|
|
33503
33329
|
continue;
|
|
33504
|
-
|
|
33330
|
+
issues.push(rereadValidationIssue(input.impacted.path, reason.target));
|
|
33505
33331
|
}
|
|
33506
|
-
return
|
|
33332
|
+
return issues;
|
|
33333
|
+
}
|
|
33334
|
+
function uncoveredChangedPathDiagnostics(relPaths, config, patterns) {
|
|
33335
|
+
return relPaths.map(normalizeRelPath).filter((path3) => pathRequiresReviewCoverage(path3, config)).filter((path3) => !pathHasReviewOwner(path3, patterns)).map((path3) => validationIssue("uncovered-changed-path", path3, "no scanned document claims this path with review-deps. Add a review-deps marker on the owning paper."));
|
|
33507
33336
|
}
|
|
33508
|
-
function classificationDiagnostics(
|
|
33337
|
+
function classificationDiagnostics(input) {
|
|
33338
|
+
const { classification, root: root2, base, coverageCandidateCount } = input;
|
|
33509
33339
|
const diagnostics = [];
|
|
33510
33340
|
for (const orphan of classification.orphans) {
|
|
33511
33341
|
diagnostics.push(validationIssue("orphan-policy", orphan, "file is under .skeleton/ but is not referenced by any plugin policies glob; export it from a plugin policies array or move it"));
|
|
@@ -33514,7 +33344,7 @@ function classificationDiagnostics(classification, root2, base) {
|
|
|
33514
33344
|
diagnostics.push(validationIssue("missing-path", missing, "path not found"));
|
|
33515
33345
|
}
|
|
33516
33346
|
const audited = auditedPathCount(classification.buckets);
|
|
33517
|
-
if ((classification.skipped.length > 0 || classification.buckets.code.length > 0) && audited === 0 && !base) {
|
|
33347
|
+
if ((classification.skipped.length > 0 || classification.buckets.code.length > 0) && audited === 0 && coverageCandidateCount === 0 && !base) {
|
|
33518
33348
|
diagnostics.push(validationIssue("all-paths-skipped", ".", `all paths were skipped (code/config). This does not verify application code.
|
|
33519
33349
|
${codeValidationHint(root2)}`));
|
|
33520
33350
|
}
|
|
@@ -33545,35 +33375,54 @@ function auditOptions(suite, root2, paths, extra = {}) {
|
|
|
33545
33375
|
...extra
|
|
33546
33376
|
};
|
|
33547
33377
|
}
|
|
33378
|
+
async function auditSkillChanges(input) {
|
|
33379
|
+
const sourceOpt = { fileSource: input.fileSource };
|
|
33380
|
+
if (input.skills.length === 0)
|
|
33381
|
+
return [];
|
|
33382
|
+
if (!input.base) {
|
|
33383
|
+
return [await evaluateAudit(auditOptions("skills", input.root, [], sourceOpt))];
|
|
33384
|
+
}
|
|
33385
|
+
return [
|
|
33386
|
+
await evaluateAudit(auditOptions("skills", input.root, input.skills, {
|
|
33387
|
+
pathScopedOnly: true,
|
|
33388
|
+
...sourceOpt
|
|
33389
|
+
}))
|
|
33390
|
+
];
|
|
33391
|
+
}
|
|
33392
|
+
async function auditPolicyChanges(input) {
|
|
33393
|
+
const sourceOpt = { fileSource: input.fileSource };
|
|
33394
|
+
const audits = [await evaluateAudit(auditOptions("docs", input.root, [], sourceOpt))];
|
|
33395
|
+
const skillPaths = listSkillMarkdownPaths(input.root, input.skillIndex);
|
|
33396
|
+
if (skillPaths.length === 0)
|
|
33397
|
+
return audits;
|
|
33398
|
+
audits.push(await evaluateAudit(auditOptions("skills", input.root, skillPaths, {
|
|
33399
|
+
pathScopedOnly: true,
|
|
33400
|
+
...sourceOpt
|
|
33401
|
+
})));
|
|
33402
|
+
return audits;
|
|
33403
|
+
}
|
|
33548
33404
|
async function evaluateBucketAudits(input) {
|
|
33549
|
-
const { classification, root: root2, skillIndex, base } = input;
|
|
33405
|
+
const { classification, root: root2, skillIndex, base, fileSource } = input;
|
|
33550
33406
|
const audits = [];
|
|
33551
|
-
const diagnostics = validateLocalBuckets(classification.buckets, root2);
|
|
33407
|
+
const diagnostics = validateLocalBuckets(classification.buckets, root2, fileSource);
|
|
33408
|
+
const sourceOpt = { fileSource };
|
|
33552
33409
|
if (base) {
|
|
33553
|
-
audits.push(await evaluateAudit(auditOptions("self", root2, [], { globalOnly: true })));
|
|
33410
|
+
audits.push(await evaluateAudit(auditOptions("self", root2, [], { globalOnly: true, ...sourceOpt })));
|
|
33554
33411
|
}
|
|
33555
33412
|
if (classification.buckets.docs.length > 0) {
|
|
33556
|
-
audits.push(await evaluateAudit(auditOptions("docs", root2, classification.buckets.docs, {
|
|
33557
|
-
|
|
33558
|
-
|
|
33559
|
-
|
|
33560
|
-
diagnostics.push(validationIssue("full-skills-audit-required", classification.buckets.skills[0] ?? ".", "skill paths need the full skills suite; run skeleton audit skills (audit self covers docs and .skeleton; excluded skill trees still need audit skills)"));
|
|
33561
|
-
} else {
|
|
33562
|
-
audits.push(await evaluateAudit(auditOptions("skills", root2, classification.buckets.skills, {
|
|
33563
|
-
pathScopedOnly: true
|
|
33564
|
-
})));
|
|
33565
|
-
}
|
|
33413
|
+
audits.push(await evaluateAudit(auditOptions("docs", root2, classification.buckets.docs, {
|
|
33414
|
+
pathScopedOnly: true,
|
|
33415
|
+
...sourceOpt
|
|
33416
|
+
})));
|
|
33566
33417
|
}
|
|
33567
|
-
|
|
33568
|
-
|
|
33569
|
-
|
|
33570
|
-
|
|
33571
|
-
|
|
33572
|
-
|
|
33573
|
-
|
|
33574
|
-
|
|
33575
|
-
}
|
|
33576
|
-
}
|
|
33418
|
+
audits.push(...await auditSkillChanges({
|
|
33419
|
+
skills: classification.buckets.skills,
|
|
33420
|
+
root: root2,
|
|
33421
|
+
base,
|
|
33422
|
+
fileSource
|
|
33423
|
+
}));
|
|
33424
|
+
if (classification.buckets.policy.length > 0 && !diagnostics.some((item) => item.code === "invalid-policy")) {
|
|
33425
|
+
audits.push(...await auditPolicyChanges({ root: root2, skillIndex, fileSource }));
|
|
33577
33426
|
}
|
|
33578
33427
|
return { audits, diagnostics };
|
|
33579
33428
|
}
|
|
@@ -33611,6 +33460,7 @@ function emptyClassification() {
|
|
|
33611
33460
|
}
|
|
33612
33461
|
async function evaluateValidateChanged(options = {}) {
|
|
33613
33462
|
const root2 = options.root ?? findRepoRoot();
|
|
33463
|
+
refreshLocalCatalog(root2);
|
|
33614
33464
|
const resolvedPaths = resolvePaths(options);
|
|
33615
33465
|
const relPaths = resolvedPaths.paths;
|
|
33616
33466
|
if (relPaths.length === 0) {
|
|
@@ -33618,6 +33468,7 @@ async function evaluateValidateChanged(options = {}) {
|
|
|
33618
33468
|
}
|
|
33619
33469
|
const config = loadConfig(root2);
|
|
33620
33470
|
const skillIndex = buildSkillIndex(root2, config.skillOwnership);
|
|
33471
|
+
const fileSource = options.staged ? "index" : "worktree";
|
|
33621
33472
|
let wiredPolicies;
|
|
33622
33473
|
try {
|
|
33623
33474
|
wiredPolicies = await collectWiredPolicyRelPaths(root2, config);
|
|
@@ -33638,37 +33489,58 @@ async function evaluateValidateChanged(options = {}) {
|
|
|
33638
33489
|
skillIndex
|
|
33639
33490
|
});
|
|
33640
33491
|
classification.missing = classification.missing.filter((path3) => !resolvedPaths.deleted.has(path3));
|
|
33641
|
-
const impactedDocuments = discoverImpactedDocuments({
|
|
33492
|
+
const impactedDocuments = discoverImpactedDocuments({
|
|
33493
|
+
relPaths,
|
|
33494
|
+
root: root2,
|
|
33495
|
+
config,
|
|
33496
|
+
skillIndex,
|
|
33497
|
+
fileSource
|
|
33498
|
+
});
|
|
33642
33499
|
for (const impacted of impactedDocuments) {
|
|
33643
33500
|
if (!classification.buckets.docs.includes(impacted.path)) {
|
|
33644
33501
|
classification.buckets.docs.push(impacted.path);
|
|
33645
33502
|
}
|
|
33646
33503
|
}
|
|
33647
33504
|
classification.buckets.docs.sort();
|
|
33648
|
-
const
|
|
33649
|
-
|
|
33650
|
-
|
|
33651
|
-
|
|
33652
|
-
|
|
33653
|
-
|
|
33654
|
-
|
|
33655
|
-
|
|
33656
|
-
|
|
33657
|
-
|
|
33505
|
+
const ownerPatterns = collectReviewDependencyPatterns({
|
|
33506
|
+
root: root2,
|
|
33507
|
+
config,
|
|
33508
|
+
skillIndex,
|
|
33509
|
+
fileSource
|
|
33510
|
+
});
|
|
33511
|
+
const livePaths = relPaths.filter((path3) => !resolvedPaths.deleted.has(path3));
|
|
33512
|
+
const coverageCandidateCount = livePaths.filter((path3) => pathRequiresReviewCoverage(path3, config)).length;
|
|
33513
|
+
const diagnostics = [
|
|
33514
|
+
...classificationDiagnostics({
|
|
33515
|
+
classification,
|
|
33516
|
+
root: root2,
|
|
33517
|
+
base: options.base,
|
|
33518
|
+
coverageCandidateCount
|
|
33519
|
+
}),
|
|
33520
|
+
...uncoveredChangedPathDiagnostics(livePaths, config, ownerPatterns),
|
|
33521
|
+
...stageRequiredDiagnostics({
|
|
33522
|
+
staged: options.staged ?? false,
|
|
33523
|
+
stagedPaths: relPaths,
|
|
33524
|
+
impactedDocuments: impactedDocuments.map((item) => item.path),
|
|
33525
|
+
config,
|
|
33526
|
+
root: root2
|
|
33527
|
+
})
|
|
33528
|
+
];
|
|
33658
33529
|
const evaluated = await evaluateBucketAudits({
|
|
33659
33530
|
classification,
|
|
33660
33531
|
root: root2,
|
|
33661
33532
|
skillIndex,
|
|
33662
|
-
base: options.base
|
|
33533
|
+
base: options.base,
|
|
33534
|
+
fileSource
|
|
33663
33535
|
});
|
|
33664
|
-
evaluated.diagnostics.push(...dateModeImpactDiagnostics({ config, impactedDocuments, relPaths, root: root2 }));
|
|
33536
|
+
evaluated.diagnostics.push(...dateModeImpactDiagnostics({ config, impactedDocuments, relPaths, root: root2, fileSource }));
|
|
33665
33537
|
return resultFor({
|
|
33666
33538
|
options,
|
|
33667
33539
|
relPaths,
|
|
33668
33540
|
classification: publicClassification(classification),
|
|
33669
33541
|
impactedDocuments,
|
|
33670
33542
|
audits: evaluated.audits,
|
|
33671
|
-
diagnostics: evaluated.diagnostics
|
|
33543
|
+
diagnostics: [...diagnostics, ...evaluated.diagnostics]
|
|
33672
33544
|
});
|
|
33673
33545
|
}
|
|
33674
33546
|
function printValidateChangedResult(result) {
|
|
@@ -33679,16 +33551,20 @@ function printValidateChangedResult(result) {
|
|
|
33679
33551
|
for (const path3 of result.classification.foreignSkills) {
|
|
33680
33552
|
console.log(`validate changed: skipping foreign skill ${path3} (owned upstream; see skills-lock.json / skillOwnership)`);
|
|
33681
33553
|
}
|
|
33682
|
-
|
|
33683
|
-
|
|
33684
|
-
|
|
33685
|
-
|
|
33686
|
-
|
|
33687
|
-
|
|
33688
|
-
console.log(`validate changed: ${impacted.path} requires review (dependency ${reason.dependency} matched ${reason.target})`);
|
|
33554
|
+
if (result.ok) {
|
|
33555
|
+
for (const audit of result.audits)
|
|
33556
|
+
printAuditResult(audit, false);
|
|
33557
|
+
} else {
|
|
33558
|
+
for (const audit of result.audits.filter((item) => !item.ok)) {
|
|
33559
|
+
printAuditResult(audit, false);
|
|
33689
33560
|
}
|
|
33690
33561
|
}
|
|
33691
|
-
|
|
33562
|
+
const rereadDiagnostics = result.diagnostics.filter(isRereadIssue);
|
|
33563
|
+
const otherDiagnostics = result.diagnostics.filter((item) => !isRereadIssue(item));
|
|
33564
|
+
if (rereadDiagnostics.length > 0) {
|
|
33565
|
+
printReport(rereadDiagnostics, { label: "validate changed" });
|
|
33566
|
+
}
|
|
33567
|
+
for (const diagnostic of otherDiagnostics) {
|
|
33692
33568
|
const path3 = diagnostic.file === "." ? "" : `${diagnostic.file}: `;
|
|
33693
33569
|
console.error(`validate changed: ${path3}${diagnostic.message}`);
|
|
33694
33570
|
}
|
|
@@ -33716,16 +33592,14 @@ function usage() {
|
|
|
33716
33592
|
console.error(`Usage: skeleton <command>
|
|
33717
33593
|
|
|
33718
33594
|
Commands:
|
|
33719
|
-
init [--
|
|
33595
|
+
init [--skills] [--no-skills] [skills add flags…]
|
|
33720
33596
|
audit docs|self|skills [--strict] [--json] [--paths=a,b] [--only=rule]
|
|
33721
33597
|
[--fix[=doc-meta|anchors|ssot]] [--dry-run]
|
|
33722
33598
|
[--confirm-reviewed (doc-meta only; requires --paths)]
|
|
33723
33599
|
build-plugin [path] [--check]
|
|
33724
33600
|
validate changed [paths…] [--staged] [--base <ref>]
|
|
33725
33601
|
catalog [--check] [--strict] write or check .skeleton/catalog.md (gitignored)
|
|
33726
|
-
|
|
33727
|
-
hook customize (reads a host hook payload on stdin)
|
|
33728
|
-
Note: \`register\` was removed — add a source-of-truth marker to the file and run \`skeleton catalog\`.`);
|
|
33602
|
+
Note: \`register\`, \`customize\`, and \`hook\` were removed.`);
|
|
33729
33603
|
}
|
|
33730
33604
|
function parseValidateChangedArgs(rest) {
|
|
33731
33605
|
const paths = [];
|
|
@@ -33788,28 +33662,9 @@ function handleCatalog(argv) {
|
|
|
33788
33662
|
}
|
|
33789
33663
|
return runCatalogCli({ check: argv.includes("--check"), strict: argv.includes("--strict") });
|
|
33790
33664
|
}
|
|
33791
|
-
function
|
|
33792
|
-
|
|
33793
|
-
|
|
33794
|
-
if (!slug2) {
|
|
33795
|
-
console.error("customize resolve: slug required");
|
|
33796
|
-
return 1;
|
|
33797
|
-
}
|
|
33798
|
-
const result = resolveCustomizeFromRoot(slug2);
|
|
33799
|
-
if (json) {
|
|
33800
|
-
console.log(JSON.stringify(result, null, 2));
|
|
33801
|
-
} else if (result.content) {
|
|
33802
|
-
process7.stdout.write(result.content);
|
|
33803
|
-
}
|
|
33804
|
-
return 0;
|
|
33805
|
-
}
|
|
33806
|
-
function handleHook(argv) {
|
|
33807
|
-
if (argv[0] !== "customize") {
|
|
33808
|
-
usage();
|
|
33809
|
-
return 1;
|
|
33810
|
-
}
|
|
33811
|
-
process7.stdout.write(runCustomizeHook(readFileSync22(0, "utf8")));
|
|
33812
|
-
return 0;
|
|
33665
|
+
function handleRemovedOverlay(command) {
|
|
33666
|
+
console.error(`${command}: removed — overlay inject is gone. Edit the skill in its owning repo.`);
|
|
33667
|
+
return 1;
|
|
33813
33668
|
}
|
|
33814
33669
|
function handleInit(argv) {
|
|
33815
33670
|
runInit(parseInitArgs(argv));
|
|
@@ -33830,9 +33685,8 @@ async function dispatchCommand(argv) {
|
|
|
33830
33685
|
case "catalog":
|
|
33831
33686
|
return handleCatalog(rest);
|
|
33832
33687
|
case "customize":
|
|
33833
|
-
return rest[0] === "resolve" ? handleCustomizeResolve(rest.slice(1)) : null;
|
|
33834
33688
|
case "hook":
|
|
33835
|
-
return
|
|
33689
|
+
return handleRemovedOverlay(command);
|
|
33836
33690
|
case "init":
|
|
33837
33691
|
return handleInit(rest);
|
|
33838
33692
|
default:
|
|
@@ -33840,22 +33694,22 @@ async function dispatchCommand(argv) {
|
|
|
33840
33694
|
}
|
|
33841
33695
|
}
|
|
33842
33696
|
async function main() {
|
|
33843
|
-
const argv =
|
|
33697
|
+
const argv = process5.argv.slice(2);
|
|
33844
33698
|
const command = argv[0];
|
|
33845
33699
|
if (!command || command === "--help" || command === "-h") {
|
|
33846
33700
|
usage();
|
|
33847
|
-
|
|
33701
|
+
process5.exit(command ? 0 : 1);
|
|
33848
33702
|
}
|
|
33849
33703
|
try {
|
|
33850
33704
|
const exitCode = await dispatchCommand(argv);
|
|
33851
33705
|
if (exitCode === null) {
|
|
33852
33706
|
usage();
|
|
33853
|
-
|
|
33707
|
+
process5.exit(1);
|
|
33854
33708
|
}
|
|
33855
|
-
|
|
33709
|
+
process5.exit(exitCode);
|
|
33856
33710
|
} catch (error) {
|
|
33857
33711
|
console.error(String(error));
|
|
33858
|
-
|
|
33712
|
+
process5.exit(1);
|
|
33859
33713
|
}
|
|
33860
33714
|
}
|
|
33861
33715
|
main();
|