opencode-swarm 7.112.1 → 7.113.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/.opencode/skills/brainstorm/SKILL.md +1 -0
- package/.opencode/skills/ci-failure-batching/SKILL.md +2 -1
- package/.opencode/skills/ci-fix-monitor/SKILL.md +203 -0
- package/.opencode/skills/clarify/SKILL.md +1 -0
- package/.opencode/skills/clarify-spec/SKILL.md +1 -0
- package/.opencode/skills/codebase-review-swarm/SKILL.md +1 -0
- package/.opencode/skills/commit-pr/SKILL.md +15 -0
- package/.opencode/skills/consult/SKILL.md +1 -0
- package/.opencode/skills/council/SKILL.md +1 -0
- package/.opencode/skills/critic-gate/SKILL.md +1 -0
- package/.opencode/skills/deep-dive/SKILL.md +1 -0
- package/.opencode/skills/deep-research/SKILL.md +1 -0
- package/.opencode/skills/design-docs/SKILL.md +2 -1
- package/.opencode/skills/discover/SKILL.md +1 -0
- package/.opencode/skills/engineering-conventions/SKILL.md +2 -1
- package/.opencode/skills/execute/SKILL.md +1 -0
- package/.opencode/skills/fork-pr-operations/SKILL.md +136 -0
- package/.opencode/skills/gate-attribution/SKILL.md +2 -1
- package/.opencode/skills/issue-ingest/SKILL.md +1 -0
- package/.opencode/skills/loop/SKILL.md +7 -6
- package/.opencode/skills/merge-queue-readiness/SKILL.md +2 -1
- package/.opencode/skills/parallel-work-check/SKILL.md +133 -0
- package/.opencode/skills/phase-wrap/SKILL.md +1 -0
- package/.opencode/skills/plan/SKILL.md +1 -0
- package/.opencode/skills/pre-phase-briefing/SKILL.md +1 -0
- package/.opencode/skills/resume/SKILL.md +1 -0
- package/.opencode/skills/running-tests/SKILL.md +1 -0
- package/.opencode/skills/skill-edit-validation/SKILL.md +2 -1
- package/.opencode/skills/specify/SKILL.md +1 -0
- package/.opencode/skills/swarm/SKILL.md +1 -0
- package/.opencode/skills/swarm-ci-monitor/SKILL.md +5 -4
- package/.opencode/skills/swarm-implement/SKILL.md +4 -4
- package/.opencode/skills/swarm-pr-feedback/SKILL.md +26 -5
- package/.opencode/skills/swarm-pr-review/SKILL.md +2 -1
- package/.opencode/skills/swarm-pr-subscribe/SKILL.md +1 -0
- package/.opencode/skills/test-file-split/SKILL.md +102 -0
- package/.opencode/skills/worktree-retry-cleanup/SKILL.md +2 -1
- package/.opencode/skills/writing-tests/SKILL.md +43 -0
- package/dist/cli/{config-doctor-mmw4c50z.js → config-doctor-az11cwf5.js} +2 -2
- package/dist/cli/{curator-llm-factory-vxa4pz48.js → curator-llm-factory-3t0ehhfe.js} +5 -5
- package/dist/cli/{curator-ztdgdpb6.js → curator-pdcpw4x6.js} +5 -5
- package/dist/cli/{guardrail-explain-gv1ht6b0.js → guardrail-explain-xsfdc49z.js} +6 -6
- package/dist/cli/{guardrail-log-xjgr00eq.js → guardrail-log-hysxdwg9.js} +3 -3
- package/dist/cli/{hive-promoter-wdextf8h.js → hive-promoter-4htgk6zb.js} +5 -5
- package/dist/cli/{index-xehrmy76.js → index-0gf6de8b.js} +1 -1
- package/dist/cli/{index-cq2hgvd2.js → index-1ey3dxq7.js} +9 -1
- package/dist/cli/{index-hy5r0mgf.js → index-6gdxwfsh.js} +7 -7
- package/dist/cli/{index-npxmfj48.js → index-8f256d7t.js} +1 -1
- package/dist/cli/{index-20hyxe3z.js → index-bsvwnzh7.js} +1989 -935
- package/dist/cli/{index-82v0d72f.js → index-h88ktb5r.js} +418 -87
- package/dist/cli/{index-w84stmkx.js → index-r9fjzr3k.js} +1 -1
- package/dist/cli/{index-azf2bmr5.js → index-v96kmbkw.js} +2 -2
- package/dist/cli/index.js +5 -5
- package/dist/cli/{schema-2p15yrvn.js → schema-qb313fyy.js} +1 -1
- package/dist/cli/{skill-generator-fe6q8xgm.js → skill-generator-0my4jrx6.js} +1 -1
- package/dist/commands/loop.d.ts +1 -1
- package/dist/commands/registry.d.ts +1 -1
- package/dist/config/bundled-skills.d.ts +15 -5
- package/dist/config/schema.d.ts +2 -0
- package/dist/hooks/skill-propagation-gate.d.ts +45 -5
- package/dist/hooks/skill-scoring.d.ts +22 -1
- package/dist/index.js +83 -83
- package/package.json +5 -1
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./index-bfwt3abw.js";
|
|
20
20
|
import {
|
|
21
21
|
init_logger,
|
|
22
|
+
log,
|
|
22
23
|
warn
|
|
23
24
|
} from "./index-zgwm4ryv.js";
|
|
24
25
|
import {
|
|
@@ -27,34 +28,345 @@ import {
|
|
|
27
28
|
|
|
28
29
|
// src/services/skill-generator.ts
|
|
29
30
|
import { existsSync as existsSync2, unlinkSync } from "fs";
|
|
30
|
-
import { mkdir as
|
|
31
|
-
import * as
|
|
31
|
+
import { mkdir as mkdir4, readFile as readFile4, rename as rename3, unlink, writeFile as writeFile4 } from "fs/promises";
|
|
32
|
+
import * as path4 from "path";
|
|
33
|
+
|
|
34
|
+
// src/config/bundled-skills.ts
|
|
35
|
+
import { randomUUID } from "crypto";
|
|
36
|
+
import * as fsp from "fs/promises";
|
|
37
|
+
import * as path from "path";
|
|
38
|
+
|
|
39
|
+
// src/services/warning-buffer.ts
|
|
40
|
+
init_logger();
|
|
41
|
+
var deferredWarnings = [];
|
|
42
|
+
var MAX_DEFERRED_WARNINGS = 50;
|
|
43
|
+
function addDeferredWarning(warning) {
|
|
44
|
+
if (deferredWarnings.length < MAX_DEFERRED_WARNINGS) {
|
|
45
|
+
deferredWarnings.push(warning);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function advisoryWarn(message, data) {
|
|
49
|
+
addDeferredWarning(message);
|
|
50
|
+
log(message, data);
|
|
51
|
+
}
|
|
52
|
+
function getDeferredWarnings() {
|
|
53
|
+
return [...deferredWarnings];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/config/bundled-skills.ts
|
|
57
|
+
init_logger();
|
|
58
|
+
var BUNDLED_PROJECT_SKILLS = [
|
|
59
|
+
"brainstorm",
|
|
60
|
+
"specify",
|
|
61
|
+
"clarify-spec",
|
|
62
|
+
"resume",
|
|
63
|
+
"clarify",
|
|
64
|
+
"discover",
|
|
65
|
+
"consult",
|
|
66
|
+
"pre-phase-briefing",
|
|
67
|
+
"council",
|
|
68
|
+
"deep-dive",
|
|
69
|
+
"deep-research",
|
|
70
|
+
"codebase-review-swarm",
|
|
71
|
+
"swarm-implement",
|
|
72
|
+
"design-docs",
|
|
73
|
+
"swarm-pr-review",
|
|
74
|
+
"swarm",
|
|
75
|
+
"swarm-pr-feedback",
|
|
76
|
+
"swarm-pr-subscribe",
|
|
77
|
+
"swarm-ci-monitor",
|
|
78
|
+
"issue-ingest",
|
|
79
|
+
"plan",
|
|
80
|
+
"critic-gate",
|
|
81
|
+
"execute",
|
|
82
|
+
"phase-wrap",
|
|
83
|
+
"loop",
|
|
84
|
+
"writing-tests",
|
|
85
|
+
"running-tests",
|
|
86
|
+
"engineering-conventions",
|
|
87
|
+
"commit-pr",
|
|
88
|
+
"ci-failure-batching",
|
|
89
|
+
"gate-attribution",
|
|
90
|
+
"merge-queue-readiness",
|
|
91
|
+
"skill-edit-validation",
|
|
92
|
+
"worktree-retry-cleanup",
|
|
93
|
+
"test-file-split",
|
|
94
|
+
"fork-pr-operations",
|
|
95
|
+
"parallel-work-check",
|
|
96
|
+
"ci-fix-monitor"
|
|
97
|
+
];
|
|
98
|
+
var BUNDLED_PROJECT_SKILL_ROOT = ".swarm/bundled-skills";
|
|
99
|
+
function bundledProjectSkillFileReference(slug) {
|
|
100
|
+
return `file:${BUNDLED_PROJECT_SKILL_ROOT}/${slug}/SKILL.md`;
|
|
101
|
+
}
|
|
102
|
+
var MAX_SKILL_FILES = 64;
|
|
103
|
+
var MAX_SKILL_BYTES = 512000;
|
|
104
|
+
var MAX_IN_FLIGHT_SYNCS = 64;
|
|
105
|
+
var inFlightSyncs = new Map;
|
|
106
|
+
function describeBundledSkillSyncFailure(err) {
|
|
107
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
108
|
+
return `[opencode-swarm] Could not install bundled project skills; continuing without sync: ${message}`;
|
|
109
|
+
}
|
|
110
|
+
async function isSymbolicLinkAsync(p) {
|
|
111
|
+
try {
|
|
112
|
+
return (await fsp.lstat(p)).isSymbolicLink();
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function ensureNotSymlinkedDirectoryAsync(p) {
|
|
118
|
+
try {
|
|
119
|
+
const stat2 = await fsp.lstat(p);
|
|
120
|
+
return stat2.isDirectory() && !stat2.isSymbolicLink();
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return err.code === "ENOENT";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function pathExistsAsync(p) {
|
|
126
|
+
try {
|
|
127
|
+
await fsp.access(p);
|
|
128
|
+
return true;
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function collectBundledSkillFilesBoundedAsync(sourceDir, state, relativeDir = "") {
|
|
134
|
+
const currentSource = path.join(sourceDir, relativeDir);
|
|
135
|
+
const entries = await fsp.readdir(currentSource, { withFileTypes: true });
|
|
136
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
137
|
+
const files = [];
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
const relativeEntry = path.join(relativeDir, entry.name);
|
|
140
|
+
const sourcePath = path.join(sourceDir, relativeEntry);
|
|
141
|
+
if (entry.isSymbolicLink() || await isSymbolicLinkAsync(sourcePath)) {
|
|
142
|
+
throw new Error("refusing to copy symlinked bundled skill entry");
|
|
143
|
+
}
|
|
144
|
+
if (entry.isDirectory()) {
|
|
145
|
+
files.push(...await collectBundledSkillFilesBoundedAsync(sourceDir, state, relativeEntry));
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (!entry.isFile())
|
|
149
|
+
continue;
|
|
150
|
+
const stat2 = await fsp.stat(sourcePath);
|
|
151
|
+
const nextFiles = state.files + 1;
|
|
152
|
+
const nextBytes = state.bytes + stat2.size;
|
|
153
|
+
if (nextFiles > MAX_SKILL_FILES || nextBytes > MAX_SKILL_BYTES) {
|
|
154
|
+
throw new Error("bundled skill package exceeds copy bounds");
|
|
155
|
+
}
|
|
156
|
+
state.files = nextFiles;
|
|
157
|
+
state.bytes = nextBytes;
|
|
158
|
+
files.push({ relativePath: relativeEntry });
|
|
159
|
+
}
|
|
160
|
+
return files;
|
|
161
|
+
}
|
|
162
|
+
async function ensureContainedDirectoryAsync(rootDir, directory) {
|
|
163
|
+
const safeRoot = path.resolve(rootDir);
|
|
164
|
+
const safeDirectory = path.resolve(directory);
|
|
165
|
+
const relative2 = path.relative(safeRoot, safeDirectory);
|
|
166
|
+
if (relative2.startsWith("..") || path.isAbsolute(relative2)) {
|
|
167
|
+
throw new Error("bundled skill destination escaped its private root");
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const rootStat = await fsp.lstat(safeRoot);
|
|
171
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
172
|
+
throw new Error("refusing to traverse unsafe bundled skill directory");
|
|
173
|
+
}
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (err.code !== "ENOENT")
|
|
176
|
+
throw err;
|
|
177
|
+
await fsp.mkdir(safeRoot, { recursive: true });
|
|
178
|
+
const rootStat = await fsp.lstat(safeRoot);
|
|
179
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
180
|
+
throw new Error("refusing to traverse unsafe bundled skill directory");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
let current = safeRoot;
|
|
184
|
+
for (const segment of relative2.split(path.sep).filter(Boolean)) {
|
|
185
|
+
current = path.join(current, segment);
|
|
186
|
+
try {
|
|
187
|
+
const stat2 = await fsp.lstat(current);
|
|
188
|
+
if (stat2.isSymbolicLink() || !stat2.isDirectory()) {
|
|
189
|
+
throw new Error("refusing to traverse unsafe bundled skill directory");
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
if (err.code !== "ENOENT")
|
|
193
|
+
throw err;
|
|
194
|
+
try {
|
|
195
|
+
await fsp.mkdir(current);
|
|
196
|
+
} catch (mkdirErr) {
|
|
197
|
+
if (mkdirErr.code !== "EEXIST") {
|
|
198
|
+
throw mkdirErr;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const stat2 = await fsp.lstat(current);
|
|
202
|
+
if (stat2.isSymbolicLink() || !stat2.isDirectory()) {
|
|
203
|
+
throw new Error("refusing to traverse unsafe bundled skill directory");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function rollbackCopiedFilesAsync(copiedFiles, destDir, overwrittenFiles = []) {
|
|
209
|
+
const safeDestDir = path.resolve(destDir);
|
|
210
|
+
const dirs = new Set;
|
|
211
|
+
for (const copiedFile of copiedFiles) {
|
|
212
|
+
const resolvedFile = path.resolve(copiedFile);
|
|
213
|
+
const relative2 = path.relative(safeDestDir, resolvedFile);
|
|
214
|
+
if (relative2.startsWith("..") || path.isAbsolute(relative2))
|
|
215
|
+
continue;
|
|
216
|
+
try {
|
|
217
|
+
await fsp.rm(resolvedFile, { force: true });
|
|
218
|
+
} catch {}
|
|
219
|
+
dirs.add(path.dirname(resolvedFile));
|
|
220
|
+
}
|
|
221
|
+
for (const overwritten of overwrittenFiles.reverse()) {
|
|
222
|
+
const resolvedFile = path.resolve(overwritten.path);
|
|
223
|
+
const relative2 = path.relative(safeDestDir, resolvedFile);
|
|
224
|
+
if (relative2.startsWith("..") || path.isAbsolute(relative2))
|
|
225
|
+
continue;
|
|
226
|
+
try {
|
|
227
|
+
await fsp.writeFile(resolvedFile, overwritten.contents);
|
|
228
|
+
} catch {}
|
|
229
|
+
}
|
|
230
|
+
for (const dir of [...dirs].sort((a, b) => b.length - a.length)) {
|
|
231
|
+
const relative2 = path.relative(safeDestDir, path.resolve(dir));
|
|
232
|
+
if (relative2.startsWith("..") || path.isAbsolute(relative2))
|
|
233
|
+
continue;
|
|
234
|
+
try {
|
|
235
|
+
await fsp.rmdir(dir);
|
|
236
|
+
} catch {}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function copyFileAtomicAsync(sourcePath, destPath) {
|
|
240
|
+
const tempPath = path.join(path.dirname(destPath), `.${path.basename(destPath)}.tmp.${randomUUID()}`);
|
|
241
|
+
try {
|
|
242
|
+
await fsp.copyFile(sourcePath, tempPath);
|
|
243
|
+
await fsp.rename(tempPath, destPath);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
try {
|
|
246
|
+
await fsp.rm(tempPath, { force: true });
|
|
247
|
+
} catch {}
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function copyBundledDirectoryBoundedAsync(sourceDir, destDir) {
|
|
252
|
+
const files = await collectBundledSkillFilesBoundedAsync(sourceDir, {
|
|
253
|
+
files: 0,
|
|
254
|
+
bytes: 0
|
|
255
|
+
});
|
|
256
|
+
const copiedFiles = [];
|
|
257
|
+
const overwrittenFiles = [];
|
|
258
|
+
try {
|
|
259
|
+
for (const file of files) {
|
|
260
|
+
const sourcePath = path.join(sourceDir, file.relativePath);
|
|
261
|
+
const destPath = path.join(destDir, file.relativePath);
|
|
262
|
+
await ensureContainedDirectoryAsync(destDir, path.dirname(destPath));
|
|
263
|
+
if (await pathExistsAsync(destPath)) {
|
|
264
|
+
if (await isSymbolicLinkAsync(destPath)) {
|
|
265
|
+
throw new Error("refusing to overwrite symlinked bundled skill file");
|
|
266
|
+
}
|
|
267
|
+
const [sourceContents, destContents] = await Promise.all([
|
|
268
|
+
fsp.readFile(sourcePath),
|
|
269
|
+
fsp.readFile(destPath)
|
|
270
|
+
]);
|
|
271
|
+
if (sourceContents.equals(destContents)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
overwrittenFiles.push({ path: destPath, contents: destContents });
|
|
275
|
+
await copyFileAtomicAsync(sourcePath, destPath);
|
|
276
|
+
} else {
|
|
277
|
+
await copyFileAtomicAsync(sourcePath, destPath);
|
|
278
|
+
copiedFiles.push(destPath);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} catch (err) {
|
|
282
|
+
await rollbackCopiedFilesAsync(copiedFiles, destDir, overwrittenFiles);
|
|
283
|
+
throw err;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function performBundledProjectSkillSyncAsync(projectDirectory, packageRoot, quiet) {
|
|
287
|
+
try {
|
|
288
|
+
const sourceRoot = path.join(packageRoot, ".opencode", "skills");
|
|
289
|
+
const swarmDir = path.join(projectDirectory, ".swarm");
|
|
290
|
+
const skillsDir = path.join(projectDirectory, BUNDLED_PROJECT_SKILL_ROOT);
|
|
291
|
+
if (!await ensureNotSymlinkedDirectoryAsync(sourceRoot)) {
|
|
292
|
+
throw new Error("refusing to copy symlinked bundled skill source root");
|
|
293
|
+
}
|
|
294
|
+
if (!await ensureNotSymlinkedDirectoryAsync(swarmDir))
|
|
295
|
+
return;
|
|
296
|
+
if (!await ensureNotSymlinkedDirectoryAsync(skillsDir))
|
|
297
|
+
return;
|
|
298
|
+
for (const slug of BUNDLED_PROJECT_SKILLS) {
|
|
299
|
+
const sourceDir = path.join(sourceRoot, slug);
|
|
300
|
+
const sourceSkill = path.join(sourceDir, "SKILL.md");
|
|
301
|
+
const destDir = path.join(skillsDir, slug);
|
|
302
|
+
if (!await pathExistsAsync(sourceSkill))
|
|
303
|
+
continue;
|
|
304
|
+
if (!await ensureNotSymlinkedDirectoryAsync(sourceDir)) {
|
|
305
|
+
throw new Error("refusing to copy symlinked bundled skill directory");
|
|
306
|
+
}
|
|
307
|
+
if (!await ensureNotSymlinkedDirectoryAsync(destDir))
|
|
308
|
+
continue;
|
|
309
|
+
await copyBundledDirectoryBoundedAsync(sourceDir, destDir);
|
|
310
|
+
log("synchronized bundled skill", {
|
|
311
|
+
slug: bundledProjectSkillFileReference(slug)
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
} catch (err) {
|
|
315
|
+
const failureMsg = describeBundledSkillSyncFailure(err);
|
|
316
|
+
if (quiet) {
|
|
317
|
+
advisoryWarn(failureMsg);
|
|
318
|
+
} else {
|
|
319
|
+
console.warn(failureMsg);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function syncBundledProjectSkillsIfMissingAsync(projectDirectory, packageRoot, quiet = false) {
|
|
324
|
+
const syncKey = path.resolve(projectDirectory, BUNDLED_PROJECT_SKILL_ROOT);
|
|
325
|
+
const existing = inFlightSyncs.get(syncKey);
|
|
326
|
+
if (existing)
|
|
327
|
+
return existing;
|
|
328
|
+
if (inFlightSyncs.size >= MAX_IN_FLIGHT_SYNCS) {
|
|
329
|
+
log("skipping bundled skill sync because the in-flight limit was reached", {
|
|
330
|
+
limit: MAX_IN_FLIGHT_SYNCS
|
|
331
|
+
});
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const syncPromise = performBundledProjectSkillSyncAsync(projectDirectory, packageRoot, quiet).finally(() => {
|
|
335
|
+
if (inFlightSyncs.get(syncKey) === syncPromise) {
|
|
336
|
+
inFlightSyncs.delete(syncKey);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
inFlightSyncs.set(syncKey, syncPromise);
|
|
340
|
+
return syncPromise;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/services/skill-generator.ts
|
|
32
344
|
init_logger();
|
|
33
345
|
|
|
34
346
|
// src/services/skill-changelog.ts
|
|
35
347
|
init_logger();
|
|
36
|
-
import { appendFile, mkdir, readFile, writeFile } from "fs/promises";
|
|
37
|
-
import * as
|
|
348
|
+
import { appendFile, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
349
|
+
import * as path2 from "path";
|
|
38
350
|
var MAX_CHANGELOG_ENTRIES_PER_SKILL = 200;
|
|
39
351
|
function resolveSkillChangelogPath(directory, slug) {
|
|
40
352
|
if (slug.includes("..") || slug.includes("/") || slug.includes("\\")) {
|
|
41
353
|
throw new Error(`Invalid skill slug: ${slug} \u2014 must not contain "..", "/" or "\\"`);
|
|
42
354
|
}
|
|
43
|
-
return
|
|
355
|
+
return path2.join(directory, ".swarm", "skill-changelogs", `${slug}.jsonl`);
|
|
44
356
|
}
|
|
45
357
|
async function appendSkillChangelog(directory, slug, entry) {
|
|
46
358
|
const filePath = resolveSkillChangelogPath(directory, slug);
|
|
47
|
-
const dirPath =
|
|
48
|
-
await
|
|
359
|
+
const dirPath = path2.dirname(filePath);
|
|
360
|
+
await mkdir2(dirPath, { recursive: true });
|
|
49
361
|
await appendFile(filePath, `${JSON.stringify(entry)}
|
|
50
362
|
`, "utf-8");
|
|
51
363
|
try {
|
|
52
|
-
const content = await
|
|
364
|
+
const content = await readFile2(filePath, "utf-8");
|
|
53
365
|
const lines = content.split(`
|
|
54
366
|
`).filter((line) => line.trim().length > 0);
|
|
55
367
|
if (lines.length > MAX_CHANGELOG_ENTRIES_PER_SKILL) {
|
|
56
368
|
const trimmed = lines.slice(lines.length - MAX_CHANGELOG_ENTRIES_PER_SKILL);
|
|
57
|
-
await
|
|
369
|
+
await writeFile2(filePath, `${trimmed.join(`
|
|
58
370
|
`)}
|
|
59
371
|
`, "utf-8");
|
|
60
372
|
}
|
|
@@ -67,16 +379,16 @@ async function appendSkillChangelog(directory, slug, entry) {
|
|
|
67
379
|
import { createHash } from "crypto";
|
|
68
380
|
import { existsSync } from "fs";
|
|
69
381
|
import {
|
|
70
|
-
lstat,
|
|
71
|
-
mkdir as
|
|
72
|
-
readdir,
|
|
73
|
-
readFile as
|
|
382
|
+
lstat as lstat2,
|
|
383
|
+
mkdir as mkdir3,
|
|
384
|
+
readdir as readdir2,
|
|
385
|
+
readFile as readFile3,
|
|
74
386
|
realpath,
|
|
75
|
-
rename,
|
|
76
|
-
stat,
|
|
77
|
-
writeFile as
|
|
387
|
+
rename as rename2,
|
|
388
|
+
stat as stat2,
|
|
389
|
+
writeFile as writeFile3
|
|
78
390
|
} from "fs/promises";
|
|
79
|
-
import * as
|
|
391
|
+
import * as path3 from "path";
|
|
80
392
|
var SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
81
393
|
var MAX_EVAL_FILES = 50;
|
|
82
394
|
var MAX_EVAL_FILE_BYTES = 64 * 1024;
|
|
@@ -142,15 +454,15 @@ function casesFromParsedJson(parsed, fileLabel) {
|
|
|
142
454
|
return cases;
|
|
143
455
|
}
|
|
144
456
|
function evalRoot(directory, slug) {
|
|
145
|
-
return
|
|
457
|
+
return path3.join(directory, ".swarm", "skills", "evals", slug);
|
|
146
458
|
}
|
|
147
459
|
function isInsidePath(root, target) {
|
|
148
|
-
const resolvedRoot =
|
|
149
|
-
const resolvedTarget =
|
|
150
|
-
return resolvedTarget === resolvedRoot || resolvedTarget.startsWith(resolvedRoot +
|
|
460
|
+
const resolvedRoot = path3.resolve(root);
|
|
461
|
+
const resolvedTarget = path3.resolve(target);
|
|
462
|
+
return resolvedTarget === resolvedRoot || resolvedTarget.startsWith(resolvedRoot + path3.sep);
|
|
151
463
|
}
|
|
152
464
|
function rejectedEditsPath(directory) {
|
|
153
|
-
return
|
|
465
|
+
return path3.join(directory, ".swarm", "skills", "rejected-edits.jsonl");
|
|
154
466
|
}
|
|
155
467
|
async function loadEvalSet(directory, slug) {
|
|
156
468
|
if (!isValidSlug(slug)) {
|
|
@@ -187,7 +499,7 @@ async function loadEvalSet(directory, slug) {
|
|
|
187
499
|
}
|
|
188
500
|
let entries;
|
|
189
501
|
try {
|
|
190
|
-
entries = await
|
|
502
|
+
entries = await readdir2(root);
|
|
191
503
|
} catch (err) {
|
|
192
504
|
return {
|
|
193
505
|
status: "invalid",
|
|
@@ -203,9 +515,9 @@ async function loadEvalSet(directory, slug) {
|
|
|
203
515
|
const cases = [];
|
|
204
516
|
const fileLabels = [];
|
|
205
517
|
for (const file of files) {
|
|
206
|
-
const fullPath =
|
|
207
|
-
const resolved =
|
|
208
|
-
const resolvedRoot =
|
|
518
|
+
const fullPath = path3.join(root, file);
|
|
519
|
+
const resolved = path3.resolve(fullPath);
|
|
520
|
+
const resolvedRoot = path3.resolve(root);
|
|
209
521
|
if (!isInsidePath(resolvedRoot, resolved)) {
|
|
210
522
|
return {
|
|
211
523
|
status: "invalid",
|
|
@@ -217,7 +529,7 @@ async function loadEvalSet(directory, slug) {
|
|
|
217
529
|
let readPath = fullPath;
|
|
218
530
|
let info;
|
|
219
531
|
try {
|
|
220
|
-
const linkInfo = await
|
|
532
|
+
const linkInfo = await lstat2(fullPath);
|
|
221
533
|
if (linkInfo.isSymbolicLink()) {
|
|
222
534
|
const realFile = await realpath(fullPath);
|
|
223
535
|
if (!isInsidePath(realRoot, realFile)) {
|
|
@@ -229,7 +541,7 @@ async function loadEvalSet(directory, slug) {
|
|
|
229
541
|
};
|
|
230
542
|
}
|
|
231
543
|
readPath = realFile;
|
|
232
|
-
info = await
|
|
544
|
+
info = await stat2(realFile);
|
|
233
545
|
} else {
|
|
234
546
|
if (!linkInfo.isFile())
|
|
235
547
|
continue;
|
|
@@ -255,7 +567,7 @@ async function loadEvalSet(directory, slug) {
|
|
|
255
567
|
}
|
|
256
568
|
let parsed;
|
|
257
569
|
try {
|
|
258
|
-
parsed = JSON.parse(await
|
|
570
|
+
parsed = JSON.parse(await readFile3(readPath, "utf-8"));
|
|
259
571
|
} catch (err) {
|
|
260
572
|
return {
|
|
261
573
|
status: "invalid",
|
|
@@ -264,7 +576,7 @@ async function loadEvalSet(directory, slug) {
|
|
|
264
576
|
reason: `eval file invalid JSON: ${file}: ${err instanceof Error ? err.message : String(err)}`
|
|
265
577
|
};
|
|
266
578
|
}
|
|
267
|
-
fileLabels.push(
|
|
579
|
+
fileLabels.push(path3.relative(directory, fullPath).replace(/\\/g, "/"));
|
|
268
580
|
cases.push(...casesFromParsedJson(parsed, file));
|
|
269
581
|
if (cases.length >= MAX_EVAL_CASES)
|
|
270
582
|
break;
|
|
@@ -382,10 +694,10 @@ async function evaluateSkillChange(req) {
|
|
|
382
694
|
};
|
|
383
695
|
}
|
|
384
696
|
async function atomicWrite(filePath, content) {
|
|
385
|
-
await
|
|
697
|
+
await mkdir3(path3.dirname(filePath), { recursive: true });
|
|
386
698
|
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
387
|
-
await
|
|
388
|
-
await
|
|
699
|
+
await writeFile3(tmp, content, "utf-8");
|
|
700
|
+
await rename2(tmp, filePath);
|
|
389
701
|
}
|
|
390
702
|
async function appendRejectedSkillEdit(req, evaluation) {
|
|
391
703
|
const filePath = rejectedEditsPath(req.directory);
|
|
@@ -405,7 +717,7 @@ async function appendRejectedSkillEdit(req, evaluation) {
|
|
|
405
717
|
};
|
|
406
718
|
let prior = [];
|
|
407
719
|
try {
|
|
408
|
-
prior = (await
|
|
720
|
+
prior = (await readFile3(filePath, "utf-8")).split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
409
721
|
} catch {
|
|
410
722
|
prior = [];
|
|
411
723
|
}
|
|
@@ -422,7 +734,7 @@ async function isRejectedSkillContent(directory, slug, content) {
|
|
|
422
734
|
const normalizedHash = hashNormalizedContent(content);
|
|
423
735
|
let raw = "";
|
|
424
736
|
try {
|
|
425
|
-
raw = await
|
|
737
|
+
raw = await readFile3(rejectedEditsPath(directory), "utf-8");
|
|
426
738
|
} catch {
|
|
427
739
|
return false;
|
|
428
740
|
}
|
|
@@ -449,6 +761,10 @@ async function isRejectedSkillContent(directory, slug, content) {
|
|
|
449
761
|
|
|
450
762
|
// src/services/skill-generator.ts
|
|
451
763
|
var SLUG_PATTERN2 = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
764
|
+
var RESERVED_ACTIVE_SKILL_SLUGS = new Set(BUNDLED_PROJECT_SKILLS);
|
|
765
|
+
function reservedActiveSlugReason(slug) {
|
|
766
|
+
return `reserved bundled skill slug "${slug}" cannot be used for an active generated skill`;
|
|
767
|
+
}
|
|
452
768
|
function sanitizeSlug(input) {
|
|
453
769
|
const lc = input.toLowerCase().trim();
|
|
454
770
|
const mapped = lc.replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-");
|
|
@@ -459,10 +775,10 @@ function isValidSlug2(slug) {
|
|
|
459
775
|
return SLUG_PATTERN2.test(slug);
|
|
460
776
|
}
|
|
461
777
|
function proposalPath(directory, slug) {
|
|
462
|
-
return
|
|
778
|
+
return path4.join(directory, ".swarm", "skills", "proposals", `${slug}.md`);
|
|
463
779
|
}
|
|
464
780
|
function activePath(directory, slug) {
|
|
465
|
-
return
|
|
781
|
+
return path4.join(directory, ".opencode", "skills", "generated", slug, "SKILL.md");
|
|
466
782
|
}
|
|
467
783
|
function activeRepoRelativePath(slug) {
|
|
468
784
|
return `.opencode/skills/generated/${slug}/SKILL.md`;
|
|
@@ -486,12 +802,12 @@ function generateEvalStub(cluster) {
|
|
|
486
802
|
];
|
|
487
803
|
}
|
|
488
804
|
async function writeEvalStub(directory, slug, cases) {
|
|
489
|
-
const stubPath =
|
|
805
|
+
const stubPath = path4.join(directory, ".swarm", "skills", "evals", slug, "auto-stub.json");
|
|
490
806
|
if (cases.length === 0) {
|
|
491
807
|
return { written: false, path: stubPath, reason: "no directive fields" };
|
|
492
808
|
}
|
|
493
809
|
try {
|
|
494
|
-
await
|
|
810
|
+
await mkdir4(path4.dirname(stubPath), { recursive: true });
|
|
495
811
|
await atomicWrite2(stubPath, `${JSON.stringify({ cases })}
|
|
496
812
|
`);
|
|
497
813
|
return { written: true, path: stubPath };
|
|
@@ -726,10 +1042,10 @@ function escapeMarkdown(s) {
|
|
|
726
1042
|
return s.replace(/[\r\n]+/g, " ").slice(0, 280);
|
|
727
1043
|
}
|
|
728
1044
|
async function atomicWrite2(p, content) {
|
|
729
|
-
await
|
|
1045
|
+
await mkdir4(path4.dirname(p), { recursive: true });
|
|
730
1046
|
const tmp = `${p}.tmp-${process.pid}-${Date.now()}`;
|
|
731
|
-
await
|
|
732
|
-
await
|
|
1047
|
+
await writeFile4(tmp, content, "utf-8");
|
|
1048
|
+
await rename3(tmp, p);
|
|
733
1049
|
}
|
|
734
1050
|
async function generateSkills(req) {
|
|
735
1051
|
const minConfidence = req.minConfidence ?? DEFAULT_SKILL_MIN_CONFIDENCE;
|
|
@@ -778,8 +1094,15 @@ async function generateSkills(req) {
|
|
|
778
1094
|
});
|
|
779
1095
|
continue;
|
|
780
1096
|
}
|
|
1097
|
+
if (req.mode === "active" && RESERVED_ACTIVE_SKILL_SLUGS.has(cluster.slug)) {
|
|
1098
|
+
result.skipped.push({
|
|
1099
|
+
slug: cluster.slug,
|
|
1100
|
+
reason: reservedActiveSlugReason(cluster.slug)
|
|
1101
|
+
});
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
781
1104
|
const targetPath = req.mode === "active" ? activePath(req.directory, cluster.slug) : proposalPath(req.directory, cluster.slug);
|
|
782
|
-
const repoRel =
|
|
1105
|
+
const repoRel = path4.relative(req.directory, targetPath).replace(/\\/g, "/");
|
|
783
1106
|
if (!validateSkillPath(repoRel)) {
|
|
784
1107
|
result.skipped.push({
|
|
785
1108
|
slug: cluster.slug,
|
|
@@ -789,7 +1112,7 @@ async function generateSkills(req) {
|
|
|
789
1112
|
}
|
|
790
1113
|
let preserved = false;
|
|
791
1114
|
if (req.mode === "active" && existsSync2(targetPath) && !req.force) {
|
|
792
|
-
const existing = await
|
|
1115
|
+
const existing = await readFile4(targetPath, "utf-8");
|
|
793
1116
|
if (!existing.includes("generated by opencode-swarm skill-generator")) {
|
|
794
1117
|
preserved = true;
|
|
795
1118
|
result.skipped.push({
|
|
@@ -814,7 +1137,7 @@ async function generateSkills(req) {
|
|
|
814
1137
|
const existingActivePath = activePath(req.directory, cluster.slug);
|
|
815
1138
|
if (existsSync2(existingActivePath)) {
|
|
816
1139
|
try {
|
|
817
|
-
incumbentContent = await
|
|
1140
|
+
incumbentContent = await readFile4(existingActivePath, "utf-8");
|
|
818
1141
|
} catch {
|
|
819
1142
|
incumbentContent = undefined;
|
|
820
1143
|
}
|
|
@@ -1089,6 +1412,14 @@ async function activateProposal(directory, slug, force = false, options = {}) {
|
|
|
1089
1412
|
}
|
|
1090
1413
|
const from = proposalPath(directory, cleanSlug);
|
|
1091
1414
|
const to = activePath(directory, cleanSlug);
|
|
1415
|
+
if (RESERVED_ACTIVE_SKILL_SLUGS.has(cleanSlug)) {
|
|
1416
|
+
return {
|
|
1417
|
+
activated: false,
|
|
1418
|
+
from,
|
|
1419
|
+
to,
|
|
1420
|
+
reason: reservedActiveSlugReason(cleanSlug)
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1092
1423
|
if (!existsSync2(from)) {
|
|
1093
1424
|
return {
|
|
1094
1425
|
activated: false,
|
|
@@ -1098,7 +1429,7 @@ async function activateProposal(directory, slug, force = false, options = {}) {
|
|
|
1098
1429
|
};
|
|
1099
1430
|
}
|
|
1100
1431
|
if (existsSync2(to) && !force) {
|
|
1101
|
-
const existing = await
|
|
1432
|
+
const existing = await readFile4(to, "utf-8");
|
|
1102
1433
|
if (!existing.includes("generated by opencode-swarm skill-generator")) {
|
|
1103
1434
|
return {
|
|
1104
1435
|
activated: false,
|
|
@@ -1110,7 +1441,7 @@ async function activateProposal(directory, slug, force = false, options = {}) {
|
|
|
1110
1441
|
}
|
|
1111
1442
|
let proposalContent;
|
|
1112
1443
|
try {
|
|
1113
|
-
proposalContent = await
|
|
1444
|
+
proposalContent = await readFile4(from, "utf-8");
|
|
1114
1445
|
} catch (readErr) {
|
|
1115
1446
|
return {
|
|
1116
1447
|
activated: false,
|
|
@@ -1133,7 +1464,7 @@ async function activateProposal(directory, slug, force = false, options = {}) {
|
|
|
1133
1464
|
let incumbentContent;
|
|
1134
1465
|
if (existsSync2(to)) {
|
|
1135
1466
|
try {
|
|
1136
|
-
incumbentContent = await
|
|
1467
|
+
incumbentContent = await readFile4(to, "utf-8");
|
|
1137
1468
|
} catch {
|
|
1138
1469
|
incumbentContent = undefined;
|
|
1139
1470
|
}
|
|
@@ -1208,7 +1539,7 @@ async function activateProposal(directory, slug, force = false, options = {}) {
|
|
|
1208
1539
|
}
|
|
1209
1540
|
}
|
|
1210
1541
|
async function findSkillsBySourceKnowledgeId(directory, sourceId) {
|
|
1211
|
-
const activeDir =
|
|
1542
|
+
const activeDir = path4.join(directory, ".opencode", "skills", "generated");
|
|
1212
1543
|
const fs = await import("fs/promises");
|
|
1213
1544
|
if (!existsSync2(activeDir))
|
|
1214
1545
|
return [];
|
|
@@ -1217,14 +1548,14 @@ async function findSkillsBySourceKnowledgeId(directory, sourceId) {
|
|
|
1217
1548
|
for (const e of entries) {
|
|
1218
1549
|
if (!e.isDirectory())
|
|
1219
1550
|
continue;
|
|
1220
|
-
const skillDir =
|
|
1221
|
-
const retiredMarker =
|
|
1551
|
+
const skillDir = path4.join(activeDir, e.name);
|
|
1552
|
+
const retiredMarker = path4.join(skillDir, "retired.marker");
|
|
1222
1553
|
if (existsSync2(retiredMarker))
|
|
1223
1554
|
continue;
|
|
1224
|
-
const staleMarker =
|
|
1555
|
+
const staleMarker = path4.join(skillDir, "stale.marker");
|
|
1225
1556
|
if (existsSync2(staleMarker))
|
|
1226
1557
|
continue;
|
|
1227
|
-
const skillPath =
|
|
1558
|
+
const skillPath = path4.join(skillDir, "SKILL.md");
|
|
1228
1559
|
if (!existsSync2(skillPath))
|
|
1229
1560
|
continue;
|
|
1230
1561
|
let content;
|
|
@@ -1241,7 +1572,7 @@ async function findSkillsBySourceKnowledgeId(directory, sourceId) {
|
|
|
1241
1572
|
return matches;
|
|
1242
1573
|
}
|
|
1243
1574
|
async function findStaleSkillsBySourceKnowledgeId(directory, archivedIds) {
|
|
1244
|
-
const activeDir =
|
|
1575
|
+
const activeDir = path4.join(directory, ".opencode", "skills", "generated");
|
|
1245
1576
|
if (!existsSync2(activeDir))
|
|
1246
1577
|
return [];
|
|
1247
1578
|
const fs = await import("fs/promises");
|
|
@@ -1250,14 +1581,14 @@ async function findStaleSkillsBySourceKnowledgeId(directory, archivedIds) {
|
|
|
1250
1581
|
for (const e of entries) {
|
|
1251
1582
|
if (!e.isDirectory())
|
|
1252
1583
|
continue;
|
|
1253
|
-
const skillDir =
|
|
1254
|
-
const retiredMarker =
|
|
1584
|
+
const skillDir = path4.join(activeDir, e.name);
|
|
1585
|
+
const retiredMarker = path4.join(skillDir, "retired.marker");
|
|
1255
1586
|
if (existsSync2(retiredMarker))
|
|
1256
1587
|
continue;
|
|
1257
|
-
const staleMarker =
|
|
1588
|
+
const staleMarker = path4.join(skillDir, "stale.marker");
|
|
1258
1589
|
if (!existsSync2(staleMarker))
|
|
1259
1590
|
continue;
|
|
1260
|
-
const skillPath =
|
|
1591
|
+
const skillPath = path4.join(skillDir, "SKILL.md");
|
|
1261
1592
|
if (!existsSync2(skillPath))
|
|
1262
1593
|
continue;
|
|
1263
1594
|
let content;
|
|
@@ -1283,8 +1614,8 @@ async function listSkills(directory) {
|
|
|
1283
1614
|
active: [],
|
|
1284
1615
|
stale: []
|
|
1285
1616
|
};
|
|
1286
|
-
const proposalsDir =
|
|
1287
|
-
const activeDir =
|
|
1617
|
+
const proposalsDir = path4.join(directory, ".swarm", "skills", "proposals");
|
|
1618
|
+
const activeDir = path4.join(directory, ".opencode", "skills", "generated");
|
|
1288
1619
|
const fs = await import("fs/promises");
|
|
1289
1620
|
if (existsSync2(proposalsDir)) {
|
|
1290
1621
|
const entries = await fs.readdir(proposalsDir);
|
|
@@ -1294,7 +1625,7 @@ async function listSkills(directory) {
|
|
|
1294
1625
|
const slug = f.replace(/\.md$/, "");
|
|
1295
1626
|
result.proposals.push({
|
|
1296
1627
|
slug,
|
|
1297
|
-
path:
|
|
1628
|
+
path: path4.join(proposalsDir, f)
|
|
1298
1629
|
});
|
|
1299
1630
|
}
|
|
1300
1631
|
}
|
|
@@ -1303,10 +1634,10 @@ async function listSkills(directory) {
|
|
|
1303
1634
|
for (const e of entries) {
|
|
1304
1635
|
if (!e.isDirectory())
|
|
1305
1636
|
continue;
|
|
1306
|
-
const retiredMarker =
|
|
1637
|
+
const retiredMarker = path4.join(activeDir, e.name, "retired.marker");
|
|
1307
1638
|
if (existsSync2(retiredMarker))
|
|
1308
1639
|
continue;
|
|
1309
|
-
const staleMarker =
|
|
1640
|
+
const staleMarker = path4.join(activeDir, e.name, "stale.marker");
|
|
1310
1641
|
if (existsSync2(staleMarker)) {
|
|
1311
1642
|
let reason = "stale";
|
|
1312
1643
|
try {
|
|
@@ -1316,7 +1647,7 @@ async function listSkills(directory) {
|
|
|
1316
1647
|
result.stale.push({ slug: e.name, reason });
|
|
1317
1648
|
continue;
|
|
1318
1649
|
}
|
|
1319
|
-
const skillPath =
|
|
1650
|
+
const skillPath = path4.join(activeDir, e.name, "SKILL.md");
|
|
1320
1651
|
if (existsSync2(skillPath)) {
|
|
1321
1652
|
result.active.push({
|
|
1322
1653
|
slug: e.name,
|
|
@@ -1341,7 +1672,7 @@ async function autoApplyProposals(directory, llmDelegate) {
|
|
|
1341
1672
|
}
|
|
1342
1673
|
let content;
|
|
1343
1674
|
try {
|
|
1344
|
-
content = await
|
|
1675
|
+
content = await readFile4(proposal.path, "utf-8");
|
|
1345
1676
|
} catch {
|
|
1346
1677
|
result.skipped.push(proposal.slug);
|
|
1347
1678
|
continue;
|
|
@@ -1407,7 +1738,7 @@ async function inspectSkill(directory, slug, prefer = "auto") {
|
|
|
1407
1738
|
candidates.push({ p: proposalPath(directory, cleanSlug), m: "draft" });
|
|
1408
1739
|
for (const c of candidates) {
|
|
1409
1740
|
if (existsSync2(c.p)) {
|
|
1410
|
-
const content = await
|
|
1741
|
+
const content = await readFile4(c.p, "utf-8");
|
|
1411
1742
|
const result = { found: true, path: c.p, content, mode: c.m };
|
|
1412
1743
|
const fm = parseDraftFrontmatter(content);
|
|
1413
1744
|
if (fm && fm.sourceKnowledgeIds.length > 0) {
|
|
@@ -1427,11 +1758,11 @@ async function inspectSkill(directory, slug, prefer = "auto") {
|
|
|
1427
1758
|
});
|
|
1428
1759
|
}
|
|
1429
1760
|
if (c.m === "active") {
|
|
1430
|
-
const skillDir =
|
|
1431
|
-
const staleMarker =
|
|
1761
|
+
const skillDir = path4.join(directory, ".opencode", "skills", "generated", cleanSlug);
|
|
1762
|
+
const staleMarker = path4.join(skillDir, "stale.marker");
|
|
1432
1763
|
if (existsSync2(staleMarker)) {
|
|
1433
1764
|
try {
|
|
1434
|
-
result.stale_reason = await
|
|
1765
|
+
result.stale_reason = await readFile4(staleMarker, "utf-8");
|
|
1435
1766
|
} catch {}
|
|
1436
1767
|
}
|
|
1437
1768
|
}
|
|
@@ -1446,7 +1777,7 @@ async function retireSkill(directory, slug, reason) {
|
|
|
1446
1777
|
return {
|
|
1447
1778
|
retired: false,
|
|
1448
1779
|
path: activePath(directory, cleanSlug),
|
|
1449
|
-
markerPath:
|
|
1780
|
+
markerPath: path4.join(directory, ".opencode", "skills", "generated", cleanSlug, "retired.marker"),
|
|
1450
1781
|
reason: "invalid slug"
|
|
1451
1782
|
};
|
|
1452
1783
|
}
|
|
@@ -1455,18 +1786,18 @@ async function retireSkill(directory, slug, reason) {
|
|
|
1455
1786
|
return {
|
|
1456
1787
|
retired: false,
|
|
1457
1788
|
path: skillPath,
|
|
1458
|
-
markerPath:
|
|
1789
|
+
markerPath: path4.join(directory, ".opencode", "skills", "generated", cleanSlug, "retired.marker"),
|
|
1459
1790
|
reason: "active skill not found"
|
|
1460
1791
|
};
|
|
1461
1792
|
}
|
|
1462
|
-
const markerDir =
|
|
1463
|
-
const markerPath =
|
|
1793
|
+
const markerDir = path4.join(directory, ".opencode", "skills", "generated", cleanSlug);
|
|
1794
|
+
const markerPath = path4.join(markerDir, "retired.marker");
|
|
1464
1795
|
const markerContent = JSON.stringify({
|
|
1465
1796
|
retiredAt: new Date().toISOString(),
|
|
1466
1797
|
reason: reason ?? "manual_retire"
|
|
1467
1798
|
});
|
|
1468
|
-
await
|
|
1469
|
-
await
|
|
1799
|
+
await mkdir4(markerDir, { recursive: true });
|
|
1800
|
+
await writeFile4(markerPath, markerContent, "utf-8");
|
|
1470
1801
|
let clearedLinks = [];
|
|
1471
1802
|
try {
|
|
1472
1803
|
clearedLinks = await clearRetiredSkillLinks(directory, skillPath, cleanSlug);
|
|
@@ -1490,7 +1821,7 @@ async function clearRetiredSkillLinks(directory, skillFilePath, slug) {
|
|
|
1490
1821
|
async function clearSkillLinks(directory, skillFilePath, slug, mode) {
|
|
1491
1822
|
let content;
|
|
1492
1823
|
try {
|
|
1493
|
-
content = await
|
|
1824
|
+
content = await readFile4(skillFilePath, "utf-8");
|
|
1494
1825
|
} catch (err) {
|
|
1495
1826
|
warn(`[skill-generator] clearSkillLinks: failed to read '${skillFilePath}' for slug '${slug}': ${err instanceof Error ? err.message : String(err)}`);
|
|
1496
1827
|
return [];
|
|
@@ -1555,11 +1886,11 @@ async function clearSkillLinks(directory, skillFilePath, slug, mode) {
|
|
|
1555
1886
|
return cleared;
|
|
1556
1887
|
}
|
|
1557
1888
|
async function markSkillStale(skillDir, reason) {
|
|
1558
|
-
await
|
|
1559
|
-
await
|
|
1889
|
+
await mkdir4(skillDir, { recursive: true });
|
|
1890
|
+
await writeFile4(path4.join(skillDir, "stale.marker"), reason, "utf-8");
|
|
1560
1891
|
}
|
|
1561
1892
|
async function clearSkillStale(skillDir) {
|
|
1562
|
-
const markerPath =
|
|
1893
|
+
const markerPath = path4.join(skillDir, "stale.marker");
|
|
1563
1894
|
try {
|
|
1564
1895
|
await unlink(markerPath);
|
|
1565
1896
|
} catch (err) {
|
|
@@ -1571,10 +1902,10 @@ async function clearSkillStale(skillDir) {
|
|
|
1571
1902
|
}
|
|
1572
1903
|
async function retireOrMarkStale(directory, skillDir, archivedKnowledgeIds) {
|
|
1573
1904
|
const fs = await import("fs/promises");
|
|
1574
|
-
const skillPath =
|
|
1905
|
+
const skillPath = path4.join(skillDir, "SKILL.md");
|
|
1575
1906
|
if (!existsSync2(skillPath)) {
|
|
1576
1907
|
await markSkillStale(skillDir, "source knowledge archived, SKILL.md missing");
|
|
1577
|
-
const slug2 =
|
|
1908
|
+
const slug2 = path4.basename(skillDir);
|
|
1578
1909
|
return { action: "stale", slug: slug2, skillDir };
|
|
1579
1910
|
}
|
|
1580
1911
|
let content;
|
|
@@ -1582,18 +1913,18 @@ async function retireOrMarkStale(directory, skillDir, archivedKnowledgeIds) {
|
|
|
1582
1913
|
content = await fs.readFile(skillPath, "utf-8");
|
|
1583
1914
|
} catch {
|
|
1584
1915
|
await markSkillStale(skillDir, "source knowledge archived, SKILL.md unreadable");
|
|
1585
|
-
const slug2 =
|
|
1916
|
+
const slug2 = path4.basename(skillDir);
|
|
1586
1917
|
return { action: "stale", slug: slug2, skillDir };
|
|
1587
1918
|
}
|
|
1588
1919
|
const fm = parseDraftFrontmatter(content);
|
|
1589
1920
|
const sourceIds = fm?.sourceKnowledgeIds ?? [];
|
|
1590
1921
|
if (sourceIds.length === 0) {
|
|
1591
1922
|
await markSkillStale(skillDir, "source knowledge archived, no source_knowledge_ids in frontmatter");
|
|
1592
|
-
const slug2 =
|
|
1923
|
+
const slug2 = path4.basename(skillDir);
|
|
1593
1924
|
return { action: "stale", slug: slug2, skillDir };
|
|
1594
1925
|
}
|
|
1595
1926
|
const allArchived = sourceIds.every((id) => archivedKnowledgeIds.has(id));
|
|
1596
|
-
const slug =
|
|
1927
|
+
const slug = path4.basename(skillDir);
|
|
1597
1928
|
if (allArchived) {
|
|
1598
1929
|
await retireSkill(directory, slug, "all source knowledge entries archived or deleted");
|
|
1599
1930
|
return { action: "retire", slug, skillDir };
|
|
@@ -1623,7 +1954,7 @@ async function regenerateSkill(directory, slug, options = {}) {
|
|
|
1623
1954
|
}
|
|
1624
1955
|
let existingContent;
|
|
1625
1956
|
try {
|
|
1626
|
-
existingContent = await
|
|
1957
|
+
existingContent = await readFile4(skillPath, "utf-8");
|
|
1627
1958
|
} catch (err) {
|
|
1628
1959
|
return {
|
|
1629
1960
|
regenerated: false,
|
|
@@ -1762,7 +2093,7 @@ async function regenerateSkill(directory, slug, options = {}) {
|
|
|
1762
2093
|
try {
|
|
1763
2094
|
await atomicWrite2(skillPath, content);
|
|
1764
2095
|
await stampSourceEntries(directory, cleanSlug, matchedEntries.map((e) => e.id));
|
|
1765
|
-
await clearSkillStale(
|
|
2096
|
+
await clearSkillStale(path4.dirname(activePath(directory, cleanSlug)));
|
|
1766
2097
|
} catch (writeErr) {
|
|
1767
2098
|
return {
|
|
1768
2099
|
regenerated: false,
|
|
@@ -1815,4 +2146,4 @@ var _internals = {
|
|
|
1815
2146
|
clearRetiredSkillLinks
|
|
1816
2147
|
};
|
|
1817
2148
|
|
|
1818
|
-
export { appendSkillChangelog, evaluateSkillChange, appendRejectedSkillEdit, sanitizeSlug, isValidSlug2 as isValidSlug, proposalPath, activePath, activeRepoRelativePath, proposalRepoRelativePath, generateEvalStub, writeEvalStub, DEFAULT_SKILL_MIN_CONFIDENCE, DEFAULT_SKILL_MIN_CONFIRMATIONS, STRONG_SKILL_OUTCOME_COUNT, HIGH_PRIORITY_SKILL_MIN_CONFIDENCE, selectCandidateEntries, isSkillMaturityEligible, clusterEntries, buildKnowledgeCluster, renderSkillMarkdown, generateSkills, parseDraftFrontmatter, activateProposal, findSkillsBySourceKnowledgeId, findStaleSkillsBySourceKnowledgeId, listSkills, autoApplyProposals, inspectSkill, retireSkill, clearDraftSkillLinks, clearRetiredSkillLinks, markSkillStale, clearSkillStale, retireOrMarkStale, regenerateSkill, _internals };
|
|
2149
|
+
export { getDeferredWarnings, BUNDLED_PROJECT_SKILL_ROOT, bundledProjectSkillFileReference, syncBundledProjectSkillsIfMissingAsync, appendSkillChangelog, evaluateSkillChange, appendRejectedSkillEdit, sanitizeSlug, isValidSlug2 as isValidSlug, proposalPath, activePath, activeRepoRelativePath, proposalRepoRelativePath, generateEvalStub, writeEvalStub, DEFAULT_SKILL_MIN_CONFIDENCE, DEFAULT_SKILL_MIN_CONFIRMATIONS, STRONG_SKILL_OUTCOME_COUNT, HIGH_PRIORITY_SKILL_MIN_CONFIDENCE, selectCandidateEntries, isSkillMaturityEligible, clusterEntries, buildKnowledgeCluster, renderSkillMarkdown, generateSkills, parseDraftFrontmatter, activateProposal, findSkillsBySourceKnowledgeId, findStaleSkillsBySourceKnowledgeId, listSkills, autoApplyProposals, inspectSkill, retireSkill, clearDraftSkillLinks, clearRetiredSkillLinks, markSkillStale, clearSkillStale, retireOrMarkStale, regenerateSkill, _internals };
|