@rhize/skill-forge 0.9.0 → 0.10.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 +4 -0
- package/README.md +125 -5
- package/dist/cli.js +1510 -3
- package/dist/cli.js.map +1 -1
- package/dist/hooks/refinement-detector.sh +115 -0
- package/dist/hooks/session-end.sh +122 -0
- package/dist/ingest-prompt.md +6 -6
- package/dist/refine-prompt.md +367 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1370,14 +1370,14 @@ skill skills user claude code based comprehensive enhanced practical including i
|
|
|
1370
1370
|
`.split(/\s+/).filter(Boolean)
|
|
1371
1371
|
);
|
|
1372
1372
|
var DEFAULT_SET_THRESHOLD = 0.45;
|
|
1373
|
-
function splitLimited(text,
|
|
1373
|
+
function splitLimited(text, sep3, maxSplit) {
|
|
1374
1374
|
const parts = [];
|
|
1375
1375
|
let rest = text;
|
|
1376
1376
|
for (let i = 0; i < maxSplit; i++) {
|
|
1377
|
-
const idx = rest.indexOf(
|
|
1377
|
+
const idx = rest.indexOf(sep3);
|
|
1378
1378
|
if (idx === -1) break;
|
|
1379
1379
|
parts.push(rest.slice(0, idx));
|
|
1380
|
-
rest = rest.slice(idx +
|
|
1380
|
+
rest = rest.slice(idx + sep3.length);
|
|
1381
1381
|
}
|
|
1382
1382
|
parts.push(rest);
|
|
1383
1383
|
return parts;
|
|
@@ -5022,6 +5022,1506 @@ async function queueClose(id, options = {}) {
|
|
|
5022
5022
|
console.log(`[skill-forge] queue entry ${updated.id} closed -> status: ${updated.status}`);
|
|
5023
5023
|
}
|
|
5024
5024
|
|
|
5025
|
+
// src/commands/refine.ts
|
|
5026
|
+
import { createHash as createHash3, randomUUID as randomUUID4 } from "crypto";
|
|
5027
|
+
import { chmodSync, existsSync as existsSync20, lstatSync as lstatSync3, mkdirSync as mkdirSync9, readFileSync as readFileSync15, realpathSync as realpathSync7, renameSync as renameSync5, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
5028
|
+
import { dirname as dirname11, isAbsolute as isAbsolute3, join as join25, relative as relative3, resolve as resolvePath11, sep as sep2 } from "path";
|
|
5029
|
+
import { createInterface as createInterface5 } from "readline/promises";
|
|
5030
|
+
|
|
5031
|
+
// src/refine/context.ts
|
|
5032
|
+
import { existsSync as existsSync19, realpathSync as realpathSync6, statSync as statSync11 } from "fs";
|
|
5033
|
+
import { isAbsolute as isAbsolute2, join as join24, relative as relative2, resolve as resolvePath10 } from "path";
|
|
5034
|
+
|
|
5035
|
+
// src/refine/models.ts
|
|
5036
|
+
import { randomBytes } from "crypto";
|
|
5037
|
+
var REFINEMENT_CATEGORIES = ["trigger", "content", "hook", "tool", "pattern", "config", "new"];
|
|
5038
|
+
var OVERRIDE_TYPES = ["patch", "extend", "config", "full", "hook", "script", "new"];
|
|
5039
|
+
var PATCH_ACTIONS = ["append", "prepend", "replace-section", "insert-after", "insert-before", "delete-section"];
|
|
5040
|
+
var PATTERN_STATUSES = ["tracking", "ready", "generalized", "dismissed"];
|
|
5041
|
+
function isRefinementCategory(value) {
|
|
5042
|
+
return REFINEMENT_CATEGORIES.includes(value);
|
|
5043
|
+
}
|
|
5044
|
+
function isOverrideType(value) {
|
|
5045
|
+
return OVERRIDE_TYPES.includes(value);
|
|
5046
|
+
}
|
|
5047
|
+
function isPatchAction(value) {
|
|
5048
|
+
return PATCH_ACTIONS.includes(value);
|
|
5049
|
+
}
|
|
5050
|
+
function isPatternStatus(value) {
|
|
5051
|
+
return PATTERN_STATUSES.includes(value);
|
|
5052
|
+
}
|
|
5053
|
+
var REFINEMENT_STATUSES = ["pending", "applied", "generalized"];
|
|
5054
|
+
function isRefinementStatus(value) {
|
|
5055
|
+
return REFINEMENT_STATUSES.includes(value);
|
|
5056
|
+
}
|
|
5057
|
+
function patternCount(pattern) {
|
|
5058
|
+
return new Set(pattern.occurrences.map((o) => o.projectRealpath)).size;
|
|
5059
|
+
}
|
|
5060
|
+
function defaultRandomHex() {
|
|
5061
|
+
return randomBytes(2).toString("hex");
|
|
5062
|
+
}
|
|
5063
|
+
function formatYYYYMMDD(date) {
|
|
5064
|
+
const yyyy = date.getUTCFullYear().toString().padStart(4, "0");
|
|
5065
|
+
const mm = (date.getUTCMonth() + 1).toString().padStart(2, "0");
|
|
5066
|
+
const dd = date.getUTCDate().toString().padStart(2, "0");
|
|
5067
|
+
return `${yyyy}${mm}${dd}`;
|
|
5068
|
+
}
|
|
5069
|
+
function generateRefinementId(existingIds = /* @__PURE__ */ new Set(), now = /* @__PURE__ */ new Date(), randomHex = defaultRandomHex) {
|
|
5070
|
+
const datePart = formatYYYYMMDD(now);
|
|
5071
|
+
let id;
|
|
5072
|
+
do {
|
|
5073
|
+
id = `REF-${datePart}-${randomHex()}`;
|
|
5074
|
+
} while (existingIds.has(id));
|
|
5075
|
+
return id;
|
|
5076
|
+
}
|
|
5077
|
+
function generatePatternId(existingIds = /* @__PURE__ */ new Set(), randomHex = defaultRandomHex) {
|
|
5078
|
+
let id;
|
|
5079
|
+
do {
|
|
5080
|
+
id = `PAT-${randomHex()}`;
|
|
5081
|
+
} while (existingIds.has(id));
|
|
5082
|
+
return id;
|
|
5083
|
+
}
|
|
5084
|
+
function fingerprintKey(skill, target, overrideType, action) {
|
|
5085
|
+
const normalize = (s) => s.trim().toLowerCase().replace(/\s+/g, " ");
|
|
5086
|
+
return [normalize(skill), normalize(target), overrideType, action ?? ""].join("::");
|
|
5087
|
+
}
|
|
5088
|
+
function contentSimilarity(a, b) {
|
|
5089
|
+
const tokenize2 = (s) => new Set(s.toLowerCase().split(/\s+/).filter(Boolean));
|
|
5090
|
+
const tokensA = tokenize2(a);
|
|
5091
|
+
const tokensB = tokenize2(b);
|
|
5092
|
+
if (tokensA.size === 0 || tokensB.size === 0) return 0;
|
|
5093
|
+
let intersectionSize = 0;
|
|
5094
|
+
for (const token of tokensA) {
|
|
5095
|
+
if (tokensB.has(token)) intersectionSize++;
|
|
5096
|
+
}
|
|
5097
|
+
const unionSize2 = tokensA.size + tokensB.size - intersectionSize;
|
|
5098
|
+
return unionSize2 === 0 ? 0 : intersectionSize / unionSize2;
|
|
5099
|
+
}
|
|
5100
|
+
|
|
5101
|
+
// src/refine/store.ts
|
|
5102
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
5103
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync8, readFileSync as readFileSync14, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync10, writeFileSync as writeFileSync8, realpathSync as realpathSync5 } from "fs";
|
|
5104
|
+
import { basename as basename9, dirname as dirname10, join as join23, resolve as resolvePath9 } from "path";
|
|
5105
|
+
var SIMILARITY_JOIN_THRESHOLD = 0.7;
|
|
5106
|
+
var READY_THRESHOLD = 2;
|
|
5107
|
+
function getRefinementsDir() {
|
|
5108
|
+
return join23(getHomeDir(), "refinements");
|
|
5109
|
+
}
|
|
5110
|
+
function getRefinementsPath() {
|
|
5111
|
+
return join23(getRefinementsDir(), "refinements.json");
|
|
5112
|
+
}
|
|
5113
|
+
function getPatternsPath() {
|
|
5114
|
+
return join23(getRefinementsDir(), "patterns.json");
|
|
5115
|
+
}
|
|
5116
|
+
function getBackupsDir() {
|
|
5117
|
+
return join23(getRefinementsDir(), "backups");
|
|
5118
|
+
}
|
|
5119
|
+
function writeJsonAtomic(path, data) {
|
|
5120
|
+
const dir = dirname10(path);
|
|
5121
|
+
mkdirSync8(dir, { recursive: true });
|
|
5122
|
+
const tempPath = join23(dir, `.${basename9(path)}.tmp-${randomUUID3()}`);
|
|
5123
|
+
try {
|
|
5124
|
+
writeFileSync8(tempPath, JSON.stringify(data, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
5125
|
+
renameSync4(tempPath, path);
|
|
5126
|
+
} catch (err) {
|
|
5127
|
+
rmSync6(tempPath, { force: true });
|
|
5128
|
+
throw err;
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
function readJsonFile(path, label) {
|
|
5132
|
+
let raw;
|
|
5133
|
+
try {
|
|
5134
|
+
raw = readFileSync14(path, "utf-8");
|
|
5135
|
+
} catch (err) {
|
|
5136
|
+
throw new UserError(`unable to read ${label} at ${path}: ${err.message}`);
|
|
5137
|
+
}
|
|
5138
|
+
try {
|
|
5139
|
+
return JSON.parse(raw);
|
|
5140
|
+
} catch {
|
|
5141
|
+
throw new UserError(`malformed ${label} at ${path}: not valid JSON`);
|
|
5142
|
+
}
|
|
5143
|
+
}
|
|
5144
|
+
function readStore(path, label, validateRecord) {
|
|
5145
|
+
if (!existsSync18(path)) {
|
|
5146
|
+
return { schemaVersion: 1, records: [] };
|
|
5147
|
+
}
|
|
5148
|
+
const parsed = readJsonFile(path, label);
|
|
5149
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.records)) {
|
|
5150
|
+
throw new UserError(`malformed ${label} at ${path}: expected an object with a "records" array`);
|
|
5151
|
+
}
|
|
5152
|
+
const records = parsed.records;
|
|
5153
|
+
if (validateRecord) {
|
|
5154
|
+
records.forEach((record, index) => validateRecord(record, index));
|
|
5155
|
+
}
|
|
5156
|
+
return { schemaVersion: 1, records };
|
|
5157
|
+
}
|
|
5158
|
+
function validateRefinementRecordShape(record, index, path) {
|
|
5159
|
+
if (typeof record !== "object" || record === null) {
|
|
5160
|
+
throw new UserError(`malformed refinements.json at ${path}: record[${index}] is not an object`);
|
|
5161
|
+
}
|
|
5162
|
+
const r = record;
|
|
5163
|
+
if (typeof r.id !== "string" || r.id.length === 0) {
|
|
5164
|
+
throw new UserError(`malformed refinements.json at ${path}: record[${index}] is missing a valid "id"`);
|
|
5165
|
+
}
|
|
5166
|
+
if (typeof r.createdAt !== "string" || r.createdAt.length === 0) {
|
|
5167
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "createdAt"`);
|
|
5168
|
+
}
|
|
5169
|
+
if (typeof r.status !== "string" || r.status.length === 0) {
|
|
5170
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "status"`);
|
|
5171
|
+
}
|
|
5172
|
+
if (typeof r.skill !== "string" || r.skill.length === 0) {
|
|
5173
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "skill"`);
|
|
5174
|
+
}
|
|
5175
|
+
const patchActionsRequiringMarker = /* @__PURE__ */ new Set(["insert-after", "insert-before", "replace-section", "delete-section"]);
|
|
5176
|
+
if (r.overrideType === "patch") {
|
|
5177
|
+
if (typeof r.patchAction !== "string" || r.patchAction.length === 0) {
|
|
5178
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchAction"`);
|
|
5179
|
+
}
|
|
5180
|
+
if (patchActionsRequiringMarker.has(r.patchAction) && (typeof r.patchMarker !== "string" || r.patchMarker.length === 0)) {
|
|
5181
|
+
throw new UserError(
|
|
5182
|
+
`malformed refinements.json at ${path}: record "${r.id}" has patchAction "${r.patchAction}" but is missing "patchMarker"`
|
|
5183
|
+
);
|
|
5184
|
+
}
|
|
5185
|
+
if (typeof r.patchContent !== "string" || r.patchContent.length === 0) {
|
|
5186
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchContent"`);
|
|
5187
|
+
}
|
|
5188
|
+
} else if (r.patchAction !== void 0) {
|
|
5189
|
+
throw new UserError(
|
|
5190
|
+
`malformed refinements.json at ${path}: record "${r.id}" has overrideType "${String(r.overrideType)}" but carries "patchAction" \u2014 only valid when overrideType is "patch"`
|
|
5191
|
+
);
|
|
5192
|
+
}
|
|
5193
|
+
}
|
|
5194
|
+
function validatePatternRecordShape(record, index, path) {
|
|
5195
|
+
if (typeof record !== "object" || record === null) {
|
|
5196
|
+
throw new UserError(`malformed patterns.json at ${path}: record[${index}] is not an object`);
|
|
5197
|
+
}
|
|
5198
|
+
const p = record;
|
|
5199
|
+
if (typeof p.id !== "string" || p.id.length === 0) {
|
|
5200
|
+
throw new UserError(`malformed patterns.json at ${path}: record[${index}] is missing a valid "id"`);
|
|
5201
|
+
}
|
|
5202
|
+
if (typeof p.status !== "string" || p.status.length === 0) {
|
|
5203
|
+
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" is missing a valid "status"`);
|
|
5204
|
+
}
|
|
5205
|
+
if (!Array.isArray(p.occurrences)) {
|
|
5206
|
+
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" is missing a valid "occurrences" array`);
|
|
5207
|
+
}
|
|
5208
|
+
if (p.status === "dismissed" && (typeof p.dismissedReason !== "string" || p.dismissedReason.length === 0)) {
|
|
5209
|
+
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "dismissed" but is missing "dismissedReason"`);
|
|
5210
|
+
}
|
|
5211
|
+
if (p.status === "generalized") {
|
|
5212
|
+
if (typeof p.generalizedAt !== "string" || p.generalizedAt.length === 0) {
|
|
5213
|
+
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "generalized" but is missing "generalizedAt"`);
|
|
5214
|
+
}
|
|
5215
|
+
if (typeof p.generalizedBackupId !== "string" || p.generalizedBackupId.length === 0) {
|
|
5216
|
+
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "generalized" but is missing "generalizedBackupId"`);
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
5220
|
+
function readRefinementsStore() {
|
|
5221
|
+
const path = getRefinementsPath();
|
|
5222
|
+
return readStore(path, "refinements.json", (record, index) => validateRefinementRecordShape(record, index, path));
|
|
5223
|
+
}
|
|
5224
|
+
function writeRefinementsStore(store) {
|
|
5225
|
+
writeJsonAtomic(getRefinementsPath(), store);
|
|
5226
|
+
}
|
|
5227
|
+
function readPatternsStore() {
|
|
5228
|
+
const path = getPatternsPath();
|
|
5229
|
+
return readStore(path, "patterns.json", (record, index) => validatePatternRecordShape(record, index, path));
|
|
5230
|
+
}
|
|
5231
|
+
function writePatternsStore(store) {
|
|
5232
|
+
writeJsonAtomic(getPatternsPath(), store);
|
|
5233
|
+
}
|
|
5234
|
+
function resolveProjectRealpath(projectRoot) {
|
|
5235
|
+
const resolved = resolvePath9(projectRoot);
|
|
5236
|
+
try {
|
|
5237
|
+
return realpathSync5(resolved);
|
|
5238
|
+
} catch {
|
|
5239
|
+
return resolved;
|
|
5240
|
+
}
|
|
5241
|
+
}
|
|
5242
|
+
function generatePatternName(refinement) {
|
|
5243
|
+
const parts = refinement.targetSection.replace(/\//g, " ").replace(/-/g, " ").split(/\s+/).filter(Boolean);
|
|
5244
|
+
const lastThree = parts.slice(-3);
|
|
5245
|
+
if (lastThree.length === 0) return refinement.targetSection || `pattern-${refinement.skill}`;
|
|
5246
|
+
return lastThree.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
5247
|
+
}
|
|
5248
|
+
function generatePatternDescription(refinement) {
|
|
5249
|
+
const category = refinement.category.charAt(0).toUpperCase() + refinement.category.slice(1);
|
|
5250
|
+
const cause = refinement.rootCause || refinement.expectedBehavior;
|
|
5251
|
+
return `${category} modification for ${refinement.skill}: ${cause}`;
|
|
5252
|
+
}
|
|
5253
|
+
function upsertPattern(records, refinement, projectRealpath, nowIso, randomHex, explicitPatternId) {
|
|
5254
|
+
let pattern;
|
|
5255
|
+
if (explicitPatternId) {
|
|
5256
|
+
pattern = records.find((p) => p.id === explicitPatternId);
|
|
5257
|
+
if (!pattern) {
|
|
5258
|
+
throw new UserError(`no pattern with id: ${explicitPatternId}`);
|
|
5259
|
+
}
|
|
5260
|
+
if (pattern.status === "dismissed" || pattern.status === "generalized") {
|
|
5261
|
+
throw new UserError(`cannot join pattern ${explicitPatternId}: it is ${pattern.status} and no longer accepts new occurrences`);
|
|
5262
|
+
}
|
|
5263
|
+
} else {
|
|
5264
|
+
const joinable = records.filter((p) => p.status !== "generalized" && p.status !== "dismissed");
|
|
5265
|
+
const fingerprint = fingerprintKey(refinement.skill, refinement.targetSection, refinement.overrideType, refinement.patchAction);
|
|
5266
|
+
pattern = joinable.find((p) => p.fingerprint === fingerprint);
|
|
5267
|
+
if (!pattern && refinement.patchContent) {
|
|
5268
|
+
pattern = joinable.find((p) => {
|
|
5269
|
+
const sample = p.proposedChanges.content;
|
|
5270
|
+
return sample ? contentSimilarity(refinement.patchContent, sample) > SIMILARITY_JOIN_THRESHOLD : false;
|
|
5271
|
+
});
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
5274
|
+
const wasTracking = pattern?.status === "tracking";
|
|
5275
|
+
if (!pattern) {
|
|
5276
|
+
const created = {
|
|
5277
|
+
id: generatePatternId(new Set(records.map((p) => p.id)), randomHex),
|
|
5278
|
+
name: generatePatternName(refinement),
|
|
5279
|
+
description: generatePatternDescription(refinement),
|
|
5280
|
+
affectedSkills: [refinement.skill],
|
|
5281
|
+
refinementIds: [refinement.id],
|
|
5282
|
+
occurrences: [{ refinementId: refinement.id, projectRealpath, timestamp: nowIso }],
|
|
5283
|
+
status: "tracking",
|
|
5284
|
+
proposedChanges: refinement.patchContent ? { content: refinement.patchContent } : {},
|
|
5285
|
+
fingerprint: fingerprintKey(refinement.skill, refinement.targetSection, refinement.overrideType, refinement.patchAction),
|
|
5286
|
+
firstSeen: nowIso,
|
|
5287
|
+
lastSeen: nowIso
|
|
5288
|
+
};
|
|
5289
|
+
records.push(created);
|
|
5290
|
+
return { pattern: created, becameReady: false };
|
|
5291
|
+
}
|
|
5292
|
+
if (!pattern.affectedSkills.includes(refinement.skill)) pattern.affectedSkills.push(refinement.skill);
|
|
5293
|
+
if (!pattern.refinementIds.includes(refinement.id)) pattern.refinementIds.push(refinement.id);
|
|
5294
|
+
pattern.occurrences.push({ refinementId: refinement.id, projectRealpath, timestamp: nowIso });
|
|
5295
|
+
pattern.lastSeen = nowIso;
|
|
5296
|
+
const becameReady = wasTracking && patternCount(pattern) >= READY_THRESHOLD;
|
|
5297
|
+
if (becameReady) pattern.status = "ready";
|
|
5298
|
+
return { pattern, becameReady };
|
|
5299
|
+
}
|
|
5300
|
+
function recordRefinement(input, deps = {}) {
|
|
5301
|
+
const now = deps.now ?? /* @__PURE__ */ new Date();
|
|
5302
|
+
const nowIso = now.toISOString();
|
|
5303
|
+
const refinementsStore = readRefinementsStore();
|
|
5304
|
+
const patternsStore = readPatternsStore();
|
|
5305
|
+
const existingRefIds = new Set(refinementsStore.records.map((r) => r.id));
|
|
5306
|
+
const refinementId = deps.id ?? generateRefinementId(existingRefIds, now, deps.randomHex);
|
|
5307
|
+
const projectRealpath = resolveProjectRealpath(input.projectRoot);
|
|
5308
|
+
const refinement = {
|
|
5309
|
+
id: refinementId,
|
|
5310
|
+
skill: input.skill,
|
|
5311
|
+
category: input.category,
|
|
5312
|
+
overrideType: input.overrideType,
|
|
5313
|
+
targetSection: input.targetSection,
|
|
5314
|
+
expectedBehavior: input.expectedBehavior,
|
|
5315
|
+
actualBehavior: input.actualBehavior,
|
|
5316
|
+
example: input.example,
|
|
5317
|
+
desiredOutcome: input.desiredOutcome,
|
|
5318
|
+
rootCause: input.rootCause,
|
|
5319
|
+
confidence: input.confidence,
|
|
5320
|
+
project: input.project,
|
|
5321
|
+
projectRoot: projectRealpath,
|
|
5322
|
+
patchAction: input.patchAction,
|
|
5323
|
+
patchMarker: input.patchMarker,
|
|
5324
|
+
patchContent: input.patchContent,
|
|
5325
|
+
generalizationPotential: input.generalizationPotential,
|
|
5326
|
+
scope: input.scope,
|
|
5327
|
+
status: input.status ?? "applied",
|
|
5328
|
+
createdAt: nowIso
|
|
5329
|
+
};
|
|
5330
|
+
const { pattern, becameReady } = upsertPattern(patternsStore.records, refinement, projectRealpath, nowIso, deps.randomHex, input.patternId);
|
|
5331
|
+
refinement.patternId = pattern.id;
|
|
5332
|
+
refinementsStore.records.push(refinement);
|
|
5333
|
+
writeRefinementsStore(refinementsStore);
|
|
5334
|
+
try {
|
|
5335
|
+
writePatternsStore(patternsStore);
|
|
5336
|
+
} catch (err) {
|
|
5337
|
+
throw new UserError(
|
|
5338
|
+
`refinements.json was updated with refinement ${refinement.id}, but patterns.json failed to update: ${err.message}`
|
|
5339
|
+
);
|
|
5340
|
+
}
|
|
5341
|
+
return { refinement, pattern, patternBecameReady: becameReady };
|
|
5342
|
+
}
|
|
5343
|
+
function markGeneralized(id, backupId) {
|
|
5344
|
+
const store = readPatternsStore();
|
|
5345
|
+
const pattern = store.records.find((p) => p.id === id);
|
|
5346
|
+
if (!pattern) {
|
|
5347
|
+
throw new UserError(`no pattern with id: ${id}`);
|
|
5348
|
+
}
|
|
5349
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
5350
|
+
pattern.status = "generalized";
|
|
5351
|
+
pattern.generalizedAt = nowIso;
|
|
5352
|
+
pattern.generalizedBackupId = backupId;
|
|
5353
|
+
pattern.lastSeen = nowIso;
|
|
5354
|
+
writePatternsStore(store);
|
|
5355
|
+
return pattern;
|
|
5356
|
+
}
|
|
5357
|
+
function formatBackupTimestamp(date) {
|
|
5358
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
5359
|
+
}
|
|
5360
|
+
var BACKUP_ID_PATTERN = /^\d{8}T\d{6}Z-[0-9a-f]{4}$/;
|
|
5361
|
+
function isValidBackupId(id) {
|
|
5362
|
+
return BACKUP_ID_PATTERN.test(id);
|
|
5363
|
+
}
|
|
5364
|
+
function createBackup(filePaths, deps = {}) {
|
|
5365
|
+
const now = deps.now ?? /* @__PURE__ */ new Date();
|
|
5366
|
+
const randomHex = deps.randomHex ?? defaultRandomHex;
|
|
5367
|
+
const id = `${formatBackupTimestamp(now)}-${randomHex()}`;
|
|
5368
|
+
const files = filePaths.map((path) => {
|
|
5369
|
+
if (!existsSync18(path)) {
|
|
5370
|
+
return { path, existed: false, sha256: null, mode: null, contentBase64: null };
|
|
5371
|
+
}
|
|
5372
|
+
const bytes = readFileSync14(path);
|
|
5373
|
+
const mode = statSync10(path).mode & 511;
|
|
5374
|
+
const sha256 = createHash2("sha256").update(bytes).digest("hex");
|
|
5375
|
+
return { path, existed: true, sha256, mode, contentBase64: bytes.toString("base64") };
|
|
5376
|
+
});
|
|
5377
|
+
const manifest = { id, createdAt: now.toISOString(), files };
|
|
5378
|
+
writeJsonAtomic(join23(getBackupsDir(), id, "manifest.json"), manifest);
|
|
5379
|
+
return manifest;
|
|
5380
|
+
}
|
|
5381
|
+
function validateBackupManifestShape(value, path) {
|
|
5382
|
+
if (typeof value !== "object" || value === null) {
|
|
5383
|
+
throw new UserError(`malformed backup manifest at ${path}: not an object`);
|
|
5384
|
+
}
|
|
5385
|
+
const m = value;
|
|
5386
|
+
if (typeof m.id !== "string" || m.id.length === 0) {
|
|
5387
|
+
throw new UserError(`malformed backup manifest at ${path}: missing a valid "id"`);
|
|
5388
|
+
}
|
|
5389
|
+
if (typeof m.createdAt !== "string" || m.createdAt.length === 0) {
|
|
5390
|
+
throw new UserError(`malformed backup manifest at ${path}: missing a valid "createdAt"`);
|
|
5391
|
+
}
|
|
5392
|
+
if (!Array.isArray(m.files)) {
|
|
5393
|
+
throw new UserError(`malformed backup manifest at ${path}: missing a valid "files" array`);
|
|
5394
|
+
}
|
|
5395
|
+
m.files.forEach((entry, index) => {
|
|
5396
|
+
if (typeof entry !== "object" || entry === null) {
|
|
5397
|
+
throw new UserError(`malformed backup manifest at ${path}: files[${index}] is not an object`);
|
|
5398
|
+
}
|
|
5399
|
+
const f = entry;
|
|
5400
|
+
if (typeof f.path !== "string" || f.path.length === 0) {
|
|
5401
|
+
throw new UserError(`malformed backup manifest at ${path}: files[${index}] is missing a valid "path"`);
|
|
5402
|
+
}
|
|
5403
|
+
if (typeof f.existed !== "boolean") {
|
|
5404
|
+
throw new UserError(`malformed backup manifest at ${path}: files[${index}] ("${f.path}") is missing a valid "existed" (must be boolean)`);
|
|
5405
|
+
}
|
|
5406
|
+
if (f.existed) {
|
|
5407
|
+
if (typeof f.sha256 !== "string" || f.sha256.length === 0) {
|
|
5408
|
+
throw new UserError(`malformed backup manifest at ${path}: files[${index}] ("${f.path}") has existed:true but is missing "sha256"`);
|
|
5409
|
+
}
|
|
5410
|
+
if (typeof f.mode !== "number") {
|
|
5411
|
+
throw new UserError(`malformed backup manifest at ${path}: files[${index}] ("${f.path}") has existed:true but is missing a numeric "mode"`);
|
|
5412
|
+
}
|
|
5413
|
+
if (typeof f.contentBase64 !== "string") {
|
|
5414
|
+
throw new UserError(`malformed backup manifest at ${path}: files[${index}] ("${f.path}") has existed:true but is missing "contentBase64"`);
|
|
5415
|
+
}
|
|
5416
|
+
} else {
|
|
5417
|
+
if (f.sha256 !== null || f.mode !== null || f.contentBase64 !== null) {
|
|
5418
|
+
throw new UserError(`malformed backup manifest at ${path}: files[${index}] ("${f.path}") has existed:false (a tombstone) but sha256/mode/contentBase64 are not all null`);
|
|
5419
|
+
}
|
|
5420
|
+
}
|
|
5421
|
+
});
|
|
5422
|
+
}
|
|
5423
|
+
function readBackupManifest(id) {
|
|
5424
|
+
if (!isValidBackupId(id)) {
|
|
5425
|
+
throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
|
|
5426
|
+
}
|
|
5427
|
+
const manifestPath = join23(getBackupsDir(), id, "manifest.json");
|
|
5428
|
+
if (!existsSync18(manifestPath)) {
|
|
5429
|
+
throw new UserError(`no backup with id: ${id}`);
|
|
5430
|
+
}
|
|
5431
|
+
const parsed = readJsonFile(manifestPath, "backup manifest");
|
|
5432
|
+
validateBackupManifestShape(parsed, manifestPath);
|
|
5433
|
+
return parsed;
|
|
5434
|
+
}
|
|
5435
|
+
|
|
5436
|
+
// src/refine/context.ts
|
|
5437
|
+
function resolveConfiguredRoot2(root) {
|
|
5438
|
+
return resolvePath10(expandHome(root));
|
|
5439
|
+
}
|
|
5440
|
+
function assertPlainSkillName(name) {
|
|
5441
|
+
if (!name || name.trim().length === 0) {
|
|
5442
|
+
throw new UserError("skill name must not be empty");
|
|
5443
|
+
}
|
|
5444
|
+
if (name.includes("/") || name.includes("\\") || name === "." || name === "..") {
|
|
5445
|
+
throw new UserError(`invalid skill name "${name}" \u2014 must be a plain directory name, not a path`);
|
|
5446
|
+
}
|
|
5447
|
+
}
|
|
5448
|
+
function realpathOrNull2(p) {
|
|
5449
|
+
try {
|
|
5450
|
+
return realpathSync6(p);
|
|
5451
|
+
} catch {
|
|
5452
|
+
return null;
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
function isUnderRoot2(candidate, root) {
|
|
5456
|
+
const rel = relative2(root, candidate);
|
|
5457
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
5458
|
+
}
|
|
5459
|
+
function resolveScopes(config, cwd, skill, userRootFlag) {
|
|
5460
|
+
assertPlainSkillName(skill);
|
|
5461
|
+
const resolvedCwd = resolvePath10(cwd);
|
|
5462
|
+
const localDir = join24(resolvedCwd, ".claude", "skills", skill);
|
|
5463
|
+
const sharedDir = join24(resolvedCwd, "skills", skill);
|
|
5464
|
+
const local = { scope: "local", dir: localDir, exists: existsSync19(localDir) };
|
|
5465
|
+
const shared = { scope: "shared", dir: sharedDir, exists: existsSync19(sharedDir) };
|
|
5466
|
+
const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
|
|
5467
|
+
return { local, shared, user };
|
|
5468
|
+
}
|
|
5469
|
+
function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
|
|
5470
|
+
if (userRootFlag) {
|
|
5471
|
+
const root = resolvePath10(userRootFlag);
|
|
5472
|
+
return { resolved: true, dir: join24(root, skill), root: userRootFlag, reason: "flag" };
|
|
5473
|
+
}
|
|
5474
|
+
const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
|
|
5475
|
+
const candidates = config.skillsRoots.map((root) => ({ root, dir: join24(resolveConfiguredRoot2(root), skill) }));
|
|
5476
|
+
for (const candidate of candidates) {
|
|
5477
|
+
const skillReal = realpathOrNull2(candidate.dir);
|
|
5478
|
+
if (skillReal) {
|
|
5479
|
+
return { resolved: true, dir: candidate.dir, root: candidate.root, reason: "contains-skill" };
|
|
5480
|
+
}
|
|
5481
|
+
}
|
|
5482
|
+
const externalCandidates = candidates.filter((candidate) => {
|
|
5483
|
+
const rootReal = realpathOrNull2(resolveConfiguredRoot2(candidate.root)) ?? resolveConfiguredRoot2(candidate.root);
|
|
5484
|
+
return !isUnderRoot2(rootReal, cwdReal) && !isUnderRoot2(cwdReal, rootReal);
|
|
5485
|
+
});
|
|
5486
|
+
if (externalCandidates.length === 1) {
|
|
5487
|
+
return { resolved: true, dir: externalCandidates[0].dir, root: externalCandidates[0].root, reason: "sole-external-root" };
|
|
5488
|
+
}
|
|
5489
|
+
return { resolved: false, candidates: externalCandidates.length > 0 ? externalCandidates : candidates };
|
|
5490
|
+
}
|
|
5491
|
+
function isFile2(p) {
|
|
5492
|
+
try {
|
|
5493
|
+
return statSync11(p).isFile();
|
|
5494
|
+
} catch {
|
|
5495
|
+
return false;
|
|
5496
|
+
}
|
|
5497
|
+
}
|
|
5498
|
+
function isDirectory(p) {
|
|
5499
|
+
try {
|
|
5500
|
+
return statSync11(p).isDirectory();
|
|
5501
|
+
} catch {
|
|
5502
|
+
return false;
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
function inventoryScopeDir(scope, dir) {
|
|
5506
|
+
return {
|
|
5507
|
+
scope,
|
|
5508
|
+
dir,
|
|
5509
|
+
dirExists: isDirectory(dir),
|
|
5510
|
+
skillMd: isFile2(join24(dir, "SKILL.md")),
|
|
5511
|
+
patchMd: isFile2(join24(dir, "SKILL.patch.md")),
|
|
5512
|
+
extendMd: isFile2(join24(dir, "SKILL.extend.md")),
|
|
5513
|
+
configJson: isFile2(join24(dir, "skill-config.json")),
|
|
5514
|
+
hooksDir: isDirectory(join24(dir, "hooks")),
|
|
5515
|
+
scriptsDir: isDirectory(join24(dir, "scripts"))
|
|
5516
|
+
};
|
|
5517
|
+
}
|
|
5518
|
+
function inventoryAllScopes(resolution) {
|
|
5519
|
+
const rows = [inventoryScopeDir("local", resolution.local.dir), inventoryScopeDir("shared", resolution.shared.dir)];
|
|
5520
|
+
if (resolution.user.resolved) {
|
|
5521
|
+
rows.push(inventoryScopeDir("user", resolution.user.dir));
|
|
5522
|
+
}
|
|
5523
|
+
return rows;
|
|
5524
|
+
}
|
|
5525
|
+
|
|
5526
|
+
// src/refine/patch.ts
|
|
5527
|
+
var FENCE_RE = /^\s*(```|~~~)/;
|
|
5528
|
+
var HEADING_RE = /^(#{1,6})\s+(.*)$/;
|
|
5529
|
+
function frontmatterOffset(lines) {
|
|
5530
|
+
if (lines[0] !== "---") return 0;
|
|
5531
|
+
for (let i = 1; i < lines.length; i++) {
|
|
5532
|
+
if (lines[i] === "---") return i + 1;
|
|
5533
|
+
}
|
|
5534
|
+
return 0;
|
|
5535
|
+
}
|
|
5536
|
+
function buildHeadingTree(lines, bodyOffset) {
|
|
5537
|
+
const flat = [];
|
|
5538
|
+
let inFence = false;
|
|
5539
|
+
for (let i = bodyOffset; i < lines.length; i++) {
|
|
5540
|
+
const line = lines[i];
|
|
5541
|
+
if (FENCE_RE.test(line)) {
|
|
5542
|
+
inFence = !inFence;
|
|
5543
|
+
continue;
|
|
5544
|
+
}
|
|
5545
|
+
if (inFence) continue;
|
|
5546
|
+
const m = HEADING_RE.exec(line);
|
|
5547
|
+
if (m) flat.push({ level: m[1].length, text: m[2].trim(), line: i });
|
|
5548
|
+
}
|
|
5549
|
+
const root = { level: 0, heading: "", headingLine: bodyOffset - 1, bodyStart: bodyOffset, bodyEnd: lines.length, children: [] };
|
|
5550
|
+
const stack = [root];
|
|
5551
|
+
for (const h of flat) {
|
|
5552
|
+
while (stack.length > 1 && stack[stack.length - 1].level >= h.level) {
|
|
5553
|
+
const done = stack.pop();
|
|
5554
|
+
if (done) done.bodyEnd = h.line;
|
|
5555
|
+
}
|
|
5556
|
+
const node = { level: h.level, heading: h.text, headingLine: h.line, bodyStart: h.line + 1, bodyEnd: lines.length, children: [] };
|
|
5557
|
+
stack[stack.length - 1].children.push(node);
|
|
5558
|
+
stack.push(node);
|
|
5559
|
+
}
|
|
5560
|
+
return root;
|
|
5561
|
+
}
|
|
5562
|
+
function normalizeText(s) {
|
|
5563
|
+
return s.trim().replace(/\s+/g, " ");
|
|
5564
|
+
}
|
|
5565
|
+
function splitHeadingPath(target) {
|
|
5566
|
+
return target.split(">").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
|
|
5567
|
+
}
|
|
5568
|
+
function collectDescendants(node, acc) {
|
|
5569
|
+
acc.push(node);
|
|
5570
|
+
for (const child of node.children) collectDescendants(child, acc);
|
|
5571
|
+
}
|
|
5572
|
+
function resolveHeadingPath(root, segments) {
|
|
5573
|
+
if (segments.length === 0) return root;
|
|
5574
|
+
const allNodes = [];
|
|
5575
|
+
collectDescendants(root, allNodes);
|
|
5576
|
+
const headingNodes = allNodes.filter((n) => n.level > 0);
|
|
5577
|
+
const firstNorm = normalizeText(segments[0]);
|
|
5578
|
+
let candidates = headingNodes.filter((n) => normalizeText(n.heading) === firstNorm);
|
|
5579
|
+
if (candidates.length === 0) {
|
|
5580
|
+
const available = headingNodes.map((n) => n.heading);
|
|
5581
|
+
throw new UserError(`No section matching "${segments[0]}" found (available headings: ${available.length > 0 ? available.join(", ") : "none"})`);
|
|
5582
|
+
}
|
|
5583
|
+
for (let i = 1; i < segments.length; i++) {
|
|
5584
|
+
const segment = segments[i];
|
|
5585
|
+
const norm = normalizeText(segment);
|
|
5586
|
+
const next = [];
|
|
5587
|
+
for (const candidate of candidates) {
|
|
5588
|
+
for (const child of candidate.children) {
|
|
5589
|
+
if (normalizeText(child.heading) === norm) next.push(child);
|
|
5590
|
+
}
|
|
5591
|
+
}
|
|
5592
|
+
if (next.length === 0) {
|
|
5593
|
+
const available = [...new Set(candidates.flatMap((c) => c.children.map((child) => child.heading)))];
|
|
5594
|
+
throw new UserError(`No section matching "${segment}" found (available headings: ${available.length > 0 ? available.join(", ") : "none"})`);
|
|
5595
|
+
}
|
|
5596
|
+
candidates = next;
|
|
5597
|
+
}
|
|
5598
|
+
if (candidates.length > 1) {
|
|
5599
|
+
throw new UserError(`Target selector "${segments.join(" > ")}" matched ${candidates.length} sections; be more specific`);
|
|
5600
|
+
}
|
|
5601
|
+
return candidates[0];
|
|
5602
|
+
}
|
|
5603
|
+
function findMarkerLine(lines, start, end, marker) {
|
|
5604
|
+
const norm = normalizeText(marker);
|
|
5605
|
+
const matches = [];
|
|
5606
|
+
for (let i = start; i < end; i++) {
|
|
5607
|
+
if (normalizeText(lines[i]).includes(norm)) matches.push(i);
|
|
5608
|
+
}
|
|
5609
|
+
if (matches.length === 0) {
|
|
5610
|
+
throw new UserError(`No line matching marker "${marker}" found in target section`);
|
|
5611
|
+
}
|
|
5612
|
+
if (matches.length > 1) {
|
|
5613
|
+
const preview = matches.map((i) => `line ${i + 1}: "${lines[i].trim()}"`).join("; ");
|
|
5614
|
+
throw new UserError(`Marker "${marker}" matched ${matches.length} lines, expected exactly 1 (${preview})`);
|
|
5615
|
+
}
|
|
5616
|
+
return matches[0];
|
|
5617
|
+
}
|
|
5618
|
+
function findChildByMarker(target, marker) {
|
|
5619
|
+
const norm = normalizeText(marker);
|
|
5620
|
+
const matches = target.children.filter((c) => normalizeText(c.heading).includes(norm));
|
|
5621
|
+
if (matches.length === 0) {
|
|
5622
|
+
const available = target.children.map((c) => c.heading).join(", ") || "none";
|
|
5623
|
+
throw new UserError(`No subsection matching "${marker}" found under target section (available: ${available})`);
|
|
5624
|
+
}
|
|
5625
|
+
if (matches.length > 1) {
|
|
5626
|
+
throw new UserError(`Marker "${marker}" matched ${matches.length} subsections: ${matches.map((c) => c.heading).join(", ")}`);
|
|
5627
|
+
}
|
|
5628
|
+
return matches[0];
|
|
5629
|
+
}
|
|
5630
|
+
function requireMarker(block) {
|
|
5631
|
+
if (!block.marker || block.marker.trim().length === 0) {
|
|
5632
|
+
throw new UserError(`Action "${block.action}" requires a marker`);
|
|
5633
|
+
}
|
|
5634
|
+
return block.marker;
|
|
5635
|
+
}
|
|
5636
|
+
function applyOneAction(lines, target, block) {
|
|
5637
|
+
const contentLines = block.content.length === 0 ? [""] : block.content.split("\n");
|
|
5638
|
+
switch (block.action) {
|
|
5639
|
+
case "append": {
|
|
5640
|
+
lines.splice(target.bodyEnd, 0, ...contentLines);
|
|
5641
|
+
return;
|
|
5642
|
+
}
|
|
5643
|
+
case "prepend": {
|
|
5644
|
+
lines.splice(target.bodyStart, 0, ...contentLines);
|
|
5645
|
+
return;
|
|
5646
|
+
}
|
|
5647
|
+
case "insert-after":
|
|
5648
|
+
case "insert-before": {
|
|
5649
|
+
const idx = findMarkerLine(lines, target.bodyStart, target.bodyEnd, requireMarker(block));
|
|
5650
|
+
lines.splice(block.action === "insert-after" ? idx + 1 : idx, 0, ...contentLines);
|
|
5651
|
+
return;
|
|
5652
|
+
}
|
|
5653
|
+
case "replace-section":
|
|
5654
|
+
case "delete-section": {
|
|
5655
|
+
const child = findChildByMarker(target, requireMarker(block));
|
|
5656
|
+
const replacement = block.action === "replace-section" ? contentLines : [];
|
|
5657
|
+
lines.splice(child.headingLine, child.bodyEnd - child.headingLine, ...replacement);
|
|
5658
|
+
return;
|
|
5659
|
+
}
|
|
5660
|
+
default: {
|
|
5661
|
+
const exhaustive = block.action;
|
|
5662
|
+
throw new UserError(`Unknown patch action "${exhaustive}"`);
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5666
|
+
function applyPatchBlocks(skillMd, blocks) {
|
|
5667
|
+
const lines = skillMd.split("\n");
|
|
5668
|
+
for (const block of blocks) {
|
|
5669
|
+
const bodyOffset = frontmatterOffset(lines);
|
|
5670
|
+
const root = buildHeadingTree(lines, bodyOffset);
|
|
5671
|
+
const segments = block.target ? splitHeadingPath(block.target) : [];
|
|
5672
|
+
const target = resolveHeadingPath(root, segments);
|
|
5673
|
+
applyOneAction(lines, target, block);
|
|
5674
|
+
}
|
|
5675
|
+
return lines.join("\n");
|
|
5676
|
+
}
|
|
5677
|
+
function renderExtendFile(meta) {
|
|
5678
|
+
return [
|
|
5679
|
+
"# SKILL.extend.md",
|
|
5680
|
+
`# Extensions for: ${meta.skillName}`,
|
|
5681
|
+
`# Generated: ${meta.generatedAt}`,
|
|
5682
|
+
`# Refinement: ${meta.refinementId}`,
|
|
5683
|
+
"",
|
|
5684
|
+
`## EXTEND: ${meta.targetSection}`,
|
|
5685
|
+
"<!-- ACTION: append -->",
|
|
5686
|
+
meta.content,
|
|
5687
|
+
""
|
|
5688
|
+
].join("\n");
|
|
5689
|
+
}
|
|
5690
|
+
function renderConfigOverride(meta) {
|
|
5691
|
+
const payload = {
|
|
5692
|
+
skill: meta.skillName,
|
|
5693
|
+
version: "1.0.0",
|
|
5694
|
+
generated: meta.generatedAt,
|
|
5695
|
+
config: meta.config
|
|
5696
|
+
};
|
|
5697
|
+
return `${JSON.stringify(payload, null, 2)}
|
|
5698
|
+
`;
|
|
5699
|
+
}
|
|
5700
|
+
function computeLcsDiff(a, b) {
|
|
5701
|
+
const n = a.length;
|
|
5702
|
+
const m = b.length;
|
|
5703
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
5704
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
5705
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
5706
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5709
|
+
const ops = [];
|
|
5710
|
+
let i = 0;
|
|
5711
|
+
let j = 0;
|
|
5712
|
+
while (i < n && j < m) {
|
|
5713
|
+
if (a[i] === b[j]) {
|
|
5714
|
+
ops.push({ type: "context", line: a[i] });
|
|
5715
|
+
i++;
|
|
5716
|
+
j++;
|
|
5717
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
5718
|
+
ops.push({ type: "remove", line: a[i] });
|
|
5719
|
+
i++;
|
|
5720
|
+
} else {
|
|
5721
|
+
ops.push({ type: "add", line: b[j] });
|
|
5722
|
+
j++;
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
while (i < n) {
|
|
5726
|
+
ops.push({ type: "remove", line: a[i] });
|
|
5727
|
+
i++;
|
|
5728
|
+
}
|
|
5729
|
+
while (j < m) {
|
|
5730
|
+
ops.push({ type: "add", line: b[j] });
|
|
5731
|
+
j++;
|
|
5732
|
+
}
|
|
5733
|
+
return ops;
|
|
5734
|
+
}
|
|
5735
|
+
function previewDiff(before, after, path) {
|
|
5736
|
+
const a = before.split("\n");
|
|
5737
|
+
const b = after.split("\n");
|
|
5738
|
+
const ops = computeLcsDiff(a, b);
|
|
5739
|
+
const lines = [`--- ${path}`, `+++ ${path}`];
|
|
5740
|
+
for (const op of ops) {
|
|
5741
|
+
const prefix = op.type === "add" ? "+" : op.type === "remove" ? "-" : " ";
|
|
5742
|
+
lines.push(`${prefix}${op.line}`);
|
|
5743
|
+
}
|
|
5744
|
+
return lines.join("\n");
|
|
5745
|
+
}
|
|
5746
|
+
|
|
5747
|
+
// src/commands/refine.ts
|
|
5748
|
+
var PRO_FEATURE_LABEL6 = "skill refinement";
|
|
5749
|
+
var REFINE_PROMPT_ASSET = "refine-prompt.md";
|
|
5750
|
+
function requireProFeature2() {
|
|
5751
|
+
const status2 = proFeatureStatus(PRO_FEATURE_LABEL6);
|
|
5752
|
+
if (!status2.enabled) {
|
|
5753
|
+
throw new UserError(status2.notice ?? "refine is a Pro feature \u2014 set SKILL_FORGE_LICENSE or config.licenseKey to a valid license to unlock.");
|
|
5754
|
+
}
|
|
5755
|
+
if (status2.notice) console.error(`[skill-forge] ${status2.notice}`);
|
|
5756
|
+
}
|
|
5757
|
+
function isInteractive(json) {
|
|
5758
|
+
return !json && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
5759
|
+
}
|
|
5760
|
+
function emit(json, payload, renderText) {
|
|
5761
|
+
if (json) {
|
|
5762
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
5763
|
+
} else {
|
|
5764
|
+
renderText();
|
|
5765
|
+
}
|
|
5766
|
+
}
|
|
5767
|
+
async function confirmYesNo(rl, question) {
|
|
5768
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
5769
|
+
return answer === "y" || answer === "yes";
|
|
5770
|
+
}
|
|
5771
|
+
async function confirmWrite(yes, json, prompt) {
|
|
5772
|
+
if (yes) return true;
|
|
5773
|
+
if (!isInteractive(json)) {
|
|
5774
|
+
throw new UserError("refusing to write without confirmation \u2014 pass --yes, or run interactively.");
|
|
5775
|
+
}
|
|
5776
|
+
const rl = createInterface5({ input: process.stdin, output: process.stdout });
|
|
5777
|
+
try {
|
|
5778
|
+
return await confirmYesNo(rl, prompt);
|
|
5779
|
+
} finally {
|
|
5780
|
+
rl.close();
|
|
5781
|
+
}
|
|
5782
|
+
}
|
|
5783
|
+
function resolveConfiguredRoot3(root) {
|
|
5784
|
+
return resolvePath11(expandHome(root));
|
|
5785
|
+
}
|
|
5786
|
+
function isUnderRoot3(candidate, root) {
|
|
5787
|
+
const rel = relative3(root, candidate);
|
|
5788
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
|
|
5789
|
+
}
|
|
5790
|
+
function sanitizeDiffForConsole(diff) {
|
|
5791
|
+
return diff.split("\n").map((line) => sanitizeForMarkdown(line)).join("\n");
|
|
5792
|
+
}
|
|
5793
|
+
function resolveContentOption(options) {
|
|
5794
|
+
if (options.content !== void 0) return options.content;
|
|
5795
|
+
if (options.contentFile) return readFileSync15(resolvePath11(options.contentFile), "utf-8");
|
|
5796
|
+
return void 0;
|
|
5797
|
+
}
|
|
5798
|
+
function validateCaptureOptions(options) {
|
|
5799
|
+
if (!options.skill) throw new UserError("--skill is required");
|
|
5800
|
+
assertPlainSkillName(options.skill);
|
|
5801
|
+
if (!options.category || !isRefinementCategory(options.category)) {
|
|
5802
|
+
throw new UserError(`--category is required and must be one of: trigger, content, hook, tool, pattern, config, new`);
|
|
5803
|
+
}
|
|
5804
|
+
if (!options.target) throw new UserError("--target is required (heading-path selector or a short description for whole-file override types)");
|
|
5805
|
+
if (!options.overrideType || !isOverrideType(options.overrideType)) {
|
|
5806
|
+
throw new UserError("--override-type is required and must be one of: patch, extend, config, full, hook, script, new");
|
|
5807
|
+
}
|
|
5808
|
+
if (!options.expected) throw new UserError("--expected is required");
|
|
5809
|
+
if (!options.actual) throw new UserError("--actual is required");
|
|
5810
|
+
const scopeRaw = options.scope ?? "local";
|
|
5811
|
+
if (scopeRaw !== "local" && scopeRaw !== "shared") {
|
|
5812
|
+
throw new UserError('--scope must be "local" or "shared"');
|
|
5813
|
+
}
|
|
5814
|
+
let action;
|
|
5815
|
+
if (options.overrideType === "patch") {
|
|
5816
|
+
if (!options.action || !isPatchAction(options.action)) {
|
|
5817
|
+
throw new UserError(
|
|
5818
|
+
"--action is required for --override-type patch (one of: append, prepend, replace-section, insert-after, insert-before, delete-section)"
|
|
5819
|
+
);
|
|
5820
|
+
}
|
|
5821
|
+
action = options.action;
|
|
5822
|
+
}
|
|
5823
|
+
return {
|
|
5824
|
+
skill: options.skill,
|
|
5825
|
+
category: options.category,
|
|
5826
|
+
target: options.target,
|
|
5827
|
+
overrideType: options.overrideType,
|
|
5828
|
+
action,
|
|
5829
|
+
marker: options.marker,
|
|
5830
|
+
expected: options.expected,
|
|
5831
|
+
actual: options.actual,
|
|
5832
|
+
example: options.example,
|
|
5833
|
+
outcome: options.outcome,
|
|
5834
|
+
rootCause: options.rootCause,
|
|
5835
|
+
patternId: options.patternId,
|
|
5836
|
+
scope: scopeRaw,
|
|
5837
|
+
dryRun: options.dryRun ?? false,
|
|
5838
|
+
json: options.json ?? false,
|
|
5839
|
+
yes: options.yes ?? false,
|
|
5840
|
+
handoff: options.handoff ?? false
|
|
5841
|
+
};
|
|
5842
|
+
}
|
|
5843
|
+
function renderPatchActionBlock(params) {
|
|
5844
|
+
const lines = [];
|
|
5845
|
+
lines.push(params.marker ? `<!-- ACTION: ${params.action} "${params.marker}" -->` : `<!-- ACTION: ${params.action} -->`);
|
|
5846
|
+
lines.push(`<!-- TARGET: ${params.target} -->`);
|
|
5847
|
+
lines.push(params.content);
|
|
5848
|
+
return lines.join("\n");
|
|
5849
|
+
}
|
|
5850
|
+
function defaultOverrideFilename(overrideType) {
|
|
5851
|
+
switch (overrideType) {
|
|
5852
|
+
case "full":
|
|
5853
|
+
return "SKILL.md";
|
|
5854
|
+
case "hook":
|
|
5855
|
+
return "hooks/refinement.sh";
|
|
5856
|
+
case "script":
|
|
5857
|
+
return "scripts/refinement.sh";
|
|
5858
|
+
default: {
|
|
5859
|
+
const exhaustive = overrideType;
|
|
5860
|
+
throw new Error(`internal error: defaultOverrideFilename called for non-whole-file override type "${exhaustive}"`);
|
|
5861
|
+
}
|
|
5862
|
+
}
|
|
5863
|
+
}
|
|
5864
|
+
function renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent) {
|
|
5865
|
+
switch (opts.overrideType) {
|
|
5866
|
+
case "patch": {
|
|
5867
|
+
if (!content) throw new UserError("--content or --content-file is required for --override-type patch");
|
|
5868
|
+
const block = renderPatchActionBlock({ action: opts.action, marker: opts.marker, target: opts.target, content });
|
|
5869
|
+
const preamble = `# SKILL.patch.md
|
|
5870
|
+
# Patches for: ${opts.skill}
|
|
5871
|
+
`;
|
|
5872
|
+
const base = existingContent.trim().length > 0 ? existingContent.trimEnd() : preamble.trimEnd();
|
|
5873
|
+
return { relPath: "SKILL.patch.md", content: `${base}
|
|
5874
|
+
|
|
5875
|
+
${block}
|
|
5876
|
+
` };
|
|
5877
|
+
}
|
|
5878
|
+
case "extend":
|
|
5879
|
+
case "new": {
|
|
5880
|
+
if (!content) throw new UserError(`--content or --content-file is required for --override-type ${opts.overrideType}`);
|
|
5881
|
+
const rendered = renderExtendFile({ skillName: opts.skill, refinementId, generatedAt, targetSection: opts.target, content });
|
|
5882
|
+
const combined = existingContent.trim().length > 0 ? `${existingContent.trimEnd()}
|
|
5883
|
+
|
|
5884
|
+
${rendered}` : rendered;
|
|
5885
|
+
return { relPath: "SKILL.extend.md", content: combined };
|
|
5886
|
+
}
|
|
5887
|
+
case "config": {
|
|
5888
|
+
if (!content) throw new UserError("--content or --content-file (JSON object) is required for --override-type config");
|
|
5889
|
+
let parsed;
|
|
5890
|
+
try {
|
|
5891
|
+
parsed = JSON.parse(content);
|
|
5892
|
+
} catch (err) {
|
|
5893
|
+
throw new UserError(`--content for --override-type config must be valid JSON: ${err.message}`);
|
|
5894
|
+
}
|
|
5895
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
5896
|
+
throw new UserError("--content for --override-type config must be a JSON object");
|
|
5897
|
+
}
|
|
5898
|
+
const rendered = renderConfigOverride({
|
|
5899
|
+
skillName: opts.skill,
|
|
5900
|
+
refinementId,
|
|
5901
|
+
generatedAt,
|
|
5902
|
+
config: parsed
|
|
5903
|
+
});
|
|
5904
|
+
return { relPath: "skill-config.json", content: rendered };
|
|
5905
|
+
}
|
|
5906
|
+
case "full":
|
|
5907
|
+
case "hook":
|
|
5908
|
+
case "script": {
|
|
5909
|
+
if (!content) throw new UserError(`--content or --content-file is required for --override-type ${opts.overrideType}`);
|
|
5910
|
+
return { relPath: opts.marker && opts.marker.trim().length > 0 ? opts.marker.trim() : defaultOverrideFilename(opts.overrideType), content };
|
|
5911
|
+
}
|
|
5912
|
+
default: {
|
|
5913
|
+
const exhaustive = opts.overrideType;
|
|
5914
|
+
throw new UserError(`unknown override type "${exhaustive}"`);
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5917
|
+
}
|
|
5918
|
+
function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
5919
|
+
let lst;
|
|
5920
|
+
try {
|
|
5921
|
+
lst = lstatSync3(scopeDir);
|
|
5922
|
+
} catch {
|
|
5923
|
+
lst = void 0;
|
|
5924
|
+
}
|
|
5925
|
+
if (lst?.isSymbolicLink()) {
|
|
5926
|
+
throw new UserError(`refusing to write under ${scopeDir}: scope directory is a symlink`);
|
|
5927
|
+
}
|
|
5928
|
+
mkdirSync9(scopeDir, { recursive: true });
|
|
5929
|
+
const real = resolveConfiguredRoot3(scopeDir);
|
|
5930
|
+
const lexical = resolvePath11(scopeDir);
|
|
5931
|
+
let realResolved;
|
|
5932
|
+
try {
|
|
5933
|
+
realResolved = realpathSync7(scopeDir);
|
|
5934
|
+
} catch {
|
|
5935
|
+
realResolved = real;
|
|
5936
|
+
}
|
|
5937
|
+
if (realResolved !== lexical) {
|
|
5938
|
+
throw new UserError(`refusing to write under ${scopeDir}: path contains a symlink`);
|
|
5939
|
+
}
|
|
5940
|
+
if (!isUnderRoot3(realResolved, resolveConfiguredRoot3(resolvedCwd))) {
|
|
5941
|
+
throw new UserError(`refusing to write under ${scopeDir}: resolves outside the project root ${resolvedCwd}`);
|
|
5942
|
+
}
|
|
5943
|
+
return realResolved;
|
|
5944
|
+
}
|
|
5945
|
+
function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
|
|
5946
|
+
const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
|
|
5947
|
+
const destPath = join25(scopeDir, relPath);
|
|
5948
|
+
const destDir = dirname11(destPath);
|
|
5949
|
+
mkdirSync9(destDir, { recursive: true });
|
|
5950
|
+
const realDestDir = realpathSync7(destDir);
|
|
5951
|
+
if (!isUnderRoot3(realDestDir, realScopeDir)) {
|
|
5952
|
+
throw new UserError(`refusing to write ${destPath}: resolves outside scope directory ${scopeDir}`);
|
|
5953
|
+
}
|
|
5954
|
+
let destLst;
|
|
5955
|
+
try {
|
|
5956
|
+
destLst = lstatSync3(destPath);
|
|
5957
|
+
} catch {
|
|
5958
|
+
destLst = void 0;
|
|
5959
|
+
}
|
|
5960
|
+
if (destLst?.isSymbolicLink()) {
|
|
5961
|
+
throw new UserError(`refusing to write ${destPath}: destination is a symlink`);
|
|
5962
|
+
}
|
|
5963
|
+
writeFileSync9(destPath, content, { mode: 384 });
|
|
5964
|
+
chmodSync(destPath, 384);
|
|
5965
|
+
return destPath;
|
|
5966
|
+
}
|
|
5967
|
+
async function refineCaptureCommand(rawOptions = {}) {
|
|
5968
|
+
requireProFeature2();
|
|
5969
|
+
const opts = validateCaptureOptions(rawOptions);
|
|
5970
|
+
const config = loadConfig();
|
|
5971
|
+
const cwd = process.cwd();
|
|
5972
|
+
const scopes = resolveScopes(config, cwd, opts.skill);
|
|
5973
|
+
const scopeDir = opts.scope === "local" ? scopes.local.dir : scopes.shared.dir;
|
|
5974
|
+
const content = resolveContentOption(rawOptions);
|
|
5975
|
+
const now = /* @__PURE__ */ new Date();
|
|
5976
|
+
const existingIds = new Set(readRefinementsStore().records.map((r) => r.id));
|
|
5977
|
+
const refinementId = generateRefinementId(existingIds, now, defaultRandomHex);
|
|
5978
|
+
const preview = renderCaptureArtifactPreview(opts, content, refinementId, now.toISOString(), scopeDir);
|
|
5979
|
+
if (opts.dryRun) {
|
|
5980
|
+
emit(opts.json, { dryRun: true, path: preview.destPath, diff: preview.diff }, () => {
|
|
5981
|
+
console.log(`[skill-forge] refine capture \u2014 ${opts.overrideType} override for "${sanitizeForMarkdown(opts.skill)}" at ${opts.scope} scope`);
|
|
5982
|
+
console.log(sanitizeDiffForConsole(preview.diff));
|
|
5983
|
+
});
|
|
5984
|
+
return;
|
|
5985
|
+
}
|
|
5986
|
+
if (!opts.json) {
|
|
5987
|
+
console.log(`[skill-forge] refine capture \u2014 ${opts.overrideType} override for "${sanitizeForMarkdown(opts.skill)}" at ${opts.scope} scope`);
|
|
5988
|
+
console.log(sanitizeDiffForConsole(preview.diff));
|
|
5989
|
+
}
|
|
5990
|
+
const proceed = await confirmWrite(opts.yes, opts.json, `
|
|
5991
|
+
Write ${preview.destPath}? [y/N] `);
|
|
5992
|
+
if (!proceed) {
|
|
5993
|
+
emit(opts.json, { written: false, cancelled: true, path: preview.destPath }, () => {
|
|
5994
|
+
console.log("[skill-forge] capture cancelled \u2014 nothing written.");
|
|
5995
|
+
});
|
|
5996
|
+
return;
|
|
5997
|
+
}
|
|
5998
|
+
const writtenPath = writeOverrideFile(scopeDir, preview.relPath, preview.content, cwd);
|
|
5999
|
+
let result;
|
|
6000
|
+
try {
|
|
6001
|
+
result = recordRefinement(
|
|
6002
|
+
{
|
|
6003
|
+
skill: opts.skill,
|
|
6004
|
+
category: opts.category,
|
|
6005
|
+
overrideType: opts.overrideType,
|
|
6006
|
+
targetSection: opts.target,
|
|
6007
|
+
expectedBehavior: opts.expected,
|
|
6008
|
+
actualBehavior: opts.actual,
|
|
6009
|
+
example: opts.example,
|
|
6010
|
+
desiredOutcome: opts.outcome,
|
|
6011
|
+
rootCause: opts.rootCause,
|
|
6012
|
+
project: cwd,
|
|
6013
|
+
projectRoot: cwd,
|
|
6014
|
+
patchAction: opts.action,
|
|
6015
|
+
patchMarker: opts.marker,
|
|
6016
|
+
patchContent: content,
|
|
6017
|
+
status: "applied",
|
|
6018
|
+
scope: opts.scope,
|
|
6019
|
+
patternId: opts.patternId
|
|
6020
|
+
},
|
|
6021
|
+
{ now, id: refinementId }
|
|
6022
|
+
);
|
|
6023
|
+
} catch (err) {
|
|
6024
|
+
throw new UserError(`wrote the override artifact at ${writtenPath}, but recording the refinement in the store failed: ${err.message}`);
|
|
6025
|
+
}
|
|
6026
|
+
emit(
|
|
6027
|
+
opts.json,
|
|
6028
|
+
{ written: true, path: writtenPath, refinementId: result.refinement.id, patternId: result.pattern.id, patternBecameReady: result.patternBecameReady },
|
|
6029
|
+
() => {
|
|
6030
|
+
console.log(`[skill-forge] wrote ${writtenPath}`);
|
|
6031
|
+
console.log(`[skill-forge] recorded refinement ${result.refinement.id} -> pattern ${result.pattern.id} (status: ${result.pattern.status})`);
|
|
6032
|
+
if (result.patternBecameReady) {
|
|
6033
|
+
console.log(`[skill-forge] pattern ${result.pattern.id} is now READY (seen in a second project) \u2014 consider \`skill-forge refine promote ${result.pattern.id}\``);
|
|
6034
|
+
}
|
|
6035
|
+
}
|
|
6036
|
+
);
|
|
6037
|
+
if (opts.handoff) {
|
|
6038
|
+
runHandoff(config, writtenPath, { promptAsset: REFINE_PROMPT_ASSET, log: opts.json ? console.error : console.log });
|
|
6039
|
+
}
|
|
6040
|
+
}
|
|
6041
|
+
function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
|
|
6042
|
+
const probeRelPath = opts.overrideType === "patch" ? "SKILL.patch.md" : opts.overrideType === "extend" || opts.overrideType === "new" ? "SKILL.extend.md" : void 0;
|
|
6043
|
+
const probePath = probeRelPath ? join25(scopeDir, probeRelPath) : void 0;
|
|
6044
|
+
const existingContent = probePath && existsSync20(probePath) ? readFileSync15(probePath, "utf-8") : "";
|
|
6045
|
+
const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
|
|
6046
|
+
const relPath = rendered.relPath;
|
|
6047
|
+
const destPath = join25(scopeDir, relPath);
|
|
6048
|
+
const before = existsSync20(destPath) ? readFileSync15(destPath, "utf-8") : "";
|
|
6049
|
+
const diff = previewDiff(before, rendered.content, destPath);
|
|
6050
|
+
return { relPath, content: rendered.content, destPath, diff };
|
|
6051
|
+
}
|
|
6052
|
+
async function refineListCommand(options = {}) {
|
|
6053
|
+
requireProFeature2();
|
|
6054
|
+
const { records } = readRefinementsStore();
|
|
6055
|
+
let rows = records;
|
|
6056
|
+
if (options.skill) rows = rows.filter((r) => r.skill === options.skill);
|
|
6057
|
+
if (options.project) rows = rows.filter((r) => r.project === options.project || r.projectRoot === resolvePath11(options.project));
|
|
6058
|
+
if (options.status) {
|
|
6059
|
+
if (!isRefinementStatus(options.status)) {
|
|
6060
|
+
throw new UserError("--status must be one of: pending, applied, generalized");
|
|
6061
|
+
}
|
|
6062
|
+
rows = rows.filter((r) => r.status === options.status);
|
|
6063
|
+
}
|
|
6064
|
+
const filtered = Boolean(options.skill || options.project || options.status);
|
|
6065
|
+
emit(options.json, { records: rows }, () => {
|
|
6066
|
+
if (rows.length === 0) {
|
|
6067
|
+
if (filtered && records.length > 0) {
|
|
6068
|
+
console.log(`[skill-forge] no refinements match the given filter(s) (${records.length} total recorded).`);
|
|
6069
|
+
} else {
|
|
6070
|
+
console.log("[skill-forge] no refinements recorded.");
|
|
6071
|
+
}
|
|
6072
|
+
return;
|
|
6073
|
+
}
|
|
6074
|
+
console.log(`[skill-forge] ${rows.length} refinement(s):`);
|
|
6075
|
+
for (const r of rows) {
|
|
6076
|
+
console.log(
|
|
6077
|
+
` ${r.id} ${sanitizeForMarkdown(r.skill)} [${r.category}/${r.overrideType}] status=${r.status} pattern=${r.patternId ?? "(none)"} ${r.createdAt}`
|
|
6078
|
+
);
|
|
6079
|
+
}
|
|
6080
|
+
});
|
|
6081
|
+
}
|
|
6082
|
+
async function refinePatternsCommand(options = {}) {
|
|
6083
|
+
requireProFeature2();
|
|
6084
|
+
const { records } = readPatternsStore();
|
|
6085
|
+
let rows = records;
|
|
6086
|
+
if (options.status) {
|
|
6087
|
+
if (!isPatternStatus(options.status)) {
|
|
6088
|
+
throw new UserError("--status must be one of: tracking, ready, generalized, dismissed");
|
|
6089
|
+
}
|
|
6090
|
+
rows = rows.filter((p) => p.status === options.status);
|
|
6091
|
+
}
|
|
6092
|
+
if (options.skill) rows = rows.filter((p) => p.affectedSkills.includes(options.skill));
|
|
6093
|
+
if (options.project) {
|
|
6094
|
+
const resolvedProject = resolvePath11(options.project);
|
|
6095
|
+
rows = rows.filter((p) => p.occurrences.some((o) => o.projectRealpath === resolvedProject));
|
|
6096
|
+
}
|
|
6097
|
+
const filtered = Boolean(options.skill || options.project || options.status);
|
|
6098
|
+
emit(options.json, { records: rows.map((p) => ({ ...p, count: patternCount(p) })) }, () => {
|
|
6099
|
+
if (rows.length === 0) {
|
|
6100
|
+
if (filtered && records.length > 0) {
|
|
6101
|
+
console.log(`[skill-forge] no patterns match the given filter(s) (${records.length} total recorded).`);
|
|
6102
|
+
} else {
|
|
6103
|
+
console.log("[skill-forge] no patterns recorded.");
|
|
6104
|
+
}
|
|
6105
|
+
return;
|
|
6106
|
+
}
|
|
6107
|
+
console.log(`[skill-forge] ${rows.length} pattern(s):`);
|
|
6108
|
+
for (const p of rows) {
|
|
6109
|
+
console.log(
|
|
6110
|
+
` ${p.id} ${sanitizeForMarkdown(p.name)} status=${p.status} count=${patternCount(p)} skills=${p.affectedSkills.map(sanitizeForMarkdown).join(", ")}`
|
|
6111
|
+
);
|
|
6112
|
+
}
|
|
6113
|
+
});
|
|
6114
|
+
}
|
|
6115
|
+
function renderInventoryLine(row) {
|
|
6116
|
+
const present = [
|
|
6117
|
+
row.skillMd ? "SKILL.md" : void 0,
|
|
6118
|
+
row.patchMd ? "SKILL.patch.md" : void 0,
|
|
6119
|
+
row.extendMd ? "SKILL.extend.md" : void 0,
|
|
6120
|
+
row.configJson ? "skill-config.json" : void 0,
|
|
6121
|
+
row.hooksDir ? "hooks/" : void 0,
|
|
6122
|
+
row.scriptsDir ? "scripts/" : void 0
|
|
6123
|
+
].filter((v) => Boolean(v));
|
|
6124
|
+
return ` [${row.scope}] ${row.dir}${row.dirExists ? "" : " (missing)"} \u2014 ${present.length > 0 ? present.join(", ") : "(no override files)"}`;
|
|
6125
|
+
}
|
|
6126
|
+
async function refineWhichCommand(skill, options = {}) {
|
|
6127
|
+
requireProFeature2();
|
|
6128
|
+
if (!skill) throw new UserError("a skill name is required: skill-forge refine which <skill>");
|
|
6129
|
+
const config = loadConfig();
|
|
6130
|
+
const cwd = process.cwd();
|
|
6131
|
+
const scopes = resolveScopes(config, cwd, skill, options.userRoot);
|
|
6132
|
+
const inventory = inventoryAllScopes(scopes);
|
|
6133
|
+
emit(options.json, { scopes, inventory }, () => {
|
|
6134
|
+
console.log(`[skill-forge] refine which "${sanitizeForMarkdown(skill)}" \u2014 resolution order (highest precedence first):`);
|
|
6135
|
+
for (const row of inventory) console.log(renderInventoryLine(row));
|
|
6136
|
+
if (!scopes.user.resolved) {
|
|
6137
|
+
console.log(
|
|
6138
|
+
` [user] ambiguous \u2014 candidates: ${scopes.user.candidates.map((c) => c.root).join(", ") || "(none configured)"} (pass --user-root to disambiguate)`
|
|
6139
|
+
);
|
|
6140
|
+
}
|
|
6141
|
+
});
|
|
6142
|
+
}
|
|
6143
|
+
function writePostWriteSidecar(backupId, files) {
|
|
6144
|
+
const sidecar = { schemaVersion: 1, files };
|
|
6145
|
+
const dir = join25(getBackupsDir(), backupId);
|
|
6146
|
+
mkdirSync9(dir, { recursive: true });
|
|
6147
|
+
writeFileSync9(join25(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
|
|
6148
|
+
}
|
|
6149
|
+
function readPostWriteSidecar(backupId) {
|
|
6150
|
+
const path = join25(getBackupsDir(), backupId, "post.json");
|
|
6151
|
+
if (!existsSync20(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
|
|
6152
|
+
let raw;
|
|
6153
|
+
try {
|
|
6154
|
+
raw = readFileSync15(path, "utf-8");
|
|
6155
|
+
} catch {
|
|
6156
|
+
return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
|
|
6157
|
+
}
|
|
6158
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
6159
|
+
try {
|
|
6160
|
+
const parsed = JSON.parse(raw);
|
|
6161
|
+
for (const f of parsed.files ?? []) {
|
|
6162
|
+
if (typeof f !== "object" || f === null || typeof f.path !== "string" || typeof f.sha256 !== "string") {
|
|
6163
|
+
throw new Error("malformed post.json files entry");
|
|
6164
|
+
}
|
|
6165
|
+
hashes.set(f.path, f.sha256);
|
|
6166
|
+
}
|
|
6167
|
+
} catch {
|
|
6168
|
+
return { hashes: /* @__PURE__ */ new Map(), status: "corrupt" };
|
|
6169
|
+
}
|
|
6170
|
+
return { hashes, status: "ok" };
|
|
6171
|
+
}
|
|
6172
|
+
async function refinePromoteCommand(patternId, options = {}) {
|
|
6173
|
+
requireProFeature2();
|
|
6174
|
+
if (!patternId) throw new UserError("a pattern id is required: skill-forge refine promote <pattern-id>");
|
|
6175
|
+
const patternsStore = readPatternsStore();
|
|
6176
|
+
const pattern = patternsStore.records.find((p) => p.id === patternId);
|
|
6177
|
+
if (!pattern) throw new UserError(`no pattern with id: ${patternId}`);
|
|
6178
|
+
if (pattern.status === "generalized" && !options.force) {
|
|
6179
|
+
throw new UserError(`pattern ${patternId} is already generalized (backup ${pattern.generalizedBackupId ?? "(unknown)"}) \u2014 pass --force to re-promote.`);
|
|
6180
|
+
}
|
|
6181
|
+
if (pattern.status !== "ready" && pattern.status !== "generalized" && !options.force) {
|
|
6182
|
+
throw new UserError(`pattern ${patternId} is not ready (status: ${pattern.status}) \u2014 pass --force to promote anyway.`);
|
|
6183
|
+
}
|
|
6184
|
+
const skill = pattern.affectedSkills[0];
|
|
6185
|
+
if (!skill) throw new UserError(`pattern ${patternId} has no affected skills recorded`);
|
|
6186
|
+
assertPlainSkillName(skill);
|
|
6187
|
+
const config = loadConfig();
|
|
6188
|
+
const cwd = process.cwd();
|
|
6189
|
+
const scopes = resolveScopes(config, cwd, skill, options.userRoot);
|
|
6190
|
+
if (!scopes.user.resolved) {
|
|
6191
|
+
throw new UserError(
|
|
6192
|
+
`cannot resolve a single user scope for skill "${skill}" \u2014 candidates: ${scopes.user.candidates.map((c) => c.root).join(", ") || "(none configured)"}. Pass --user-root to disambiguate.`
|
|
6193
|
+
);
|
|
6194
|
+
}
|
|
6195
|
+
const skillDir = scopes.user.dir;
|
|
6196
|
+
const resolvedUserRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
6197
|
+
let realUserRoot;
|
|
6198
|
+
try {
|
|
6199
|
+
realUserRoot = realpathSync7(resolvedUserRoot);
|
|
6200
|
+
} catch {
|
|
6201
|
+
realUserRoot = resolvedUserRoot;
|
|
6202
|
+
}
|
|
6203
|
+
let realSkillDir;
|
|
6204
|
+
try {
|
|
6205
|
+
realSkillDir = realpathSync7(skillDir);
|
|
6206
|
+
} catch {
|
|
6207
|
+
realSkillDir = resolvePath11(skillDir);
|
|
6208
|
+
}
|
|
6209
|
+
if (!isUnderRoot3(realSkillDir, realUserRoot)) {
|
|
6210
|
+
throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
|
|
6211
|
+
}
|
|
6212
|
+
const skillMdPath = join25(skillDir, "SKILL.md");
|
|
6213
|
+
if (!existsSync20(skillMdPath)) {
|
|
6214
|
+
throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
|
|
6215
|
+
}
|
|
6216
|
+
const refinementsStore = readRefinementsStore();
|
|
6217
|
+
const patternRefinements = refinementsStore.records.filter((r) => pattern.refinementIds.includes(r.id));
|
|
6218
|
+
const representative = patternRefinements[patternRefinements.length - 1];
|
|
6219
|
+
if (!representative) throw new UserError(`pattern ${patternId} has no recorded refinements to promote`);
|
|
6220
|
+
let block;
|
|
6221
|
+
if (representative.overrideType === "patch" && representative.patchAction && representative.patchContent) {
|
|
6222
|
+
block = { action: representative.patchAction, marker: representative.patchMarker, target: representative.targetSection, content: representative.patchContent };
|
|
6223
|
+
} else if ((representative.overrideType === "extend" || representative.overrideType === "new") && representative.patchContent) {
|
|
6224
|
+
block = { action: "append", target: representative.targetSection, content: representative.patchContent };
|
|
6225
|
+
} else {
|
|
6226
|
+
throw new UserError(
|
|
6227
|
+
`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.`
|
|
6228
|
+
);
|
|
6229
|
+
}
|
|
6230
|
+
const before = readFileSync15(skillMdPath, "utf-8");
|
|
6231
|
+
const after = applyPatchBlocks(before, [block]);
|
|
6232
|
+
const diff = previewDiff(before, after, skillMdPath);
|
|
6233
|
+
if (options.dryRun) {
|
|
6234
|
+
emit(options.json, { dryRun: true, path: skillMdPath, diff }, () => {
|
|
6235
|
+
console.log(`[skill-forge] refine promote ${patternId} -> ${skillMdPath}`);
|
|
6236
|
+
console.log(sanitizeDiffForConsole(diff));
|
|
6237
|
+
});
|
|
6238
|
+
return;
|
|
6239
|
+
}
|
|
6240
|
+
if (!options.json) {
|
|
6241
|
+
console.log(`[skill-forge] refine promote ${patternId} -> ${skillMdPath}`);
|
|
6242
|
+
console.log(sanitizeDiffForConsole(diff));
|
|
6243
|
+
}
|
|
6244
|
+
const proceed = await confirmWrite(options.yes, options.json, `
|
|
6245
|
+
Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
6246
|
+
if (!proceed) {
|
|
6247
|
+
emit(options.json, { promoted: false, cancelled: true }, () => {
|
|
6248
|
+
console.log("[skill-forge] promote cancelled \u2014 nothing written.");
|
|
6249
|
+
});
|
|
6250
|
+
return;
|
|
6251
|
+
}
|
|
6252
|
+
const backup = createBackup([skillMdPath]);
|
|
6253
|
+
const tempPath = `${skillMdPath}.tmp-${randomUUID4()}`;
|
|
6254
|
+
try {
|
|
6255
|
+
writeFileSync9(tempPath, after, "utf-8");
|
|
6256
|
+
renameSync5(tempPath, skillMdPath);
|
|
6257
|
+
} catch (err) {
|
|
6258
|
+
rmSync7(tempPath, { force: true });
|
|
6259
|
+
throw err;
|
|
6260
|
+
}
|
|
6261
|
+
try {
|
|
6262
|
+
const postSha256 = createHash3("sha256").update(readFileSync15(skillMdPath)).digest("hex");
|
|
6263
|
+
writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
|
|
6264
|
+
markGeneralized(patternId, backup.id);
|
|
6265
|
+
const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
6266
|
+
appendProvenance(
|
|
6267
|
+
{
|
|
6268
|
+
name: skill,
|
|
6269
|
+
source: `skill-forge refine (${patternId})`,
|
|
6270
|
+
verb: "DEFER",
|
|
6271
|
+
target: skillMdPath,
|
|
6272
|
+
took: `applied a ${block.action} patch materializing pattern ${patternId}`,
|
|
6273
|
+
notes: `generalized from ${patternId} via skill-forge refine`
|
|
6274
|
+
},
|
|
6275
|
+
skillsRoot
|
|
6276
|
+
);
|
|
6277
|
+
} catch (err) {
|
|
6278
|
+
throw new UserError(
|
|
6279
|
+
`the user-scope skill was updated at ${skillMdPath} (backup ${backup.id}), but recording post-write verification and/or pattern/provenance state failed: ${err.message}`
|
|
6280
|
+
);
|
|
6281
|
+
}
|
|
6282
|
+
emit(options.json, { promoted: true, path: skillMdPath, backupId: backup.id }, () => {
|
|
6283
|
+
console.log(`[skill-forge] promoted pattern ${patternId} -> ${skillMdPath} (backup: ${backup.id})`);
|
|
6284
|
+
});
|
|
6285
|
+
}
|
|
6286
|
+
function assertBackupDirContained(backupId) {
|
|
6287
|
+
const backupsDir = getBackupsDir();
|
|
6288
|
+
if (!existsSync20(backupsDir)) return;
|
|
6289
|
+
const realBackupsDir = realpathSync7(backupsDir);
|
|
6290
|
+
const backupDir = join25(backupsDir, backupId);
|
|
6291
|
+
if (!existsSync20(backupDir)) return;
|
|
6292
|
+
const realBackupDir = realpathSync7(backupDir);
|
|
6293
|
+
if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
|
|
6294
|
+
throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
|
|
6295
|
+
}
|
|
6296
|
+
}
|
|
6297
|
+
function resolveTargetRealPath(path) {
|
|
6298
|
+
let lexicalAncestor = resolvePath11(path);
|
|
6299
|
+
while (true) {
|
|
6300
|
+
try {
|
|
6301
|
+
const real = realpathSync7(lexicalAncestor);
|
|
6302
|
+
const remainder = relative3(lexicalAncestor, resolvePath11(path));
|
|
6303
|
+
return remainder === "" ? real : join25(real, remainder);
|
|
6304
|
+
} catch {
|
|
6305
|
+
const parent = dirname11(lexicalAncestor);
|
|
6306
|
+
if (parent === lexicalAncestor) return resolvePath11(path);
|
|
6307
|
+
lexicalAncestor = parent;
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
}
|
|
6311
|
+
function assertManifestPathsWithinRoots(manifest, config) {
|
|
6312
|
+
if (config.skillsRoots.length === 0) {
|
|
6313
|
+
throw new UserError(`refusing rollback ${manifest.id}: no skillsRoots are configured \u2014 cannot verify the backup's file paths are within a trusted skills root.`);
|
|
6314
|
+
}
|
|
6315
|
+
const roots = config.skillsRoots.map((root) => {
|
|
6316
|
+
const resolved = resolveConfiguredRoot3(root);
|
|
6317
|
+
try {
|
|
6318
|
+
return realpathSync7(resolved);
|
|
6319
|
+
} catch {
|
|
6320
|
+
return resolved;
|
|
6321
|
+
}
|
|
6322
|
+
});
|
|
6323
|
+
for (const file of manifest.files) {
|
|
6324
|
+
const resolvedTarget = resolveTargetRealPath(file.path);
|
|
6325
|
+
const withinAnyRoot = roots.some((root) => isUnderRoot3(resolvedTarget, root));
|
|
6326
|
+
if (!withinAnyRoot) {
|
|
6327
|
+
throw new UserError(`refusing rollback ${manifest.id}: backup manifest references a path outside configured skills roots \u2014 ${file.path}`);
|
|
6328
|
+
}
|
|
6329
|
+
}
|
|
6330
|
+
}
|
|
6331
|
+
function assertNoSymlinkLeaves(manifest) {
|
|
6332
|
+
for (const file of manifest.files) {
|
|
6333
|
+
let lst;
|
|
6334
|
+
try {
|
|
6335
|
+
lst = lstatSync3(file.path);
|
|
6336
|
+
} catch {
|
|
6337
|
+
lst = void 0;
|
|
6338
|
+
}
|
|
6339
|
+
if (lst?.isSymbolicLink()) {
|
|
6340
|
+
throw new UserError(`refusing rollback ${manifest.id}: backup manifest path is currently a symlink \u2014 refusing to follow or overwrite it: ${file.path}`);
|
|
6341
|
+
}
|
|
6342
|
+
}
|
|
6343
|
+
}
|
|
6344
|
+
function findLexicalRoot(filePath, config) {
|
|
6345
|
+
const lexicalTarget = resolvePath11(filePath);
|
|
6346
|
+
for (const root of config.skillsRoots) {
|
|
6347
|
+
const rootLexical = resolveConfiguredRoot3(root);
|
|
6348
|
+
const rel = relative3(rootLexical, lexicalTarget);
|
|
6349
|
+
if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute3(rel))) continue;
|
|
6350
|
+
let rootReal;
|
|
6351
|
+
try {
|
|
6352
|
+
rootReal = realpathSync7(rootLexical);
|
|
6353
|
+
} catch {
|
|
6354
|
+
rootReal = rootLexical;
|
|
6355
|
+
}
|
|
6356
|
+
return { rootLexical, rootReal };
|
|
6357
|
+
}
|
|
6358
|
+
throw new UserError(`refusing to operate on ${filePath}: does not resolve lexically under any configured skills root`);
|
|
6359
|
+
}
|
|
6360
|
+
function walkRealComponents(filePath, config, createMissing) {
|
|
6361
|
+
const { rootLexical, rootReal } = findLexicalRoot(filePath, config);
|
|
6362
|
+
const relPath = relative3(rootLexical, resolvePath11(filePath));
|
|
6363
|
+
const components = relPath.split(sep2).filter((c) => c.length > 0);
|
|
6364
|
+
if (components.length === 0) {
|
|
6365
|
+
throw new UserError(`refusing to operate on ${filePath}: resolves to the skills root itself`);
|
|
6366
|
+
}
|
|
6367
|
+
const leafComponent = components[components.length - 1];
|
|
6368
|
+
const dirComponents = components.slice(0, -1);
|
|
6369
|
+
let current = rootReal;
|
|
6370
|
+
for (const component of dirComponents) {
|
|
6371
|
+
if (current === void 0) break;
|
|
6372
|
+
const next = join25(current, component);
|
|
6373
|
+
let lst;
|
|
6374
|
+
try {
|
|
6375
|
+
lst = lstatSync3(next);
|
|
6376
|
+
} catch {
|
|
6377
|
+
lst = void 0;
|
|
6378
|
+
}
|
|
6379
|
+
if (lst === void 0) {
|
|
6380
|
+
if (!createMissing) {
|
|
6381
|
+
current = void 0;
|
|
6382
|
+
continue;
|
|
6383
|
+
}
|
|
6384
|
+
mkdirSync9(next);
|
|
6385
|
+
current = next;
|
|
6386
|
+
continue;
|
|
6387
|
+
}
|
|
6388
|
+
if (lst.isSymbolicLink()) {
|
|
6389
|
+
throw new UserError(`refusing to operate on ${filePath}: intermediate path component is a symlink \u2014 ${next}`);
|
|
6390
|
+
}
|
|
6391
|
+
if (!lst.isDirectory()) {
|
|
6392
|
+
throw new UserError(`refusing to operate on ${filePath}: intermediate path component is not a directory \u2014 ${next}`);
|
|
6393
|
+
}
|
|
6394
|
+
current = next;
|
|
6395
|
+
}
|
|
6396
|
+
return { parentReal: current, leafComponent, rootReal };
|
|
6397
|
+
}
|
|
6398
|
+
function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
6399
|
+
const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, true);
|
|
6400
|
+
if (parentReal === void 0) {
|
|
6401
|
+
throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
|
|
6402
|
+
}
|
|
6403
|
+
const finalPath = join25(parentReal, leafComponent);
|
|
6404
|
+
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
6405
|
+
throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
|
|
6406
|
+
}
|
|
6407
|
+
let leafLst;
|
|
6408
|
+
try {
|
|
6409
|
+
leafLst = lstatSync3(finalPath);
|
|
6410
|
+
} catch {
|
|
6411
|
+
leafLst = void 0;
|
|
6412
|
+
}
|
|
6413
|
+
if (leafLst?.isSymbolicLink()) {
|
|
6414
|
+
throw new UserError(`refusing to restore ${filePath}: it is now a symlink \u2014 refusing to follow or overwrite it.`);
|
|
6415
|
+
}
|
|
6416
|
+
const tempPath = `${finalPath}.tmp-${randomUUID4()}`;
|
|
6417
|
+
try {
|
|
6418
|
+
writeFileSync9(tempPath, contentBuffer);
|
|
6419
|
+
chmodSync(tempPath, mode);
|
|
6420
|
+
renameSync5(tempPath, finalPath);
|
|
6421
|
+
} catch (err) {
|
|
6422
|
+
rmSync7(tempPath, { force: true });
|
|
6423
|
+
throw err;
|
|
6424
|
+
}
|
|
6425
|
+
}
|
|
6426
|
+
function safeDeleteTombstone(filePath, config) {
|
|
6427
|
+
const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
|
|
6428
|
+
if (parentReal === void 0) return;
|
|
6429
|
+
const finalPath = join25(parentReal, leafComponent);
|
|
6430
|
+
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
6431
|
+
throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
|
|
6432
|
+
}
|
|
6433
|
+
let leafLst;
|
|
6434
|
+
try {
|
|
6435
|
+
leafLst = lstatSync3(finalPath);
|
|
6436
|
+
} catch {
|
|
6437
|
+
leafLst = void 0;
|
|
6438
|
+
}
|
|
6439
|
+
if (leafLst === void 0) return;
|
|
6440
|
+
if (leafLst.isSymbolicLink()) {
|
|
6441
|
+
throw new UserError(`refusing to delete ${filePath}: it is now a symlink \u2014 refusing to follow or overwrite it.`);
|
|
6442
|
+
}
|
|
6443
|
+
rmSync7(finalPath, { force: true });
|
|
6444
|
+
}
|
|
6445
|
+
async function refineRollbackCommand(backupId, options = {}) {
|
|
6446
|
+
requireProFeature2();
|
|
6447
|
+
if (!backupId) throw new UserError("a backup id is required: skill-forge refine rollback <backup-id>");
|
|
6448
|
+
if (!isValidBackupId(backupId)) {
|
|
6449
|
+
throw new UserError(`invalid backup id "${backupId}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
|
|
6450
|
+
}
|
|
6451
|
+
assertBackupDirContained(backupId);
|
|
6452
|
+
const manifest = readBackupManifest(backupId);
|
|
6453
|
+
const config = loadConfig();
|
|
6454
|
+
assertManifestPathsWithinRoots(manifest, config);
|
|
6455
|
+
assertNoSymlinkLeaves(manifest);
|
|
6456
|
+
const warnings = [];
|
|
6457
|
+
const sidecar = readPostWriteSidecar(backupId);
|
|
6458
|
+
if (sidecar.status !== "ok") {
|
|
6459
|
+
const reason = sidecar.status;
|
|
6460
|
+
if (!options.force) {
|
|
6461
|
+
throw new UserError(
|
|
6462
|
+
`refusing rollback ${backupId}: post-write verification data (post.json) is ${reason} \u2014 cannot confirm the current file state matches what was promoted. Pass --force to override.`
|
|
6463
|
+
);
|
|
6464
|
+
}
|
|
6465
|
+
warnings.push(`post-write verification data for backup ${backupId} is ${reason} \u2014 restoring without hash verification (--force).`);
|
|
6466
|
+
}
|
|
6467
|
+
const mismatches = [];
|
|
6468
|
+
for (const file of manifest.files) {
|
|
6469
|
+
const expected = sidecar.hashes.get(file.path);
|
|
6470
|
+
if (expected === void 0) continue;
|
|
6471
|
+
const currentSha256 = existsSync20(file.path) ? createHash3("sha256").update(readFileSync15(file.path)).digest("hex") : null;
|
|
6472
|
+
if (currentSha256 !== expected) mismatches.push(file.path);
|
|
6473
|
+
}
|
|
6474
|
+
if (mismatches.length > 0 && !options.force) {
|
|
6475
|
+
throw new UserError(
|
|
6476
|
+
`refusing rollback ${backupId}: current file(s) differ from the state recorded right after promote \u2014 ${mismatches.join(", ")} (pass --force to override)`
|
|
6477
|
+
);
|
|
6478
|
+
}
|
|
6479
|
+
const fileList = manifest.files.map((file) => ` ${file.existed ? "restore" : "delete"} ${file.path}`).join("\n");
|
|
6480
|
+
if (!options.json) {
|
|
6481
|
+
console.log(`[skill-forge] refine rollback ${backupId} \u2014 restoring ${manifest.files.length} file(s):`);
|
|
6482
|
+
console.log(fileList);
|
|
6483
|
+
for (const w of warnings) console.log(`[skill-forge] warning: ${w}`);
|
|
6484
|
+
}
|
|
6485
|
+
const proceed = await confirmWrite(options.yes, options.json, `
|
|
6486
|
+
Roll back backup ${backupId}? Affected files:
|
|
6487
|
+
${fileList}
|
|
6488
|
+
[y/N] `);
|
|
6489
|
+
if (!proceed) {
|
|
6490
|
+
emit(options.json, { rolledBack: false, cancelled: true, backupId }, () => {
|
|
6491
|
+
console.log("[skill-forge] rollback cancelled \u2014 nothing restored.");
|
|
6492
|
+
});
|
|
6493
|
+
return;
|
|
6494
|
+
}
|
|
6495
|
+
const restored = [];
|
|
6496
|
+
for (const file of manifest.files) {
|
|
6497
|
+
try {
|
|
6498
|
+
if (!file.existed) {
|
|
6499
|
+
safeDeleteTombstone(file.path, config);
|
|
6500
|
+
} else {
|
|
6501
|
+
safeRestoreWrite(file.path, Buffer.from(file.contentBase64 ?? "", "base64"), file.mode ?? 420, config);
|
|
6502
|
+
}
|
|
6503
|
+
restored.push(file.path);
|
|
6504
|
+
} catch (err) {
|
|
6505
|
+
const remaining = manifest.files.slice(restored.length + 1).map((f) => f.path);
|
|
6506
|
+
throw new UserError(
|
|
6507
|
+
`rollback ${backupId} failed partway through: restored ${restored.length} of ${manifest.files.length} file(s) (${restored.join(", ") || "(none)"}), then failed on ${file.path}: ${err.message}. NOT restored: ${[file.path, ...remaining].join(", ")}.`
|
|
6508
|
+
);
|
|
6509
|
+
}
|
|
6510
|
+
}
|
|
6511
|
+
const patternsStore = readPatternsStore();
|
|
6512
|
+
const pattern = patternsStore.records.find((p) => p.generalizedBackupId === backupId);
|
|
6513
|
+
if (pattern) {
|
|
6514
|
+
pattern.status = "ready";
|
|
6515
|
+
pattern.generalizedAt = void 0;
|
|
6516
|
+
pattern.generalizedBackupId = void 0;
|
|
6517
|
+
writePatternsStore(patternsStore);
|
|
6518
|
+
}
|
|
6519
|
+
emit(options.json, { rolledBack: true, backupId, patternReset: pattern?.id, warnings }, () => {
|
|
6520
|
+
console.log(`[skill-forge] rollback complete for ${backupId}${pattern ? ` (pattern ${pattern.id} reset to ready)` : ""}`);
|
|
6521
|
+
for (const w of warnings) console.log(`[skill-forge] warning: ${w}`);
|
|
6522
|
+
});
|
|
6523
|
+
}
|
|
6524
|
+
|
|
5025
6525
|
// src/cli.ts
|
|
5026
6526
|
var require2 = createRequire(import.meta.url);
|
|
5027
6527
|
var pkg = require2("../package.json");
|
|
@@ -5064,6 +6564,13 @@ program.command("ingest").description(
|
|
|
5064
6564
|
).option("--list", "print pending queue entries and exit \u2014 no handoff").option("--json", "with --list: print pending entries as JSON").action(ingest);
|
|
5065
6565
|
var queue = program.command("queue").description("Manage the pending-ingestion queue (Pro)");
|
|
5066
6566
|
queue.command("close <id>").description("Close a queue entry after the decide pass \u2014 agents use this instead of hand-editing queue.json").requiredOption("--status <status>", 'final status: "ingested" or "dismissed"').action(queueClose);
|
|
6567
|
+
var refine = program.command("refine").description("Capture, apply, and generalize improvements to installed skills from usage feedback (Pro)");
|
|
6568
|
+
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);
|
|
6569
|
+
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);
|
|
6570
|
+
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);
|
|
6571
|
+
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);
|
|
6572
|
+
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);
|
|
6573
|
+
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);
|
|
5067
6574
|
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").action(init);
|
|
5068
6575
|
try {
|
|
5069
6576
|
await program.parseAsync();
|