opencode-swarm 7.112.2 → 7.113.1
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-5bns4xy6.js → curator-llm-factory-nmhzc774.js} +5 -5
- package/dist/cli/{curator-snvp9542.js → curator-nm2c9y1p.js} +5 -5
- package/dist/cli/{guardrail-explain-4s7djf9p.js → guardrail-explain-q1np1xaw.js} +6 -6
- package/dist/cli/{guardrail-log-xjgr00eq.js → guardrail-log-hysxdwg9.js} +3 -3
- package/dist/cli/{hive-promoter-zs295trk.js → hive-promoter-z4dezw62.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-npxmfj48.js → index-8f256d7t.js} +1 -1
- package/dist/cli/{index-82v0d72f.js → index-h389ed8y.js} +424 -87
- package/dist/cli/{index-9fjpecax.js → index-ky8eb4q6.js} +2 -2
- package/dist/cli/{index-snmfqhez.js → index-npd9rypp.js} +2009 -958
- package/dist/cli/{index-w84stmkx.js → index-r9fjzr3k.js} +1 -1
- package/dist/cli/{index-k6wd3chp.js → index-rqpm9cwe.js} +7 -7
- 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-nnwn4sq0.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/project-init.d.ts +1 -1
- 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 +88 -88
- package/dist/sast/rules/index.d.ts +0 -9
- package/dist/utils/gitignore-warning.d.ts +8 -4
- 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,351 @@ 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
|
+
var deferredWarningsTruncated = false;
|
|
44
|
+
function addDeferredWarning(warning) {
|
|
45
|
+
if (deferredWarnings.length < MAX_DEFERRED_WARNINGS - 1) {
|
|
46
|
+
deferredWarnings.push(warning);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!deferredWarningsTruncated) {
|
|
50
|
+
deferredWarnings.push(`[opencode-swarm] ${MAX_DEFERRED_WARNINGS - 1} deferred warnings buffered; additional advisories were dropped (cap reached). Run with OPENCODE_SWARM_DEBUG=1 to see all advisories in the debug log.`);
|
|
51
|
+
deferredWarningsTruncated = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function advisoryWarn(message, data) {
|
|
55
|
+
addDeferredWarning(message);
|
|
56
|
+
log(message, data);
|
|
57
|
+
}
|
|
58
|
+
function getDeferredWarnings() {
|
|
59
|
+
return [...deferredWarnings];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/config/bundled-skills.ts
|
|
63
|
+
init_logger();
|
|
64
|
+
var BUNDLED_PROJECT_SKILLS = [
|
|
65
|
+
"brainstorm",
|
|
66
|
+
"specify",
|
|
67
|
+
"clarify-spec",
|
|
68
|
+
"resume",
|
|
69
|
+
"clarify",
|
|
70
|
+
"discover",
|
|
71
|
+
"consult",
|
|
72
|
+
"pre-phase-briefing",
|
|
73
|
+
"council",
|
|
74
|
+
"deep-dive",
|
|
75
|
+
"deep-research",
|
|
76
|
+
"codebase-review-swarm",
|
|
77
|
+
"swarm-implement",
|
|
78
|
+
"design-docs",
|
|
79
|
+
"swarm-pr-review",
|
|
80
|
+
"swarm",
|
|
81
|
+
"swarm-pr-feedback",
|
|
82
|
+
"swarm-pr-subscribe",
|
|
83
|
+
"swarm-ci-monitor",
|
|
84
|
+
"issue-ingest",
|
|
85
|
+
"plan",
|
|
86
|
+
"critic-gate",
|
|
87
|
+
"execute",
|
|
88
|
+
"phase-wrap",
|
|
89
|
+
"loop",
|
|
90
|
+
"writing-tests",
|
|
91
|
+
"running-tests",
|
|
92
|
+
"engineering-conventions",
|
|
93
|
+
"commit-pr",
|
|
94
|
+
"ci-failure-batching",
|
|
95
|
+
"gate-attribution",
|
|
96
|
+
"merge-queue-readiness",
|
|
97
|
+
"skill-edit-validation",
|
|
98
|
+
"worktree-retry-cleanup",
|
|
99
|
+
"test-file-split",
|
|
100
|
+
"fork-pr-operations",
|
|
101
|
+
"parallel-work-check",
|
|
102
|
+
"ci-fix-monitor"
|
|
103
|
+
];
|
|
104
|
+
var BUNDLED_PROJECT_SKILL_ROOT = ".swarm/bundled-skills";
|
|
105
|
+
function bundledProjectSkillFileReference(slug) {
|
|
106
|
+
return `file:${BUNDLED_PROJECT_SKILL_ROOT}/${slug}/SKILL.md`;
|
|
107
|
+
}
|
|
108
|
+
var MAX_SKILL_FILES = 64;
|
|
109
|
+
var MAX_SKILL_BYTES = 512000;
|
|
110
|
+
var MAX_IN_FLIGHT_SYNCS = 64;
|
|
111
|
+
var inFlightSyncs = new Map;
|
|
112
|
+
function describeBundledSkillSyncFailure(err) {
|
|
113
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
114
|
+
return `[opencode-swarm] Could not install bundled project skills; continuing without sync: ${message}`;
|
|
115
|
+
}
|
|
116
|
+
async function isSymbolicLinkAsync(p) {
|
|
117
|
+
try {
|
|
118
|
+
return (await fsp.lstat(p)).isSymbolicLink();
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function ensureNotSymlinkedDirectoryAsync(p) {
|
|
124
|
+
try {
|
|
125
|
+
const stat2 = await fsp.lstat(p);
|
|
126
|
+
return stat2.isDirectory() && !stat2.isSymbolicLink();
|
|
127
|
+
} catch (err) {
|
|
128
|
+
return err.code === "ENOENT";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function pathExistsAsync(p) {
|
|
132
|
+
try {
|
|
133
|
+
await fsp.access(p);
|
|
134
|
+
return true;
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function collectBundledSkillFilesBoundedAsync(sourceDir, state, relativeDir = "") {
|
|
140
|
+
const currentSource = path.join(sourceDir, relativeDir);
|
|
141
|
+
const entries = await fsp.readdir(currentSource, { withFileTypes: true });
|
|
142
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
143
|
+
const files = [];
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
const relativeEntry = path.join(relativeDir, entry.name);
|
|
146
|
+
const sourcePath = path.join(sourceDir, relativeEntry);
|
|
147
|
+
if (entry.isSymbolicLink() || await isSymbolicLinkAsync(sourcePath)) {
|
|
148
|
+
throw new Error("refusing to copy symlinked bundled skill entry");
|
|
149
|
+
}
|
|
150
|
+
if (entry.isDirectory()) {
|
|
151
|
+
files.push(...await collectBundledSkillFilesBoundedAsync(sourceDir, state, relativeEntry));
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!entry.isFile())
|
|
155
|
+
continue;
|
|
156
|
+
const stat2 = await fsp.stat(sourcePath);
|
|
157
|
+
const nextFiles = state.files + 1;
|
|
158
|
+
const nextBytes = state.bytes + stat2.size;
|
|
159
|
+
if (nextFiles > MAX_SKILL_FILES || nextBytes > MAX_SKILL_BYTES) {
|
|
160
|
+
throw new Error("bundled skill package exceeds copy bounds");
|
|
161
|
+
}
|
|
162
|
+
state.files = nextFiles;
|
|
163
|
+
state.bytes = nextBytes;
|
|
164
|
+
files.push({ relativePath: relativeEntry });
|
|
165
|
+
}
|
|
166
|
+
return files;
|
|
167
|
+
}
|
|
168
|
+
async function ensureContainedDirectoryAsync(rootDir, directory) {
|
|
169
|
+
const safeRoot = path.resolve(rootDir);
|
|
170
|
+
const safeDirectory = path.resolve(directory);
|
|
171
|
+
const relative2 = path.relative(safeRoot, safeDirectory);
|
|
172
|
+
if (relative2.startsWith("..") || path.isAbsolute(relative2)) {
|
|
173
|
+
throw new Error("bundled skill destination escaped its private root");
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const rootStat = await fsp.lstat(safeRoot);
|
|
177
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
178
|
+
throw new Error("refusing to traverse unsafe bundled skill directory");
|
|
179
|
+
}
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (err.code !== "ENOENT")
|
|
182
|
+
throw err;
|
|
183
|
+
await fsp.mkdir(safeRoot, { recursive: true });
|
|
184
|
+
const rootStat = await fsp.lstat(safeRoot);
|
|
185
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
186
|
+
throw new Error("refusing to traverse unsafe bundled skill directory");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
let current = safeRoot;
|
|
190
|
+
for (const segment of relative2.split(path.sep).filter(Boolean)) {
|
|
191
|
+
current = path.join(current, segment);
|
|
192
|
+
try {
|
|
193
|
+
const stat2 = await fsp.lstat(current);
|
|
194
|
+
if (stat2.isSymbolicLink() || !stat2.isDirectory()) {
|
|
195
|
+
throw new Error("refusing to traverse unsafe bundled skill directory");
|
|
196
|
+
}
|
|
197
|
+
} catch (err) {
|
|
198
|
+
if (err.code !== "ENOENT")
|
|
199
|
+
throw err;
|
|
200
|
+
try {
|
|
201
|
+
await fsp.mkdir(current);
|
|
202
|
+
} catch (mkdirErr) {
|
|
203
|
+
if (mkdirErr.code !== "EEXIST") {
|
|
204
|
+
throw mkdirErr;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const stat2 = await fsp.lstat(current);
|
|
208
|
+
if (stat2.isSymbolicLink() || !stat2.isDirectory()) {
|
|
209
|
+
throw new Error("refusing to traverse unsafe bundled skill directory");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function rollbackCopiedFilesAsync(copiedFiles, destDir, overwrittenFiles = []) {
|
|
215
|
+
const safeDestDir = path.resolve(destDir);
|
|
216
|
+
const dirs = new Set;
|
|
217
|
+
for (const copiedFile of copiedFiles) {
|
|
218
|
+
const resolvedFile = path.resolve(copiedFile);
|
|
219
|
+
const relative2 = path.relative(safeDestDir, resolvedFile);
|
|
220
|
+
if (relative2.startsWith("..") || path.isAbsolute(relative2))
|
|
221
|
+
continue;
|
|
222
|
+
try {
|
|
223
|
+
await fsp.rm(resolvedFile, { force: true });
|
|
224
|
+
} catch {}
|
|
225
|
+
dirs.add(path.dirname(resolvedFile));
|
|
226
|
+
}
|
|
227
|
+
for (const overwritten of overwrittenFiles.reverse()) {
|
|
228
|
+
const resolvedFile = path.resolve(overwritten.path);
|
|
229
|
+
const relative2 = path.relative(safeDestDir, resolvedFile);
|
|
230
|
+
if (relative2.startsWith("..") || path.isAbsolute(relative2))
|
|
231
|
+
continue;
|
|
232
|
+
try {
|
|
233
|
+
await fsp.writeFile(resolvedFile, overwritten.contents);
|
|
234
|
+
} catch {}
|
|
235
|
+
}
|
|
236
|
+
for (const dir of [...dirs].sort((a, b) => b.length - a.length)) {
|
|
237
|
+
const relative2 = path.relative(safeDestDir, path.resolve(dir));
|
|
238
|
+
if (relative2.startsWith("..") || path.isAbsolute(relative2))
|
|
239
|
+
continue;
|
|
240
|
+
try {
|
|
241
|
+
await fsp.rmdir(dir);
|
|
242
|
+
} catch {}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async function copyFileAtomicAsync(sourcePath, destPath) {
|
|
246
|
+
const tempPath = path.join(path.dirname(destPath), `.${path.basename(destPath)}.tmp.${randomUUID()}`);
|
|
247
|
+
try {
|
|
248
|
+
await fsp.copyFile(sourcePath, tempPath);
|
|
249
|
+
await fsp.rename(tempPath, destPath);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
try {
|
|
252
|
+
await fsp.rm(tempPath, { force: true });
|
|
253
|
+
} catch {}
|
|
254
|
+
throw err;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function copyBundledDirectoryBoundedAsync(sourceDir, destDir) {
|
|
258
|
+
const files = await collectBundledSkillFilesBoundedAsync(sourceDir, {
|
|
259
|
+
files: 0,
|
|
260
|
+
bytes: 0
|
|
261
|
+
});
|
|
262
|
+
const copiedFiles = [];
|
|
263
|
+
const overwrittenFiles = [];
|
|
264
|
+
try {
|
|
265
|
+
for (const file of files) {
|
|
266
|
+
const sourcePath = path.join(sourceDir, file.relativePath);
|
|
267
|
+
const destPath = path.join(destDir, file.relativePath);
|
|
268
|
+
await ensureContainedDirectoryAsync(destDir, path.dirname(destPath));
|
|
269
|
+
if (await pathExistsAsync(destPath)) {
|
|
270
|
+
if (await isSymbolicLinkAsync(destPath)) {
|
|
271
|
+
throw new Error("refusing to overwrite symlinked bundled skill file");
|
|
272
|
+
}
|
|
273
|
+
const [sourceContents, destContents] = await Promise.all([
|
|
274
|
+
fsp.readFile(sourcePath),
|
|
275
|
+
fsp.readFile(destPath)
|
|
276
|
+
]);
|
|
277
|
+
if (sourceContents.equals(destContents)) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
overwrittenFiles.push({ path: destPath, contents: destContents });
|
|
281
|
+
await copyFileAtomicAsync(sourcePath, destPath);
|
|
282
|
+
} else {
|
|
283
|
+
await copyFileAtomicAsync(sourcePath, destPath);
|
|
284
|
+
copiedFiles.push(destPath);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch (err) {
|
|
288
|
+
await rollbackCopiedFilesAsync(copiedFiles, destDir, overwrittenFiles);
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function performBundledProjectSkillSyncAsync(projectDirectory, packageRoot, quiet) {
|
|
293
|
+
try {
|
|
294
|
+
const sourceRoot = path.join(packageRoot, ".opencode", "skills");
|
|
295
|
+
const swarmDir = path.join(projectDirectory, ".swarm");
|
|
296
|
+
const skillsDir = path.join(projectDirectory, BUNDLED_PROJECT_SKILL_ROOT);
|
|
297
|
+
if (!await ensureNotSymlinkedDirectoryAsync(sourceRoot)) {
|
|
298
|
+
throw new Error("refusing to copy symlinked bundled skill source root");
|
|
299
|
+
}
|
|
300
|
+
if (!await ensureNotSymlinkedDirectoryAsync(swarmDir))
|
|
301
|
+
return;
|
|
302
|
+
if (!await ensureNotSymlinkedDirectoryAsync(skillsDir))
|
|
303
|
+
return;
|
|
304
|
+
for (const slug of BUNDLED_PROJECT_SKILLS) {
|
|
305
|
+
const sourceDir = path.join(sourceRoot, slug);
|
|
306
|
+
const sourceSkill = path.join(sourceDir, "SKILL.md");
|
|
307
|
+
const destDir = path.join(skillsDir, slug);
|
|
308
|
+
if (!await pathExistsAsync(sourceSkill))
|
|
309
|
+
continue;
|
|
310
|
+
if (!await ensureNotSymlinkedDirectoryAsync(sourceDir)) {
|
|
311
|
+
throw new Error("refusing to copy symlinked bundled skill directory");
|
|
312
|
+
}
|
|
313
|
+
if (!await ensureNotSymlinkedDirectoryAsync(destDir))
|
|
314
|
+
continue;
|
|
315
|
+
await copyBundledDirectoryBoundedAsync(sourceDir, destDir);
|
|
316
|
+
log("synchronized bundled skill", {
|
|
317
|
+
slug: bundledProjectSkillFileReference(slug)
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
} catch (err) {
|
|
321
|
+
const failureMsg = describeBundledSkillSyncFailure(err);
|
|
322
|
+
if (quiet) {
|
|
323
|
+
advisoryWarn(failureMsg);
|
|
324
|
+
} else {
|
|
325
|
+
console.warn(failureMsg);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async function syncBundledProjectSkillsIfMissingAsync(projectDirectory, packageRoot, quiet = false) {
|
|
330
|
+
const syncKey = path.resolve(projectDirectory, BUNDLED_PROJECT_SKILL_ROOT);
|
|
331
|
+
const existing = inFlightSyncs.get(syncKey);
|
|
332
|
+
if (existing)
|
|
333
|
+
return existing;
|
|
334
|
+
if (inFlightSyncs.size >= MAX_IN_FLIGHT_SYNCS) {
|
|
335
|
+
log("skipping bundled skill sync because the in-flight limit was reached", {
|
|
336
|
+
limit: MAX_IN_FLIGHT_SYNCS
|
|
337
|
+
});
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const syncPromise = performBundledProjectSkillSyncAsync(projectDirectory, packageRoot, quiet).finally(() => {
|
|
341
|
+
if (inFlightSyncs.get(syncKey) === syncPromise) {
|
|
342
|
+
inFlightSyncs.delete(syncKey);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
inFlightSyncs.set(syncKey, syncPromise);
|
|
346
|
+
return syncPromise;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/services/skill-generator.ts
|
|
32
350
|
init_logger();
|
|
33
351
|
|
|
34
352
|
// src/services/skill-changelog.ts
|
|
35
353
|
init_logger();
|
|
36
|
-
import { appendFile, mkdir, readFile, writeFile } from "fs/promises";
|
|
37
|
-
import * as
|
|
354
|
+
import { appendFile, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
355
|
+
import * as path2 from "path";
|
|
38
356
|
var MAX_CHANGELOG_ENTRIES_PER_SKILL = 200;
|
|
39
357
|
function resolveSkillChangelogPath(directory, slug) {
|
|
40
358
|
if (slug.includes("..") || slug.includes("/") || slug.includes("\\")) {
|
|
41
359
|
throw new Error(`Invalid skill slug: ${slug} \u2014 must not contain "..", "/" or "\\"`);
|
|
42
360
|
}
|
|
43
|
-
return
|
|
361
|
+
return path2.join(directory, ".swarm", "skill-changelogs", `${slug}.jsonl`);
|
|
44
362
|
}
|
|
45
363
|
async function appendSkillChangelog(directory, slug, entry) {
|
|
46
364
|
const filePath = resolveSkillChangelogPath(directory, slug);
|
|
47
|
-
const dirPath =
|
|
48
|
-
await
|
|
365
|
+
const dirPath = path2.dirname(filePath);
|
|
366
|
+
await mkdir2(dirPath, { recursive: true });
|
|
49
367
|
await appendFile(filePath, `${JSON.stringify(entry)}
|
|
50
368
|
`, "utf-8");
|
|
51
369
|
try {
|
|
52
|
-
const content = await
|
|
370
|
+
const content = await readFile2(filePath, "utf-8");
|
|
53
371
|
const lines = content.split(`
|
|
54
372
|
`).filter((line) => line.trim().length > 0);
|
|
55
373
|
if (lines.length > MAX_CHANGELOG_ENTRIES_PER_SKILL) {
|
|
56
374
|
const trimmed = lines.slice(lines.length - MAX_CHANGELOG_ENTRIES_PER_SKILL);
|
|
57
|
-
await
|
|
375
|
+
await writeFile2(filePath, `${trimmed.join(`
|
|
58
376
|
`)}
|
|
59
377
|
`, "utf-8");
|
|
60
378
|
}
|
|
@@ -67,16 +385,16 @@ async function appendSkillChangelog(directory, slug, entry) {
|
|
|
67
385
|
import { createHash } from "crypto";
|
|
68
386
|
import { existsSync } from "fs";
|
|
69
387
|
import {
|
|
70
|
-
lstat,
|
|
71
|
-
mkdir as
|
|
72
|
-
readdir,
|
|
73
|
-
readFile as
|
|
388
|
+
lstat as lstat2,
|
|
389
|
+
mkdir as mkdir3,
|
|
390
|
+
readdir as readdir2,
|
|
391
|
+
readFile as readFile3,
|
|
74
392
|
realpath,
|
|
75
|
-
rename,
|
|
76
|
-
stat,
|
|
77
|
-
writeFile as
|
|
393
|
+
rename as rename2,
|
|
394
|
+
stat as stat2,
|
|
395
|
+
writeFile as writeFile3
|
|
78
396
|
} from "fs/promises";
|
|
79
|
-
import * as
|
|
397
|
+
import * as path3 from "path";
|
|
80
398
|
var SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
81
399
|
var MAX_EVAL_FILES = 50;
|
|
82
400
|
var MAX_EVAL_FILE_BYTES = 64 * 1024;
|
|
@@ -142,15 +460,15 @@ function casesFromParsedJson(parsed, fileLabel) {
|
|
|
142
460
|
return cases;
|
|
143
461
|
}
|
|
144
462
|
function evalRoot(directory, slug) {
|
|
145
|
-
return
|
|
463
|
+
return path3.join(directory, ".swarm", "skills", "evals", slug);
|
|
146
464
|
}
|
|
147
465
|
function isInsidePath(root, target) {
|
|
148
|
-
const resolvedRoot =
|
|
149
|
-
const resolvedTarget =
|
|
150
|
-
return resolvedTarget === resolvedRoot || resolvedTarget.startsWith(resolvedRoot +
|
|
466
|
+
const resolvedRoot = path3.resolve(root);
|
|
467
|
+
const resolvedTarget = path3.resolve(target);
|
|
468
|
+
return resolvedTarget === resolvedRoot || resolvedTarget.startsWith(resolvedRoot + path3.sep);
|
|
151
469
|
}
|
|
152
470
|
function rejectedEditsPath(directory) {
|
|
153
|
-
return
|
|
471
|
+
return path3.join(directory, ".swarm", "skills", "rejected-edits.jsonl");
|
|
154
472
|
}
|
|
155
473
|
async function loadEvalSet(directory, slug) {
|
|
156
474
|
if (!isValidSlug(slug)) {
|
|
@@ -187,7 +505,7 @@ async function loadEvalSet(directory, slug) {
|
|
|
187
505
|
}
|
|
188
506
|
let entries;
|
|
189
507
|
try {
|
|
190
|
-
entries = await
|
|
508
|
+
entries = await readdir2(root);
|
|
191
509
|
} catch (err) {
|
|
192
510
|
return {
|
|
193
511
|
status: "invalid",
|
|
@@ -203,9 +521,9 @@ async function loadEvalSet(directory, slug) {
|
|
|
203
521
|
const cases = [];
|
|
204
522
|
const fileLabels = [];
|
|
205
523
|
for (const file of files) {
|
|
206
|
-
const fullPath =
|
|
207
|
-
const resolved =
|
|
208
|
-
const resolvedRoot =
|
|
524
|
+
const fullPath = path3.join(root, file);
|
|
525
|
+
const resolved = path3.resolve(fullPath);
|
|
526
|
+
const resolvedRoot = path3.resolve(root);
|
|
209
527
|
if (!isInsidePath(resolvedRoot, resolved)) {
|
|
210
528
|
return {
|
|
211
529
|
status: "invalid",
|
|
@@ -217,7 +535,7 @@ async function loadEvalSet(directory, slug) {
|
|
|
217
535
|
let readPath = fullPath;
|
|
218
536
|
let info;
|
|
219
537
|
try {
|
|
220
|
-
const linkInfo = await
|
|
538
|
+
const linkInfo = await lstat2(fullPath);
|
|
221
539
|
if (linkInfo.isSymbolicLink()) {
|
|
222
540
|
const realFile = await realpath(fullPath);
|
|
223
541
|
if (!isInsidePath(realRoot, realFile)) {
|
|
@@ -229,7 +547,7 @@ async function loadEvalSet(directory, slug) {
|
|
|
229
547
|
};
|
|
230
548
|
}
|
|
231
549
|
readPath = realFile;
|
|
232
|
-
info = await
|
|
550
|
+
info = await stat2(realFile);
|
|
233
551
|
} else {
|
|
234
552
|
if (!linkInfo.isFile())
|
|
235
553
|
continue;
|
|
@@ -255,7 +573,7 @@ async function loadEvalSet(directory, slug) {
|
|
|
255
573
|
}
|
|
256
574
|
let parsed;
|
|
257
575
|
try {
|
|
258
|
-
parsed = JSON.parse(await
|
|
576
|
+
parsed = JSON.parse(await readFile3(readPath, "utf-8"));
|
|
259
577
|
} catch (err) {
|
|
260
578
|
return {
|
|
261
579
|
status: "invalid",
|
|
@@ -264,7 +582,7 @@ async function loadEvalSet(directory, slug) {
|
|
|
264
582
|
reason: `eval file invalid JSON: ${file}: ${err instanceof Error ? err.message : String(err)}`
|
|
265
583
|
};
|
|
266
584
|
}
|
|
267
|
-
fileLabels.push(
|
|
585
|
+
fileLabels.push(path3.relative(directory, fullPath).replace(/\\/g, "/"));
|
|
268
586
|
cases.push(...casesFromParsedJson(parsed, file));
|
|
269
587
|
if (cases.length >= MAX_EVAL_CASES)
|
|
270
588
|
break;
|
|
@@ -382,10 +700,10 @@ async function evaluateSkillChange(req) {
|
|
|
382
700
|
};
|
|
383
701
|
}
|
|
384
702
|
async function atomicWrite(filePath, content) {
|
|
385
|
-
await
|
|
703
|
+
await mkdir3(path3.dirname(filePath), { recursive: true });
|
|
386
704
|
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
387
|
-
await
|
|
388
|
-
await
|
|
705
|
+
await writeFile3(tmp, content, "utf-8");
|
|
706
|
+
await rename2(tmp, filePath);
|
|
389
707
|
}
|
|
390
708
|
async function appendRejectedSkillEdit(req, evaluation) {
|
|
391
709
|
const filePath = rejectedEditsPath(req.directory);
|
|
@@ -405,7 +723,7 @@ async function appendRejectedSkillEdit(req, evaluation) {
|
|
|
405
723
|
};
|
|
406
724
|
let prior = [];
|
|
407
725
|
try {
|
|
408
|
-
prior = (await
|
|
726
|
+
prior = (await readFile3(filePath, "utf-8")).split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
409
727
|
} catch {
|
|
410
728
|
prior = [];
|
|
411
729
|
}
|
|
@@ -422,7 +740,7 @@ async function isRejectedSkillContent(directory, slug, content) {
|
|
|
422
740
|
const normalizedHash = hashNormalizedContent(content);
|
|
423
741
|
let raw = "";
|
|
424
742
|
try {
|
|
425
|
-
raw = await
|
|
743
|
+
raw = await readFile3(rejectedEditsPath(directory), "utf-8");
|
|
426
744
|
} catch {
|
|
427
745
|
return false;
|
|
428
746
|
}
|
|
@@ -449,6 +767,10 @@ async function isRejectedSkillContent(directory, slug, content) {
|
|
|
449
767
|
|
|
450
768
|
// src/services/skill-generator.ts
|
|
451
769
|
var SLUG_PATTERN2 = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
770
|
+
var RESERVED_ACTIVE_SKILL_SLUGS = new Set(BUNDLED_PROJECT_SKILLS);
|
|
771
|
+
function reservedActiveSlugReason(slug) {
|
|
772
|
+
return `reserved bundled skill slug "${slug}" cannot be used for an active generated skill`;
|
|
773
|
+
}
|
|
452
774
|
function sanitizeSlug(input) {
|
|
453
775
|
const lc = input.toLowerCase().trim();
|
|
454
776
|
const mapped = lc.replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-");
|
|
@@ -459,10 +781,10 @@ function isValidSlug2(slug) {
|
|
|
459
781
|
return SLUG_PATTERN2.test(slug);
|
|
460
782
|
}
|
|
461
783
|
function proposalPath(directory, slug) {
|
|
462
|
-
return
|
|
784
|
+
return path4.join(directory, ".swarm", "skills", "proposals", `${slug}.md`);
|
|
463
785
|
}
|
|
464
786
|
function activePath(directory, slug) {
|
|
465
|
-
return
|
|
787
|
+
return path4.join(directory, ".opencode", "skills", "generated", slug, "SKILL.md");
|
|
466
788
|
}
|
|
467
789
|
function activeRepoRelativePath(slug) {
|
|
468
790
|
return `.opencode/skills/generated/${slug}/SKILL.md`;
|
|
@@ -486,12 +808,12 @@ function generateEvalStub(cluster) {
|
|
|
486
808
|
];
|
|
487
809
|
}
|
|
488
810
|
async function writeEvalStub(directory, slug, cases) {
|
|
489
|
-
const stubPath =
|
|
811
|
+
const stubPath = path4.join(directory, ".swarm", "skills", "evals", slug, "auto-stub.json");
|
|
490
812
|
if (cases.length === 0) {
|
|
491
813
|
return { written: false, path: stubPath, reason: "no directive fields" };
|
|
492
814
|
}
|
|
493
815
|
try {
|
|
494
|
-
await
|
|
816
|
+
await mkdir4(path4.dirname(stubPath), { recursive: true });
|
|
495
817
|
await atomicWrite2(stubPath, `${JSON.stringify({ cases })}
|
|
496
818
|
`);
|
|
497
819
|
return { written: true, path: stubPath };
|
|
@@ -726,10 +1048,10 @@ function escapeMarkdown(s) {
|
|
|
726
1048
|
return s.replace(/[\r\n]+/g, " ").slice(0, 280);
|
|
727
1049
|
}
|
|
728
1050
|
async function atomicWrite2(p, content) {
|
|
729
|
-
await
|
|
1051
|
+
await mkdir4(path4.dirname(p), { recursive: true });
|
|
730
1052
|
const tmp = `${p}.tmp-${process.pid}-${Date.now()}`;
|
|
731
|
-
await
|
|
732
|
-
await
|
|
1053
|
+
await writeFile4(tmp, content, "utf-8");
|
|
1054
|
+
await rename3(tmp, p);
|
|
733
1055
|
}
|
|
734
1056
|
async function generateSkills(req) {
|
|
735
1057
|
const minConfidence = req.minConfidence ?? DEFAULT_SKILL_MIN_CONFIDENCE;
|
|
@@ -778,8 +1100,15 @@ async function generateSkills(req) {
|
|
|
778
1100
|
});
|
|
779
1101
|
continue;
|
|
780
1102
|
}
|
|
1103
|
+
if (req.mode === "active" && RESERVED_ACTIVE_SKILL_SLUGS.has(cluster.slug)) {
|
|
1104
|
+
result.skipped.push({
|
|
1105
|
+
slug: cluster.slug,
|
|
1106
|
+
reason: reservedActiveSlugReason(cluster.slug)
|
|
1107
|
+
});
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
781
1110
|
const targetPath = req.mode === "active" ? activePath(req.directory, cluster.slug) : proposalPath(req.directory, cluster.slug);
|
|
782
|
-
const repoRel =
|
|
1111
|
+
const repoRel = path4.relative(req.directory, targetPath).replace(/\\/g, "/");
|
|
783
1112
|
if (!validateSkillPath(repoRel)) {
|
|
784
1113
|
result.skipped.push({
|
|
785
1114
|
slug: cluster.slug,
|
|
@@ -789,7 +1118,7 @@ async function generateSkills(req) {
|
|
|
789
1118
|
}
|
|
790
1119
|
let preserved = false;
|
|
791
1120
|
if (req.mode === "active" && existsSync2(targetPath) && !req.force) {
|
|
792
|
-
const existing = await
|
|
1121
|
+
const existing = await readFile4(targetPath, "utf-8");
|
|
793
1122
|
if (!existing.includes("generated by opencode-swarm skill-generator")) {
|
|
794
1123
|
preserved = true;
|
|
795
1124
|
result.skipped.push({
|
|
@@ -814,7 +1143,7 @@ async function generateSkills(req) {
|
|
|
814
1143
|
const existingActivePath = activePath(req.directory, cluster.slug);
|
|
815
1144
|
if (existsSync2(existingActivePath)) {
|
|
816
1145
|
try {
|
|
817
|
-
incumbentContent = await
|
|
1146
|
+
incumbentContent = await readFile4(existingActivePath, "utf-8");
|
|
818
1147
|
} catch {
|
|
819
1148
|
incumbentContent = undefined;
|
|
820
1149
|
}
|
|
@@ -1089,6 +1418,14 @@ async function activateProposal(directory, slug, force = false, options = {}) {
|
|
|
1089
1418
|
}
|
|
1090
1419
|
const from = proposalPath(directory, cleanSlug);
|
|
1091
1420
|
const to = activePath(directory, cleanSlug);
|
|
1421
|
+
if (RESERVED_ACTIVE_SKILL_SLUGS.has(cleanSlug)) {
|
|
1422
|
+
return {
|
|
1423
|
+
activated: false,
|
|
1424
|
+
from,
|
|
1425
|
+
to,
|
|
1426
|
+
reason: reservedActiveSlugReason(cleanSlug)
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1092
1429
|
if (!existsSync2(from)) {
|
|
1093
1430
|
return {
|
|
1094
1431
|
activated: false,
|
|
@@ -1098,7 +1435,7 @@ async function activateProposal(directory, slug, force = false, options = {}) {
|
|
|
1098
1435
|
};
|
|
1099
1436
|
}
|
|
1100
1437
|
if (existsSync2(to) && !force) {
|
|
1101
|
-
const existing = await
|
|
1438
|
+
const existing = await readFile4(to, "utf-8");
|
|
1102
1439
|
if (!existing.includes("generated by opencode-swarm skill-generator")) {
|
|
1103
1440
|
return {
|
|
1104
1441
|
activated: false,
|
|
@@ -1110,7 +1447,7 @@ async function activateProposal(directory, slug, force = false, options = {}) {
|
|
|
1110
1447
|
}
|
|
1111
1448
|
let proposalContent;
|
|
1112
1449
|
try {
|
|
1113
|
-
proposalContent = await
|
|
1450
|
+
proposalContent = await readFile4(from, "utf-8");
|
|
1114
1451
|
} catch (readErr) {
|
|
1115
1452
|
return {
|
|
1116
1453
|
activated: false,
|
|
@@ -1133,7 +1470,7 @@ async function activateProposal(directory, slug, force = false, options = {}) {
|
|
|
1133
1470
|
let incumbentContent;
|
|
1134
1471
|
if (existsSync2(to)) {
|
|
1135
1472
|
try {
|
|
1136
|
-
incumbentContent = await
|
|
1473
|
+
incumbentContent = await readFile4(to, "utf-8");
|
|
1137
1474
|
} catch {
|
|
1138
1475
|
incumbentContent = undefined;
|
|
1139
1476
|
}
|
|
@@ -1208,7 +1545,7 @@ async function activateProposal(directory, slug, force = false, options = {}) {
|
|
|
1208
1545
|
}
|
|
1209
1546
|
}
|
|
1210
1547
|
async function findSkillsBySourceKnowledgeId(directory, sourceId) {
|
|
1211
|
-
const activeDir =
|
|
1548
|
+
const activeDir = path4.join(directory, ".opencode", "skills", "generated");
|
|
1212
1549
|
const fs = await import("fs/promises");
|
|
1213
1550
|
if (!existsSync2(activeDir))
|
|
1214
1551
|
return [];
|
|
@@ -1217,14 +1554,14 @@ async function findSkillsBySourceKnowledgeId(directory, sourceId) {
|
|
|
1217
1554
|
for (const e of entries) {
|
|
1218
1555
|
if (!e.isDirectory())
|
|
1219
1556
|
continue;
|
|
1220
|
-
const skillDir =
|
|
1221
|
-
const retiredMarker =
|
|
1557
|
+
const skillDir = path4.join(activeDir, e.name);
|
|
1558
|
+
const retiredMarker = path4.join(skillDir, "retired.marker");
|
|
1222
1559
|
if (existsSync2(retiredMarker))
|
|
1223
1560
|
continue;
|
|
1224
|
-
const staleMarker =
|
|
1561
|
+
const staleMarker = path4.join(skillDir, "stale.marker");
|
|
1225
1562
|
if (existsSync2(staleMarker))
|
|
1226
1563
|
continue;
|
|
1227
|
-
const skillPath =
|
|
1564
|
+
const skillPath = path4.join(skillDir, "SKILL.md");
|
|
1228
1565
|
if (!existsSync2(skillPath))
|
|
1229
1566
|
continue;
|
|
1230
1567
|
let content;
|
|
@@ -1241,7 +1578,7 @@ async function findSkillsBySourceKnowledgeId(directory, sourceId) {
|
|
|
1241
1578
|
return matches;
|
|
1242
1579
|
}
|
|
1243
1580
|
async function findStaleSkillsBySourceKnowledgeId(directory, archivedIds) {
|
|
1244
|
-
const activeDir =
|
|
1581
|
+
const activeDir = path4.join(directory, ".opencode", "skills", "generated");
|
|
1245
1582
|
if (!existsSync2(activeDir))
|
|
1246
1583
|
return [];
|
|
1247
1584
|
const fs = await import("fs/promises");
|
|
@@ -1250,14 +1587,14 @@ async function findStaleSkillsBySourceKnowledgeId(directory, archivedIds) {
|
|
|
1250
1587
|
for (const e of entries) {
|
|
1251
1588
|
if (!e.isDirectory())
|
|
1252
1589
|
continue;
|
|
1253
|
-
const skillDir =
|
|
1254
|
-
const retiredMarker =
|
|
1590
|
+
const skillDir = path4.join(activeDir, e.name);
|
|
1591
|
+
const retiredMarker = path4.join(skillDir, "retired.marker");
|
|
1255
1592
|
if (existsSync2(retiredMarker))
|
|
1256
1593
|
continue;
|
|
1257
|
-
const staleMarker =
|
|
1594
|
+
const staleMarker = path4.join(skillDir, "stale.marker");
|
|
1258
1595
|
if (!existsSync2(staleMarker))
|
|
1259
1596
|
continue;
|
|
1260
|
-
const skillPath =
|
|
1597
|
+
const skillPath = path4.join(skillDir, "SKILL.md");
|
|
1261
1598
|
if (!existsSync2(skillPath))
|
|
1262
1599
|
continue;
|
|
1263
1600
|
let content;
|
|
@@ -1283,8 +1620,8 @@ async function listSkills(directory) {
|
|
|
1283
1620
|
active: [],
|
|
1284
1621
|
stale: []
|
|
1285
1622
|
};
|
|
1286
|
-
const proposalsDir =
|
|
1287
|
-
const activeDir =
|
|
1623
|
+
const proposalsDir = path4.join(directory, ".swarm", "skills", "proposals");
|
|
1624
|
+
const activeDir = path4.join(directory, ".opencode", "skills", "generated");
|
|
1288
1625
|
const fs = await import("fs/promises");
|
|
1289
1626
|
if (existsSync2(proposalsDir)) {
|
|
1290
1627
|
const entries = await fs.readdir(proposalsDir);
|
|
@@ -1294,7 +1631,7 @@ async function listSkills(directory) {
|
|
|
1294
1631
|
const slug = f.replace(/\.md$/, "");
|
|
1295
1632
|
result.proposals.push({
|
|
1296
1633
|
slug,
|
|
1297
|
-
path:
|
|
1634
|
+
path: path4.join(proposalsDir, f)
|
|
1298
1635
|
});
|
|
1299
1636
|
}
|
|
1300
1637
|
}
|
|
@@ -1303,10 +1640,10 @@ async function listSkills(directory) {
|
|
|
1303
1640
|
for (const e of entries) {
|
|
1304
1641
|
if (!e.isDirectory())
|
|
1305
1642
|
continue;
|
|
1306
|
-
const retiredMarker =
|
|
1643
|
+
const retiredMarker = path4.join(activeDir, e.name, "retired.marker");
|
|
1307
1644
|
if (existsSync2(retiredMarker))
|
|
1308
1645
|
continue;
|
|
1309
|
-
const staleMarker =
|
|
1646
|
+
const staleMarker = path4.join(activeDir, e.name, "stale.marker");
|
|
1310
1647
|
if (existsSync2(staleMarker)) {
|
|
1311
1648
|
let reason = "stale";
|
|
1312
1649
|
try {
|
|
@@ -1316,7 +1653,7 @@ async function listSkills(directory) {
|
|
|
1316
1653
|
result.stale.push({ slug: e.name, reason });
|
|
1317
1654
|
continue;
|
|
1318
1655
|
}
|
|
1319
|
-
const skillPath =
|
|
1656
|
+
const skillPath = path4.join(activeDir, e.name, "SKILL.md");
|
|
1320
1657
|
if (existsSync2(skillPath)) {
|
|
1321
1658
|
result.active.push({
|
|
1322
1659
|
slug: e.name,
|
|
@@ -1341,7 +1678,7 @@ async function autoApplyProposals(directory, llmDelegate) {
|
|
|
1341
1678
|
}
|
|
1342
1679
|
let content;
|
|
1343
1680
|
try {
|
|
1344
|
-
content = await
|
|
1681
|
+
content = await readFile4(proposal.path, "utf-8");
|
|
1345
1682
|
} catch {
|
|
1346
1683
|
result.skipped.push(proposal.slug);
|
|
1347
1684
|
continue;
|
|
@@ -1407,7 +1744,7 @@ async function inspectSkill(directory, slug, prefer = "auto") {
|
|
|
1407
1744
|
candidates.push({ p: proposalPath(directory, cleanSlug), m: "draft" });
|
|
1408
1745
|
for (const c of candidates) {
|
|
1409
1746
|
if (existsSync2(c.p)) {
|
|
1410
|
-
const content = await
|
|
1747
|
+
const content = await readFile4(c.p, "utf-8");
|
|
1411
1748
|
const result = { found: true, path: c.p, content, mode: c.m };
|
|
1412
1749
|
const fm = parseDraftFrontmatter(content);
|
|
1413
1750
|
if (fm && fm.sourceKnowledgeIds.length > 0) {
|
|
@@ -1427,11 +1764,11 @@ async function inspectSkill(directory, slug, prefer = "auto") {
|
|
|
1427
1764
|
});
|
|
1428
1765
|
}
|
|
1429
1766
|
if (c.m === "active") {
|
|
1430
|
-
const skillDir =
|
|
1431
|
-
const staleMarker =
|
|
1767
|
+
const skillDir = path4.join(directory, ".opencode", "skills", "generated", cleanSlug);
|
|
1768
|
+
const staleMarker = path4.join(skillDir, "stale.marker");
|
|
1432
1769
|
if (existsSync2(staleMarker)) {
|
|
1433
1770
|
try {
|
|
1434
|
-
result.stale_reason = await
|
|
1771
|
+
result.stale_reason = await readFile4(staleMarker, "utf-8");
|
|
1435
1772
|
} catch {}
|
|
1436
1773
|
}
|
|
1437
1774
|
}
|
|
@@ -1446,7 +1783,7 @@ async function retireSkill(directory, slug, reason) {
|
|
|
1446
1783
|
return {
|
|
1447
1784
|
retired: false,
|
|
1448
1785
|
path: activePath(directory, cleanSlug),
|
|
1449
|
-
markerPath:
|
|
1786
|
+
markerPath: path4.join(directory, ".opencode", "skills", "generated", cleanSlug, "retired.marker"),
|
|
1450
1787
|
reason: "invalid slug"
|
|
1451
1788
|
};
|
|
1452
1789
|
}
|
|
@@ -1455,18 +1792,18 @@ async function retireSkill(directory, slug, reason) {
|
|
|
1455
1792
|
return {
|
|
1456
1793
|
retired: false,
|
|
1457
1794
|
path: skillPath,
|
|
1458
|
-
markerPath:
|
|
1795
|
+
markerPath: path4.join(directory, ".opencode", "skills", "generated", cleanSlug, "retired.marker"),
|
|
1459
1796
|
reason: "active skill not found"
|
|
1460
1797
|
};
|
|
1461
1798
|
}
|
|
1462
|
-
const markerDir =
|
|
1463
|
-
const markerPath =
|
|
1799
|
+
const markerDir = path4.join(directory, ".opencode", "skills", "generated", cleanSlug);
|
|
1800
|
+
const markerPath = path4.join(markerDir, "retired.marker");
|
|
1464
1801
|
const markerContent = JSON.stringify({
|
|
1465
1802
|
retiredAt: new Date().toISOString(),
|
|
1466
1803
|
reason: reason ?? "manual_retire"
|
|
1467
1804
|
});
|
|
1468
|
-
await
|
|
1469
|
-
await
|
|
1805
|
+
await mkdir4(markerDir, { recursive: true });
|
|
1806
|
+
await writeFile4(markerPath, markerContent, "utf-8");
|
|
1470
1807
|
let clearedLinks = [];
|
|
1471
1808
|
try {
|
|
1472
1809
|
clearedLinks = await clearRetiredSkillLinks(directory, skillPath, cleanSlug);
|
|
@@ -1490,7 +1827,7 @@ async function clearRetiredSkillLinks(directory, skillFilePath, slug) {
|
|
|
1490
1827
|
async function clearSkillLinks(directory, skillFilePath, slug, mode) {
|
|
1491
1828
|
let content;
|
|
1492
1829
|
try {
|
|
1493
|
-
content = await
|
|
1830
|
+
content = await readFile4(skillFilePath, "utf-8");
|
|
1494
1831
|
} catch (err) {
|
|
1495
1832
|
warn(`[skill-generator] clearSkillLinks: failed to read '${skillFilePath}' for slug '${slug}': ${err instanceof Error ? err.message : String(err)}`);
|
|
1496
1833
|
return [];
|
|
@@ -1555,11 +1892,11 @@ async function clearSkillLinks(directory, skillFilePath, slug, mode) {
|
|
|
1555
1892
|
return cleared;
|
|
1556
1893
|
}
|
|
1557
1894
|
async function markSkillStale(skillDir, reason) {
|
|
1558
|
-
await
|
|
1559
|
-
await
|
|
1895
|
+
await mkdir4(skillDir, { recursive: true });
|
|
1896
|
+
await writeFile4(path4.join(skillDir, "stale.marker"), reason, "utf-8");
|
|
1560
1897
|
}
|
|
1561
1898
|
async function clearSkillStale(skillDir) {
|
|
1562
|
-
const markerPath =
|
|
1899
|
+
const markerPath = path4.join(skillDir, "stale.marker");
|
|
1563
1900
|
try {
|
|
1564
1901
|
await unlink(markerPath);
|
|
1565
1902
|
} catch (err) {
|
|
@@ -1571,10 +1908,10 @@ async function clearSkillStale(skillDir) {
|
|
|
1571
1908
|
}
|
|
1572
1909
|
async function retireOrMarkStale(directory, skillDir, archivedKnowledgeIds) {
|
|
1573
1910
|
const fs = await import("fs/promises");
|
|
1574
|
-
const skillPath =
|
|
1911
|
+
const skillPath = path4.join(skillDir, "SKILL.md");
|
|
1575
1912
|
if (!existsSync2(skillPath)) {
|
|
1576
1913
|
await markSkillStale(skillDir, "source knowledge archived, SKILL.md missing");
|
|
1577
|
-
const slug2 =
|
|
1914
|
+
const slug2 = path4.basename(skillDir);
|
|
1578
1915
|
return { action: "stale", slug: slug2, skillDir };
|
|
1579
1916
|
}
|
|
1580
1917
|
let content;
|
|
@@ -1582,18 +1919,18 @@ async function retireOrMarkStale(directory, skillDir, archivedKnowledgeIds) {
|
|
|
1582
1919
|
content = await fs.readFile(skillPath, "utf-8");
|
|
1583
1920
|
} catch {
|
|
1584
1921
|
await markSkillStale(skillDir, "source knowledge archived, SKILL.md unreadable");
|
|
1585
|
-
const slug2 =
|
|
1922
|
+
const slug2 = path4.basename(skillDir);
|
|
1586
1923
|
return { action: "stale", slug: slug2, skillDir };
|
|
1587
1924
|
}
|
|
1588
1925
|
const fm = parseDraftFrontmatter(content);
|
|
1589
1926
|
const sourceIds = fm?.sourceKnowledgeIds ?? [];
|
|
1590
1927
|
if (sourceIds.length === 0) {
|
|
1591
1928
|
await markSkillStale(skillDir, "source knowledge archived, no source_knowledge_ids in frontmatter");
|
|
1592
|
-
const slug2 =
|
|
1929
|
+
const slug2 = path4.basename(skillDir);
|
|
1593
1930
|
return { action: "stale", slug: slug2, skillDir };
|
|
1594
1931
|
}
|
|
1595
1932
|
const allArchived = sourceIds.every((id) => archivedKnowledgeIds.has(id));
|
|
1596
|
-
const slug =
|
|
1933
|
+
const slug = path4.basename(skillDir);
|
|
1597
1934
|
if (allArchived) {
|
|
1598
1935
|
await retireSkill(directory, slug, "all source knowledge entries archived or deleted");
|
|
1599
1936
|
return { action: "retire", slug, skillDir };
|
|
@@ -1623,7 +1960,7 @@ async function regenerateSkill(directory, slug, options = {}) {
|
|
|
1623
1960
|
}
|
|
1624
1961
|
let existingContent;
|
|
1625
1962
|
try {
|
|
1626
|
-
existingContent = await
|
|
1963
|
+
existingContent = await readFile4(skillPath, "utf-8");
|
|
1627
1964
|
} catch (err) {
|
|
1628
1965
|
return {
|
|
1629
1966
|
regenerated: false,
|
|
@@ -1762,7 +2099,7 @@ async function regenerateSkill(directory, slug, options = {}) {
|
|
|
1762
2099
|
try {
|
|
1763
2100
|
await atomicWrite2(skillPath, content);
|
|
1764
2101
|
await stampSourceEntries(directory, cleanSlug, matchedEntries.map((e) => e.id));
|
|
1765
|
-
await clearSkillStale(
|
|
2102
|
+
await clearSkillStale(path4.dirname(activePath(directory, cleanSlug)));
|
|
1766
2103
|
} catch (writeErr) {
|
|
1767
2104
|
return {
|
|
1768
2105
|
regenerated: false,
|
|
@@ -1815,4 +2152,4 @@ var _internals = {
|
|
|
1815
2152
|
clearRetiredSkillLinks
|
|
1816
2153
|
};
|
|
1817
2154
|
|
|
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 };
|
|
2155
|
+
export { advisoryWarn, 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 };
|