opencode-swarm 7.107.3 → 7.107.4
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/consult/SKILL.md +10 -0
- package/.opencode/skills/council/SKILL.md +10 -0
- package/.opencode/skills/engineering-conventions/SKILL.md +20 -0
- package/README.md +2 -2
- package/dist/agents/agent-output-schema.d.ts +1 -1
- package/dist/cli/{config-doctor-cntnpyvj.js → config-doctor-kdq3vae4.js} +3 -2
- package/dist/cli/{curator-llm-factory-x9tk7e6m.js → curator-llm-factory-abw56kg0.js} +16 -11
- package/dist/cli/{curator-1nr4qszq.js → curator-rb05fjjk.js} +16 -11
- package/dist/cli/{dispatch-e4ejrsz8.js → dispatch-anmxxhen.js} +56 -13
- package/dist/cli/{evidence-summary-service-9r28ds8t.js → evidence-summary-service-nwdfnbz1.js} +2 -2
- package/dist/cli/gate-evidence-ywys9vfs.js +1 -1
- package/dist/cli/{guardrail-explain-etdrhd4d.js → guardrail-explain-dayy7zw4.js} +17 -12
- package/dist/cli/{guardrail-log-2rns4g6d.js → guardrail-log-fjypsmek.js} +4 -3
- package/dist/cli/{hive-promoter-j4e6rfa6.js → hive-promoter-34bdmw1w.js} +16 -11
- package/dist/cli/{index-502f6np6.js → index-19vxtwa6.js} +21 -16
- package/dist/cli/{index-tgtmbf3r.js → index-4vsgp5a8.js} +1 -1
- package/dist/cli/index-bfwt3abw.js +14 -0
- package/dist/cli/{index-9wstcavp.js → index-bpgqbwmf.js} +115 -1019
- package/dist/cli/index-bq8kd9r9.js +199 -0
- package/dist/cli/index-dfdg4ws5.js +741 -0
- package/dist/cli/{index-w15984gg.js → index-f45hbd5x.js} +2 -2
- package/dist/cli/{index-cyzzdbvw.js → index-gyzxeza3.js} +354 -116
- package/dist/cli/index-kxm3bdjh.js +631 -0
- package/dist/cli/index-n1sptn1w.js +147 -0
- package/dist/cli/{index-2560cctg.js → index-pchpahdf.js} +11 -0
- package/dist/cli/{index-esg1554h.js → index-wvxhynxq.js} +1 -1
- package/dist/cli/{index-8w4d325g.js → index-xg18az81.js} +61 -22
- package/dist/cli/{index-f7nma02k.js → index-xmarctsf.js} +137 -175
- package/dist/cli/{index-m1xjr58n.js → index-zej8cy0j.js} +1 -1
- package/dist/cli/index.js +17 -12
- package/dist/cli/knowledge-escalator-3sgwrgy9.js +32 -0
- package/dist/cli/knowledge-events-kfc6xmkw.js +62 -0
- package/dist/cli/{knowledge-store-tsa8ezka.js → knowledge-store-kyacem71.js} +3 -1
- package/dist/cli/knowledge-validator-ez3ws8ef.js +57 -0
- package/dist/cli/{pr-subscriptions-0dpn4epk.js → pr-subscriptions-bpvazmsw.js} +2 -2
- package/dist/cli/{schema-vw2ffhe9.js → schema-x0ekqzvs.js} +2 -1
- package/dist/cli/{skill-generator-y6mmq8n5.js → skill-generator-x83re96d.js} +6 -2
- package/dist/commands/knowledge.d.ts +5 -1
- package/dist/commands/registry.d.ts +2 -2
- package/dist/config/evidence-schema.d.ts +9 -9
- package/dist/config/schema.d.ts +42 -12
- package/dist/hooks/knowledge-curator.d.ts +27 -0
- package/dist/hooks/knowledge-escalator.d.ts +16 -0
- package/dist/hooks/knowledge-events.d.ts +11 -0
- package/dist/hooks/knowledge-reader.d.ts +0 -17
- package/dist/hooks/knowledge-store.d.ts +16 -1
- package/dist/hooks/knowledge-types.d.ts +85 -0
- package/dist/hooks/knowledge-validator.d.ts +15 -0
- package/dist/hooks/skill-usage-log.d.ts +3 -0
- package/dist/index.js +307 -305
- package/dist/memory/consolidation.d.ts +1 -1
- package/dist/memory/schema.d.ts +4 -4
- package/dist/sast/rules/rust.d.ts +6 -0
- package/dist/tools/knowledge-archive.d.ts +6 -2
- package/dist/tools/lint.d.ts +2 -2
- package/dist/tools/secretscan.d.ts +16 -0
- package/dist/tools/test-runner.d.ts +1 -1
- package/package.json +1 -1
- package/dist/cli/{index-hkpw4wwa.js → index-4r5z8sgw.js} +3 -3
- package/dist/cli/{index-kzfkggjx.js → index-9b7a6agd.js} +3 -3
- package/dist/cli/{pending-delegations-ws51m1e1.js → pending-delegations-vfnpm8zt.js} +3 -3
|
@@ -1,956 +1,52 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
import {
|
|
3
|
+
ALLOWED_SKILL_PATH_PREFIXES,
|
|
4
|
+
validateSkillPath
|
|
5
|
+
} from "./index-dfdg4ws5.js";
|
|
6
|
+
import {
|
|
7
|
+
effectiveRetrievalOutcomes,
|
|
8
|
+
readKnowledgeCounterRollups
|
|
9
|
+
} from "./index-kxm3bdjh.js";
|
|
2
10
|
import {
|
|
3
11
|
computeOutcomeSignal,
|
|
4
|
-
findNearDuplicate,
|
|
5
|
-
inferTags,
|
|
6
12
|
readKnowledge,
|
|
7
13
|
resolveHiveKnowledgePath,
|
|
8
|
-
resolveKnowledgeStoreDir,
|
|
9
14
|
resolveSwarmKnowledgePath,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
transactKnowledge
|
|
13
|
-
} from "./index-f7nma02k.js";
|
|
14
|
-
import {
|
|
15
|
-
atomicWriteFile
|
|
16
|
-
} from "./index-9b7qp18e.js";
|
|
15
|
+
rewriteKnowledge
|
|
16
|
+
} from "./index-xmarctsf.js";
|
|
17
17
|
import {
|
|
18
18
|
init_logger,
|
|
19
19
|
warn
|
|
20
20
|
} from "./index-zgwm4ryv.js";
|
|
21
21
|
import {
|
|
22
|
-
|
|
23
|
-
} from "./index-3naa2ajc.js";
|
|
24
|
-
import {
|
|
25
|
-
__require,
|
|
26
|
-
__toESM
|
|
22
|
+
__require
|
|
27
23
|
} from "./index-a76rekgs.js";
|
|
28
24
|
|
|
29
25
|
// src/services/skill-generator.ts
|
|
30
|
-
import { existsSync as
|
|
31
|
-
import { mkdir as
|
|
32
|
-
import * as
|
|
33
|
-
|
|
34
|
-
// src/hooks/knowledge-events.ts
|
|
35
|
-
import { existsSync } from "fs";
|
|
36
|
-
import { appendFile, mkdir, readFile, stat } from "fs/promises";
|
|
37
|
-
var import_proper_lockfile = __toESM(require_proper_lockfile(), 1);
|
|
38
|
-
import * as path from "path";
|
|
39
|
-
init_logger();
|
|
40
|
-
var counterRollupCache = new Map;
|
|
41
|
-
var MAX_COUNTER_ROLLUP_CACHE_DIRS = 32;
|
|
42
|
-
var RECEIPT_EVENT_TYPES = new Set([
|
|
43
|
-
"acknowledged",
|
|
44
|
-
"applied",
|
|
45
|
-
"ignored",
|
|
46
|
-
"contradicted",
|
|
47
|
-
"violated",
|
|
48
|
-
"n_a",
|
|
49
|
-
"override"
|
|
50
|
-
]);
|
|
51
|
-
function resolveKnowledgeEventsPath(directory) {
|
|
52
|
-
return path.join(resolveKnowledgeStoreDir(directory), "knowledge-events.jsonl");
|
|
53
|
-
}
|
|
54
|
-
function resolveKnowledgeCounterBaselinePath(directory) {
|
|
55
|
-
return path.join(resolveKnowledgeStoreDir(directory), "knowledge-counter-baseline.json");
|
|
56
|
-
}
|
|
57
|
-
function resolveLegacyApplicationLogPath(directory) {
|
|
58
|
-
return path.join(resolveKnowledgeStoreDir(directory), "knowledge-application.jsonl");
|
|
59
|
-
}
|
|
60
|
-
async function readKnowledgeEvents(directory, maxEvents) {
|
|
61
|
-
const filePath = resolveKnowledgeEventsPath(directory);
|
|
62
|
-
if (!existsSync(filePath))
|
|
63
|
-
return [];
|
|
64
|
-
const content = await readFile(filePath, "utf-8");
|
|
65
|
-
const out = [];
|
|
66
|
-
const max = maxEvents !== undefined && maxEvents > 0 ? maxEvents : Infinity;
|
|
67
|
-
for (const line of content.split(`
|
|
68
|
-
`)) {
|
|
69
|
-
if (out.length >= max)
|
|
70
|
-
break;
|
|
71
|
-
const trimmed = line.trim();
|
|
72
|
-
if (!trimmed)
|
|
73
|
-
continue;
|
|
74
|
-
try {
|
|
75
|
-
out.push(JSON.parse(trimmed));
|
|
76
|
-
} catch {
|
|
77
|
-
warn(`[knowledge-events] Skipping corrupted JSONL line in ${filePath}: ${trimmed.slice(0, 80)}`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return out;
|
|
81
|
-
}
|
|
82
|
-
async function readLegacyApplicationRecords(directory) {
|
|
83
|
-
const filePath = resolveLegacyApplicationLogPath(directory);
|
|
84
|
-
if (!existsSync(filePath))
|
|
85
|
-
return [];
|
|
86
|
-
const content = await readFile(filePath, "utf-8");
|
|
87
|
-
const out = [];
|
|
88
|
-
for (const line of content.split(`
|
|
89
|
-
`)) {
|
|
90
|
-
const trimmed = line.trim();
|
|
91
|
-
if (!trimmed)
|
|
92
|
-
continue;
|
|
93
|
-
try {
|
|
94
|
-
out.push(JSON.parse(trimmed));
|
|
95
|
-
} catch {
|
|
96
|
-
warn(`[knowledge-events] Skipping corrupted JSONL line in ${filePath}: ${trimmed.slice(0, 80)}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return out;
|
|
100
|
-
}
|
|
101
|
-
var MAX_VIOLATION_TIMESTAMPS = 10;
|
|
102
|
-
function emptyRollup() {
|
|
103
|
-
return {
|
|
104
|
-
shown_count: 0,
|
|
105
|
-
acknowledged_count: 0,
|
|
106
|
-
applied_explicit_count: 0,
|
|
107
|
-
ignored_count: 0,
|
|
108
|
-
violated_count: 0,
|
|
109
|
-
contradicted_count: 0,
|
|
110
|
-
n_a_count: 0,
|
|
111
|
-
succeeded_after_shown_count: 0,
|
|
112
|
-
failed_after_shown_count: 0,
|
|
113
|
-
partial_after_shown_count: 0,
|
|
114
|
-
violation_timestamps: []
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
function cloneRollup(input) {
|
|
118
|
-
return {
|
|
119
|
-
...emptyRollup(),
|
|
120
|
-
...input,
|
|
121
|
-
violation_timestamps: [...input.violation_timestamps ?? []]
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
function cloneRollupMap(input) {
|
|
125
|
-
const out = new Map;
|
|
126
|
-
for (const [id, rollup] of input) {
|
|
127
|
-
out.set(id, cloneRollup(rollup));
|
|
128
|
-
}
|
|
129
|
-
return out;
|
|
130
|
-
}
|
|
131
|
-
function normalizeRollupTimestamps(rollup) {
|
|
132
|
-
if (rollup.violation_timestamps.length > 1) {
|
|
133
|
-
rollup.violation_timestamps.sort((a, b) => a < b ? 1 : a > b ? -1 : 0);
|
|
134
|
-
}
|
|
135
|
-
if (rollup.violation_timestamps.length > MAX_VIOLATION_TIMESTAMPS) {
|
|
136
|
-
rollup.violation_timestamps = rollup.violation_timestamps.slice(0, MAX_VIOLATION_TIMESTAMPS);
|
|
137
|
-
}
|
|
138
|
-
return rollup;
|
|
139
|
-
}
|
|
140
|
-
function get(map, id) {
|
|
141
|
-
let r = map.get(id);
|
|
142
|
-
if (!r) {
|
|
143
|
-
r = emptyRollup();
|
|
144
|
-
map.set(id, r);
|
|
145
|
-
}
|
|
146
|
-
return r;
|
|
147
|
-
}
|
|
148
|
-
function maxIso(current, candidate) {
|
|
149
|
-
if (!current)
|
|
150
|
-
return candidate;
|
|
151
|
-
return candidate > current ? candidate : current;
|
|
152
|
-
}
|
|
153
|
-
async function readCounterBaseline(directory) {
|
|
154
|
-
const filePath = resolveKnowledgeCounterBaselinePath(directory);
|
|
155
|
-
if (!existsSync(filePath))
|
|
156
|
-
return new Map;
|
|
157
|
-
let raw;
|
|
158
|
-
try {
|
|
159
|
-
raw = JSON.parse(await readFile(filePath, "utf-8"));
|
|
160
|
-
} catch (err) {
|
|
161
|
-
warn(`[knowledge-events] corrupted counter baseline at ${filePath}; ignoring folded baseline and replaying live events only: ${err instanceof Error ? err.message : String(err)}`);
|
|
162
|
-
return new Map;
|
|
163
|
-
}
|
|
164
|
-
const map = new Map;
|
|
165
|
-
for (const [id, rollup] of Object.entries(raw)) {
|
|
166
|
-
map.set(id, normalizeRollupTimestamps(cloneRollup(rollup)));
|
|
167
|
-
}
|
|
168
|
-
return map;
|
|
169
|
-
}
|
|
170
|
-
async function statCacheKey(filePath) {
|
|
171
|
-
try {
|
|
172
|
-
const fileStat = await stat(filePath);
|
|
173
|
-
return `${fileStat.mtimeMs}:${fileStat.ctimeMs}:${fileStat.size}`;
|
|
174
|
-
} catch (err) {
|
|
175
|
-
if (err?.code === "ENOENT")
|
|
176
|
-
return "missing";
|
|
177
|
-
throw err;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
async function buildCounterRollupCacheKey(directory) {
|
|
181
|
-
const [eventsKey, legacyKey, baselineKey] = await Promise.all([
|
|
182
|
-
statCacheKey(resolveKnowledgeEventsPath(directory)),
|
|
183
|
-
statCacheKey(resolveLegacyApplicationLogPath(directory)),
|
|
184
|
-
statCacheKey(resolveKnowledgeCounterBaselinePath(directory))
|
|
185
|
-
]);
|
|
186
|
-
return `${eventsKey}|${legacyKey}|${baselineKey}`;
|
|
187
|
-
}
|
|
188
|
-
function setCounterRollupCache(directory, key, rollups) {
|
|
189
|
-
if (counterRollupCache.has(directory)) {
|
|
190
|
-
counterRollupCache.delete(directory);
|
|
191
|
-
}
|
|
192
|
-
counterRollupCache.set(directory, {
|
|
193
|
-
key,
|
|
194
|
-
rollups: cloneRollupMap(rollups)
|
|
195
|
-
});
|
|
196
|
-
while (counterRollupCache.size > MAX_COUNTER_ROLLUP_CACHE_DIRS) {
|
|
197
|
-
const oldestKey = counterRollupCache.keys().next().value;
|
|
198
|
-
if (oldestKey === undefined)
|
|
199
|
-
break;
|
|
200
|
-
counterRollupCache.delete(oldestKey);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
function recomputeCounters(events, legacyRecords = [], baseline = new Map) {
|
|
204
|
-
const map = new Map;
|
|
205
|
-
const retrievedIds = new Set;
|
|
206
|
-
for (const [id, rollup] of baseline) {
|
|
207
|
-
map.set(id, cloneRollup(rollup));
|
|
208
|
-
if ((rollup.shown_count ?? 0) > 0)
|
|
209
|
-
retrievedIds.add(id);
|
|
210
|
-
}
|
|
211
|
-
for (const e of events) {
|
|
212
|
-
switch (e.type) {
|
|
213
|
-
case "retrieved": {
|
|
214
|
-
for (const id of e.result_ids) {
|
|
215
|
-
retrievedIds.add(id);
|
|
216
|
-
get(map, id).shown_count += 1;
|
|
217
|
-
}
|
|
218
|
-
break;
|
|
219
|
-
}
|
|
220
|
-
case "acknowledged": {
|
|
221
|
-
const r = get(map, e.knowledge_id);
|
|
222
|
-
r.acknowledged_count += 1;
|
|
223
|
-
r.last_acknowledged_at = maxIso(r.last_acknowledged_at, e.timestamp);
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
case "applied": {
|
|
227
|
-
const r = get(map, e.knowledge_id);
|
|
228
|
-
r.applied_explicit_count += 1;
|
|
229
|
-
r.last_applied_at = maxIso(r.last_applied_at, e.timestamp);
|
|
230
|
-
break;
|
|
231
|
-
}
|
|
232
|
-
case "ignored":
|
|
233
|
-
get(map, e.knowledge_id).ignored_count += 1;
|
|
234
|
-
break;
|
|
235
|
-
case "violated": {
|
|
236
|
-
const r = get(map, e.knowledge_id);
|
|
237
|
-
r.violated_count += 1;
|
|
238
|
-
r.violation_timestamps.push(e.timestamp);
|
|
239
|
-
break;
|
|
240
|
-
}
|
|
241
|
-
case "contradicted":
|
|
242
|
-
get(map, e.knowledge_id).contradicted_count += 1;
|
|
243
|
-
break;
|
|
244
|
-
case "n_a":
|
|
245
|
-
get(map, e.knowledge_id).n_a_count += 1;
|
|
246
|
-
break;
|
|
247
|
-
case "outcome": {
|
|
248
|
-
if (!e.knowledge_id)
|
|
249
|
-
break;
|
|
250
|
-
const r = get(map, e.knowledge_id);
|
|
251
|
-
if (e.outcome === "success")
|
|
252
|
-
r.succeeded_after_shown_count += 1;
|
|
253
|
-
else if (e.outcome === "failure")
|
|
254
|
-
r.failed_after_shown_count += 1;
|
|
255
|
-
else if (e.outcome === "partial")
|
|
256
|
-
r.partial_after_shown_count += 1;
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
for (const rec of legacyRecords) {
|
|
262
|
-
const r = get(map, rec.knowledgeId);
|
|
263
|
-
switch (rec.result) {
|
|
264
|
-
case "shown":
|
|
265
|
-
if (!retrievedIds.has(rec.knowledgeId))
|
|
266
|
-
r.shown_count += 1;
|
|
267
|
-
break;
|
|
268
|
-
case "acknowledged":
|
|
269
|
-
r.acknowledged_count += 1;
|
|
270
|
-
r.last_acknowledged_at = maxIso(r.last_acknowledged_at, rec.timestamp);
|
|
271
|
-
break;
|
|
272
|
-
case "applied":
|
|
273
|
-
r.applied_explicit_count += 1;
|
|
274
|
-
r.last_applied_at = maxIso(r.last_applied_at, rec.timestamp);
|
|
275
|
-
break;
|
|
276
|
-
case "ignored":
|
|
277
|
-
r.ignored_count += 1;
|
|
278
|
-
break;
|
|
279
|
-
case "violated":
|
|
280
|
-
r.violated_count += 1;
|
|
281
|
-
r.violation_timestamps.push(rec.timestamp);
|
|
282
|
-
break;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
for (const r of map.values()) {
|
|
286
|
-
normalizeRollupTimestamps(r);
|
|
287
|
-
}
|
|
288
|
-
return map;
|
|
289
|
-
}
|
|
290
|
-
async function readKnowledgeCounterRollups(directory) {
|
|
291
|
-
try {
|
|
292
|
-
const cacheKey = await buildCounterRollupCacheKey(directory);
|
|
293
|
-
const cached = counterRollupCache.get(directory);
|
|
294
|
-
if (cached?.key === cacheKey) {
|
|
295
|
-
counterRollupCache.delete(directory);
|
|
296
|
-
counterRollupCache.set(directory, cached);
|
|
297
|
-
return cloneRollupMap(cached.rollups);
|
|
298
|
-
}
|
|
299
|
-
const [events, legacyRecords, baseline] = await Promise.all([
|
|
300
|
-
readKnowledgeEvents(directory),
|
|
301
|
-
readLegacyApplicationRecords(directory),
|
|
302
|
-
readCounterBaseline(directory)
|
|
303
|
-
]);
|
|
304
|
-
const rollups = recomputeCounters(events, legacyRecords, baseline);
|
|
305
|
-
setCounterRollupCache(directory, cacheKey, rollups);
|
|
306
|
-
return cloneRollupMap(rollups);
|
|
307
|
-
} catch (err) {
|
|
308
|
-
warn(`[knowledge-events] readKnowledgeCounterRollups failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
309
|
-
return new Map;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
function effectiveRetrievalOutcomes(stored, rollup) {
|
|
313
|
-
const base = stored ?? {
|
|
314
|
-
applied_count: 0,
|
|
315
|
-
succeeded_after_count: 0,
|
|
316
|
-
failed_after_count: 0
|
|
317
|
-
};
|
|
318
|
-
if (!rollup)
|
|
319
|
-
return base;
|
|
320
|
-
const { n_a_count: _na, ...rollupWithoutNa } = rollup;
|
|
321
|
-
return {
|
|
322
|
-
...base,
|
|
323
|
-
...rollupWithoutNa,
|
|
324
|
-
succeeded_after_shown_count: (base.succeeded_after_shown_count ?? 0) + (rollup.succeeded_after_shown_count ?? 0),
|
|
325
|
-
failed_after_shown_count: (base.failed_after_shown_count ?? 0) + (rollup.failed_after_shown_count ?? 0)
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// src/hooks/knowledge-validator.ts
|
|
330
|
-
var import_proper_lockfile2 = __toESM(require_proper_lockfile(), 1);
|
|
331
|
-
import { appendFile as appendFile2, mkdir as mkdir2 } from "fs/promises";
|
|
332
|
-
import * as path2 from "path";
|
|
333
|
-
init_logger();
|
|
334
|
-
var DANGEROUS_COMMAND_ERROR_PATTERNS = [
|
|
335
|
-
/\brm\s+-rf\b/,
|
|
336
|
-
/\bsudo\s+rm\b/,
|
|
337
|
-
/\bmkfs\b/,
|
|
338
|
-
/\bdd\s+if=/,
|
|
339
|
-
/:\(\)\s*\{/,
|
|
340
|
-
/\bchmod\s+-R\s+777\b/i,
|
|
341
|
-
/\bdeltree\b/,
|
|
342
|
-
/\brmdir\s+\/s\b/
|
|
343
|
-
];
|
|
344
|
-
var DANGEROUS_COMMAND_WARNING_PATTERNS = [
|
|
345
|
-
/\bformat\b/,
|
|
346
|
-
/\bkill\s+-9\b/,
|
|
347
|
-
/\bpkill\b/,
|
|
348
|
-
/\bkillall\b/,
|
|
349
|
-
/`[^`]*`/,
|
|
350
|
-
/\$\([^)]*\)/
|
|
351
|
-
];
|
|
352
|
-
var DANGEROUS_COMMAND_PATTERNS = [
|
|
353
|
-
...DANGEROUS_COMMAND_ERROR_PATTERNS,
|
|
354
|
-
...DANGEROUS_COMMAND_WARNING_PATTERNS
|
|
355
|
-
];
|
|
356
|
-
var SECURITY_DEGRADING_PATTERNS = [
|
|
357
|
-
/disable\s+.{0,50}firewall/i,
|
|
358
|
-
/turn\s+off\s+.{0,50}security/i,
|
|
359
|
-
/skip\s+.{0,50}auth/i,
|
|
360
|
-
/bypass\s+.{0,50}auth/i,
|
|
361
|
-
/ignore\s+.{0,50}certificate/i,
|
|
362
|
-
/disable\s+.{0,50}tls/i,
|
|
363
|
-
/disable\s+.{0,50}ssl/i,
|
|
364
|
-
/no\s+.{0,50}validation/i,
|
|
365
|
-
/disable\s+.{0,50}2fa/i,
|
|
366
|
-
/remove\s+.{0,50}password/i
|
|
367
|
-
];
|
|
368
|
-
var INVISIBLE_FORMAT_CHARS = /[\u00AD\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069\uFEFF]/g;
|
|
369
|
-
var INJECTION_PATTERNS = [
|
|
370
|
-
/[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f\x0d]/,
|
|
371
|
-
/[\u00AD\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069\uFEFF]/,
|
|
372
|
-
/^system\s*:/i,
|
|
373
|
-
/<script/i,
|
|
374
|
-
/javascript:/i,
|
|
375
|
-
/\beval\(/i,
|
|
376
|
-
/\b__proto__\b/,
|
|
377
|
-
/\bconstructor\[/,
|
|
378
|
-
/\.prototype\[/
|
|
379
|
-
];
|
|
380
|
-
var VALID_CATEGORIES = new Set([
|
|
381
|
-
"process",
|
|
382
|
-
"architecture",
|
|
383
|
-
"tooling",
|
|
384
|
-
"security",
|
|
385
|
-
"testing",
|
|
386
|
-
"debugging",
|
|
387
|
-
"performance",
|
|
388
|
-
"integration",
|
|
389
|
-
"todo",
|
|
390
|
-
"other"
|
|
391
|
-
]);
|
|
392
|
-
var TECH_REFERENCE_WORDS = new Set([
|
|
393
|
-
"git",
|
|
394
|
-
"docker",
|
|
395
|
-
"typescript",
|
|
396
|
-
"bun",
|
|
397
|
-
"vitest",
|
|
398
|
-
"node",
|
|
399
|
-
"python",
|
|
400
|
-
"react",
|
|
401
|
-
"sql",
|
|
402
|
-
"api",
|
|
403
|
-
"hook",
|
|
404
|
-
"test",
|
|
405
|
-
"schema",
|
|
406
|
-
"config",
|
|
407
|
-
"file",
|
|
408
|
-
"function",
|
|
409
|
-
"class",
|
|
410
|
-
"module",
|
|
411
|
-
"import",
|
|
412
|
-
"export"
|
|
413
|
-
]);
|
|
414
|
-
var ACTION_VERB_WORDS = new Set([
|
|
415
|
-
"use",
|
|
416
|
-
"avoid",
|
|
417
|
-
"prefer",
|
|
418
|
-
"run",
|
|
419
|
-
"check",
|
|
420
|
-
"always",
|
|
421
|
-
"never",
|
|
422
|
-
"ensure",
|
|
423
|
-
"call",
|
|
424
|
-
"write",
|
|
425
|
-
"add",
|
|
426
|
-
"remove",
|
|
427
|
-
"update",
|
|
428
|
-
"set",
|
|
429
|
-
"enable",
|
|
430
|
-
"disable"
|
|
431
|
-
]);
|
|
432
|
-
var NEGATION_PAIRS = [
|
|
433
|
-
["always", "never"],
|
|
434
|
-
["must", "must not"],
|
|
435
|
-
["must", "should not"],
|
|
436
|
-
["enable", "disable"],
|
|
437
|
-
["use", "avoid"],
|
|
438
|
-
["use", "don't use"],
|
|
439
|
-
["recommended", "not recommended"]
|
|
440
|
-
];
|
|
441
|
-
function normalizeText(text) {
|
|
442
|
-
return text.normalize("NFKC").toLowerCase().replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
443
|
-
}
|
|
444
|
-
function extractContextWords(text, word, contextWindow = 3) {
|
|
445
|
-
const words = text.split(" ");
|
|
446
|
-
const context = new Set;
|
|
447
|
-
if (!word.includes(" ")) {
|
|
448
|
-
let from = 0;
|
|
449
|
-
let idx = words.indexOf(word, from);
|
|
450
|
-
while (idx !== -1) {
|
|
451
|
-
const start = Math.max(0, idx - contextWindow);
|
|
452
|
-
const end = Math.min(words.length, idx + contextWindow + 1);
|
|
453
|
-
for (let i2 = start;i2 < end; i2++) {
|
|
454
|
-
if (i2 !== idx && words[i2] && words[i2].length > 0) {
|
|
455
|
-
context.add(words[i2]);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
from = idx + 1;
|
|
459
|
-
idx = words.indexOf(word, from);
|
|
460
|
-
}
|
|
461
|
-
return context;
|
|
462
|
-
}
|
|
463
|
-
const termLen = word.split(" ").length;
|
|
464
|
-
let i = 0;
|
|
465
|
-
while (i <= words.length - termLen) {
|
|
466
|
-
const slice = words.slice(i, i + termLen).join(" ");
|
|
467
|
-
if (slice === word) {
|
|
468
|
-
const start = Math.max(0, i - contextWindow);
|
|
469
|
-
const end = Math.min(words.length, i + termLen + contextWindow);
|
|
470
|
-
for (let j = start;j < end; j++) {
|
|
471
|
-
if (j < i || j >= i + termLen) {
|
|
472
|
-
if (words[j] && words[j].length > 0) {
|
|
473
|
-
context.add(words[j]);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
i += termLen;
|
|
478
|
-
} else {
|
|
479
|
-
i += 1;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
return context;
|
|
483
|
-
}
|
|
484
|
-
function hasSignificantOverlap(set1, set2) {
|
|
485
|
-
if (set1.size === 0 || set2.size === 0)
|
|
486
|
-
return false;
|
|
487
|
-
return [...set1].some((word) => set2.has(word));
|
|
488
|
-
}
|
|
489
|
-
function detectContradiction(candidate, existingLessons) {
|
|
490
|
-
const candidateTags = inferTags(candidate);
|
|
491
|
-
if (candidateTags.length === 0)
|
|
492
|
-
return false;
|
|
493
|
-
const candidateNorm = normalizeText(candidate);
|
|
494
|
-
for (const existing of existingLessons) {
|
|
495
|
-
const existingTags = inferTags(existing);
|
|
496
|
-
const shared = candidateTags.some((t) => existingTags.includes(t));
|
|
497
|
-
if (!shared)
|
|
498
|
-
continue;
|
|
499
|
-
const existingNorm = normalizeText(existing);
|
|
500
|
-
for (const [wordA, wordB] of NEGATION_PAIRS) {
|
|
501
|
-
if (candidateNorm.includes(wordA) && existingNorm.includes(wordB)) {
|
|
502
|
-
const contextA = extractContextWords(candidateNorm, wordA);
|
|
503
|
-
const contextB = extractContextWords(existingNorm, wordB);
|
|
504
|
-
if (hasSignificantOverlap(contextA, contextB)) {
|
|
505
|
-
return true;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
if (candidateNorm.includes(wordB) && existingNorm.includes(wordA)) {
|
|
509
|
-
const contextB = extractContextWords(candidateNorm, wordB);
|
|
510
|
-
const contextA = extractContextWords(existingNorm, wordA);
|
|
511
|
-
if (hasSignificantOverlap(contextA, contextB)) {
|
|
512
|
-
return true;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
return false;
|
|
518
|
-
}
|
|
519
|
-
function isVagueLesson(lesson) {
|
|
520
|
-
const lower = normalizeText(lesson);
|
|
521
|
-
const words = lower.split(/\s+/);
|
|
522
|
-
const hasTechRef = words.some((w) => TECH_REFERENCE_WORDS.has(w));
|
|
523
|
-
const hasActionVerb = words.some((w) => ACTION_VERB_WORDS.has(w));
|
|
524
|
-
return !hasTechRef && !hasActionVerb;
|
|
525
|
-
}
|
|
526
|
-
function validateLesson(candidate, existingLessons, meta) {
|
|
527
|
-
if (!candidate || typeof candidate !== "string") {
|
|
528
|
-
return {
|
|
529
|
-
valid: false,
|
|
530
|
-
layer: 1,
|
|
531
|
-
reason: "lesson too short (min 15 chars)",
|
|
532
|
-
severity: "error"
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
if (!Array.isArray(existingLessons)) {
|
|
536
|
-
existingLessons = [];
|
|
537
|
-
}
|
|
538
|
-
if (candidate.length < 15) {
|
|
539
|
-
return {
|
|
540
|
-
valid: false,
|
|
541
|
-
layer: 1,
|
|
542
|
-
reason: "lesson too short (min 15 chars)",
|
|
543
|
-
severity: "error"
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
if (candidate.length > 280) {
|
|
547
|
-
return {
|
|
548
|
-
valid: false,
|
|
549
|
-
layer: 1,
|
|
550
|
-
reason: "lesson too long (max 280 chars)",
|
|
551
|
-
severity: "error"
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
if (!VALID_CATEGORIES.has(meta.category)) {
|
|
555
|
-
return {
|
|
556
|
-
valid: false,
|
|
557
|
-
layer: 1,
|
|
558
|
-
reason: `invalid category: ${meta.category}`,
|
|
559
|
-
severity: "error"
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
const isGlobalScope = meta.scope === "global";
|
|
563
|
-
const isStackScope = /^stack:[a-zA-Z0-9_-]{1,64}$/.test(meta.scope);
|
|
564
|
-
if (!isGlobalScope && !isStackScope) {
|
|
565
|
-
return {
|
|
566
|
-
valid: false,
|
|
567
|
-
layer: 1,
|
|
568
|
-
reason: "invalid scope: must be 'global' or 'stack:<name>'",
|
|
569
|
-
severity: "error"
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
if (!(meta.confidence >= 0 && meta.confidence <= 1)) {
|
|
573
|
-
return {
|
|
574
|
-
valid: false,
|
|
575
|
-
layer: 1,
|
|
576
|
-
reason: "confidence out of range [0.0, 1.0]",
|
|
577
|
-
severity: "error"
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
const normalizedCandidate = candidate.normalize("NFKC").replace(INVISIBLE_FORMAT_CHARS, " ").replace(/\s+/g, " ").toLowerCase();
|
|
581
|
-
for (const pattern of DANGEROUS_COMMAND_ERROR_PATTERNS) {
|
|
582
|
-
if (pattern.test(normalizedCandidate)) {
|
|
583
|
-
return {
|
|
584
|
-
valid: false,
|
|
585
|
-
layer: 2,
|
|
586
|
-
reason: "dangerous command pattern detected",
|
|
587
|
-
severity: "error"
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
for (const pattern of DANGEROUS_COMMAND_WARNING_PATTERNS) {
|
|
592
|
-
if (pattern.test(normalizedCandidate)) {
|
|
593
|
-
return {
|
|
594
|
-
valid: true,
|
|
595
|
-
layer: 2,
|
|
596
|
-
reason: "potentially dangerous command pattern queued for review",
|
|
597
|
-
severity: "warning"
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
for (const pattern of SECURITY_DEGRADING_PATTERNS) {
|
|
602
|
-
if (pattern.test(normalizedCandidate)) {
|
|
603
|
-
return {
|
|
604
|
-
valid: false,
|
|
605
|
-
layer: 2,
|
|
606
|
-
reason: "security-degrading instruction detected",
|
|
607
|
-
severity: "error"
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
for (const pattern of INJECTION_PATTERNS) {
|
|
612
|
-
if (pattern.test(candidate)) {
|
|
613
|
-
return {
|
|
614
|
-
valid: false,
|
|
615
|
-
layer: 2,
|
|
616
|
-
reason: "injection pattern detected",
|
|
617
|
-
severity: "error"
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
if (detectContradiction(candidate, existingLessons)) {
|
|
622
|
-
return {
|
|
623
|
-
valid: true,
|
|
624
|
-
layer: 3,
|
|
625
|
-
reason: "possible contradiction with an existing lesson with shared tags",
|
|
626
|
-
severity: "warning"
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
if (isVagueLesson(candidate)) {
|
|
630
|
-
return {
|
|
631
|
-
valid: true,
|
|
632
|
-
layer: 3,
|
|
633
|
-
reason: "lesson may be too vague (no tech reference or action verb)",
|
|
634
|
-
severity: "warning"
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
return {
|
|
638
|
-
valid: true,
|
|
639
|
-
layer: null,
|
|
640
|
-
reason: null,
|
|
641
|
-
severity: null
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
var ACTIONABLE_STRING_MAX = 200;
|
|
645
|
-
var ACTIONABLE_LIST_MAX = 20;
|
|
646
|
-
var NAME_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
|
|
647
|
-
var SOURCE_REF_FORBIDDEN = /(\.\.\/|\.\.\\|\0|[\x00-\x1f\x7f])/;
|
|
648
|
-
var ALLOWED_SKILL_PATH_PREFIXES = [
|
|
649
|
-
".opencode/skills/generated/",
|
|
650
|
-
".swarm/skills/proposals/",
|
|
651
|
-
".swarm/skills/candidates/"
|
|
652
|
-
];
|
|
653
|
-
var VALID_DIRECTIVE_PRIORITIES = new Set([
|
|
654
|
-
"low",
|
|
655
|
-
"medium",
|
|
656
|
-
"high",
|
|
657
|
-
"critical"
|
|
658
|
-
]);
|
|
659
|
-
function isCleanShortString(s) {
|
|
660
|
-
if (typeof s !== "string")
|
|
661
|
-
return false;
|
|
662
|
-
if (s.length === 0 || s.length > ACTIONABLE_STRING_MAX)
|
|
663
|
-
return false;
|
|
664
|
-
return !/[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]/.test(s);
|
|
665
|
-
}
|
|
666
|
-
function validateSkillPath(p) {
|
|
667
|
-
if (typeof p !== "string")
|
|
668
|
-
return false;
|
|
669
|
-
if (p.length === 0 || p.length > 256)
|
|
670
|
-
return false;
|
|
671
|
-
if (p.includes("\x00"))
|
|
672
|
-
return false;
|
|
673
|
-
if (path2.isAbsolute(p))
|
|
674
|
-
return false;
|
|
675
|
-
if (p.includes(".."))
|
|
676
|
-
return false;
|
|
677
|
-
const norm = p.replace(/\\/g, "/");
|
|
678
|
-
return ALLOWED_SKILL_PATH_PREFIXES.some((prefix) => norm.startsWith(prefix));
|
|
679
|
-
}
|
|
680
|
-
function validateActionableFields(fields) {
|
|
681
|
-
const errors = [];
|
|
682
|
-
if (!fields)
|
|
683
|
-
return { valid: true, errors };
|
|
684
|
-
function checkStringList(name, list) {
|
|
685
|
-
if (list === undefined)
|
|
686
|
-
return;
|
|
687
|
-
if (!Array.isArray(list)) {
|
|
688
|
-
errors.push(`${name} must be an array`);
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
if (list.length > ACTIONABLE_LIST_MAX) {
|
|
692
|
-
errors.push(`${name} exceeds ${ACTIONABLE_LIST_MAX} items`);
|
|
693
|
-
}
|
|
694
|
-
for (const item of list) {
|
|
695
|
-
if (!isCleanShortString(item)) {
|
|
696
|
-
errors.push(`${name} contains invalid string`);
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
function checkNameList(name, list) {
|
|
702
|
-
if (list === undefined)
|
|
703
|
-
return;
|
|
704
|
-
if (!Array.isArray(list)) {
|
|
705
|
-
errors.push(`${name} must be an array`);
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
if (list.length > ACTIONABLE_LIST_MAX) {
|
|
709
|
-
errors.push(`${name} exceeds ${ACTIONABLE_LIST_MAX} items`);
|
|
710
|
-
}
|
|
711
|
-
for (const item of list) {
|
|
712
|
-
if (typeof item !== "string" || !NAME_PATTERN.test(item)) {
|
|
713
|
-
errors.push(`${name} contains invalid name`);
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
checkStringList("triggers", fields.triggers);
|
|
719
|
-
checkStringList("required_actions", fields.required_actions);
|
|
720
|
-
checkStringList("forbidden_actions", fields.forbidden_actions);
|
|
721
|
-
checkStringList("verification_checks", fields.verification_checks);
|
|
722
|
-
checkNameList("applies_to_agents", fields.applies_to_agents);
|
|
723
|
-
checkNameList("applies_to_tools", fields.applies_to_tools);
|
|
724
|
-
if (fields.source_refs !== undefined) {
|
|
725
|
-
if (!Array.isArray(fields.source_refs)) {
|
|
726
|
-
errors.push("source_refs must be an array");
|
|
727
|
-
} else if (fields.source_refs.length > ACTIONABLE_LIST_MAX) {
|
|
728
|
-
errors.push(`source_refs exceeds ${ACTIONABLE_LIST_MAX} items`);
|
|
729
|
-
} else {
|
|
730
|
-
for (const ref of fields.source_refs) {
|
|
731
|
-
if (typeof ref !== "string" || ref.length === 0 || ref.length > ACTIONABLE_STRING_MAX || SOURCE_REF_FORBIDDEN.test(ref)) {
|
|
732
|
-
errors.push("source_refs contains invalid value");
|
|
733
|
-
break;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
if (fields.source_knowledge_ids !== undefined) {
|
|
739
|
-
if (!Array.isArray(fields.source_knowledge_ids)) {
|
|
740
|
-
errors.push("source_knowledge_ids must be an array");
|
|
741
|
-
} else {
|
|
742
|
-
for (const id of fields.source_knowledge_ids) {
|
|
743
|
-
if (typeof id !== "string" || id.length === 0 || id.length > 64) {
|
|
744
|
-
errors.push("source_knowledge_ids contains invalid value");
|
|
745
|
-
break;
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
if (fields.directive_priority !== undefined && !VALID_DIRECTIVE_PRIORITIES.has(String(fields.directive_priority))) {
|
|
751
|
-
errors.push("directive_priority must be low|medium|high|critical");
|
|
752
|
-
}
|
|
753
|
-
if (fields.generated_skill_slug !== undefined) {
|
|
754
|
-
if (typeof fields.generated_skill_slug !== "string" || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(fields.generated_skill_slug)) {
|
|
755
|
-
errors.push("generated_skill_slug must be a kebab-case slug");
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
if (fields.generated_skill_path !== undefined && !validateSkillPath(fields.generated_skill_path)) {
|
|
759
|
-
errors.push("generated_skill_path must be repo-local under allowed prefix");
|
|
760
|
-
}
|
|
761
|
-
return { valid: errors.length === 0, errors };
|
|
762
|
-
}
|
|
763
|
-
function hasNonEmptyList(v) {
|
|
764
|
-
return Array.isArray(v) && v.length > 0;
|
|
765
|
-
}
|
|
766
|
-
function validateActionability(entry) {
|
|
767
|
-
const hasPredicate = hasNonEmptyList(entry.forbidden_actions) || hasNonEmptyList(entry.required_actions) || hasNonEmptyList(entry.verification_checks) || typeof entry.verification_predicate === "string" && entry.verification_predicate.trim().length > 0;
|
|
768
|
-
const hasScope = hasNonEmptyList(entry.applies_to_tools) || hasNonEmptyList(entry.applies_to_agents);
|
|
769
|
-
if (hasPredicate && hasScope)
|
|
770
|
-
return { actionable: true };
|
|
771
|
-
const reason = !hasPredicate && !hasScope ? "missing_predicate_and_scope" : !hasPredicate ? "missing_predicate" : "missing_scope";
|
|
772
|
-
return { actionable: false, reason };
|
|
773
|
-
}
|
|
774
|
-
function resolveUnactionablePath(directory) {
|
|
775
|
-
return path2.join(resolveKnowledgeStoreDir(directory), "knowledge-unactionable.jsonl");
|
|
776
|
-
}
|
|
777
|
-
async function appendUnactionable(directory, entry, reason) {
|
|
778
|
-
const filePath = resolveUnactionablePath(directory);
|
|
779
|
-
const dirPath = path2.dirname(filePath);
|
|
780
|
-
await mkdir2(dirPath, { recursive: true });
|
|
781
|
-
await transactKnowledge(filePath, (existing) => {
|
|
782
|
-
const record = {
|
|
783
|
-
...entry,
|
|
784
|
-
status: "quarantined_unactionable",
|
|
785
|
-
unactionable_reason: reason,
|
|
786
|
-
quarantined_at: new Date().toISOString()
|
|
787
|
-
};
|
|
788
|
-
const duplicate = findNearDuplicate(record.lesson, existing, 0.6);
|
|
789
|
-
if (duplicate?.unactionable_reason === reason) {
|
|
790
|
-
duplicate.quarantined_at = record.quarantined_at;
|
|
791
|
-
duplicate.updated_at = record.updated_at;
|
|
792
|
-
return existing;
|
|
793
|
-
}
|
|
794
|
-
const next = [...existing, record];
|
|
795
|
-
return next.length > 200 ? next.slice(-200) : next;
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
async function quarantineEntry(directory, entryId, reason, reportedBy) {
|
|
799
|
-
if (!directory || directory.includes("..")) {
|
|
800
|
-
warn("[knowledge-validator] quarantineEntry: directory traversal attempt blocked");
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
if (!entryId || entryId.includes("\x00") || entryId.includes(`
|
|
804
|
-
`)) {
|
|
805
|
-
warn("[knowledge-validator] quarantineEntry: invalid entryId rejected");
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
const validReportedBy = ["architect", "user", "auto"];
|
|
809
|
-
if (!validReportedBy.includes(reportedBy)) {
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
const sanitizedReason = reason.slice(0, 500).replace(/[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f\x0d]/g, "");
|
|
813
|
-
const knowledgePath = resolveSwarmKnowledgePath(directory);
|
|
814
|
-
const quarantinePath = path2.join(resolveKnowledgeStoreDir(directory), "knowledge-quarantined.jsonl");
|
|
815
|
-
const rejectedPath = resolveSwarmRejectedPath(directory);
|
|
816
|
-
const swarmDir = resolveKnowledgeStoreDir(directory);
|
|
817
|
-
await mkdir2(swarmDir, { recursive: true });
|
|
818
|
-
let release;
|
|
819
|
-
try {
|
|
820
|
-
release = await import_proper_lockfile2.default.lock(swarmDir, {
|
|
821
|
-
retries: { retries: 5, minTimeout: 100, maxTimeout: 500 },
|
|
822
|
-
stale: 5000
|
|
823
|
-
});
|
|
824
|
-
const entries = await readKnowledge(knowledgePath);
|
|
825
|
-
const entry = entries.find((e) => e.id === entryId);
|
|
826
|
-
if (!entry) {
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
const remaining = entries.filter((e) => e.id !== entryId);
|
|
830
|
-
const quarantined = {
|
|
831
|
-
...entry,
|
|
832
|
-
status: "quarantined",
|
|
833
|
-
original_status: entry.status,
|
|
834
|
-
quarantine_reason: sanitizedReason,
|
|
835
|
-
quarantined_at: new Date().toISOString(),
|
|
836
|
-
reported_by: reportedBy
|
|
837
|
-
};
|
|
838
|
-
const jsonlContent = remaining.length > 0 ? `${remaining.map((e) => JSON.stringify(e)).join(`
|
|
839
|
-
`)}
|
|
840
|
-
` : "";
|
|
841
|
-
await atomicWriteFile(knowledgePath, jsonlContent);
|
|
842
|
-
await appendFile2(quarantinePath, `${JSON.stringify(quarantined)}
|
|
843
|
-
`, "utf-8");
|
|
844
|
-
const quarantinedEntries = await readKnowledge(quarantinePath);
|
|
845
|
-
if (quarantinedEntries.length > 100) {
|
|
846
|
-
const trimmed = quarantinedEntries.slice(-100);
|
|
847
|
-
const capContent = trimmed.length > 0 ? `${trimmed.map((e) => JSON.stringify(e)).join(`
|
|
848
|
-
`)}
|
|
849
|
-
` : "";
|
|
850
|
-
await atomicWriteFile(quarantinePath, capContent);
|
|
851
|
-
}
|
|
852
|
-
const rejectedRecord = {
|
|
853
|
-
id: entryId,
|
|
854
|
-
lesson: entry.lesson,
|
|
855
|
-
rejection_reason: sanitizedReason,
|
|
856
|
-
rejected_at: new Date().toISOString(),
|
|
857
|
-
rejection_layer: 3
|
|
858
|
-
};
|
|
859
|
-
await appendFile2(rejectedPath, `${JSON.stringify(rejectedRecord)}
|
|
860
|
-
`, "utf-8");
|
|
861
|
-
} finally {
|
|
862
|
-
if (release) {
|
|
863
|
-
await release();
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
async function restoreEntry(directory, entryId) {
|
|
868
|
-
if (!directory || directory.includes("..")) {
|
|
869
|
-
warn("[knowledge-validator] restoreEntry: directory traversal attempt blocked");
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
if (!entryId || entryId.includes("\x00") || entryId.includes(`
|
|
873
|
-
`)) {
|
|
874
|
-
warn("[knowledge-validator] restoreEntry: invalid entryId rejected");
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
const storeDir = resolveKnowledgeStoreDir(directory);
|
|
878
|
-
const knowledgePath = resolveSwarmKnowledgePath(directory);
|
|
879
|
-
const quarantinePath = path2.join(storeDir, "knowledge-quarantined.jsonl");
|
|
880
|
-
const rejectedPath = resolveSwarmRejectedPath(directory);
|
|
881
|
-
const swarmDir = storeDir;
|
|
882
|
-
await mkdir2(swarmDir, { recursive: true });
|
|
883
|
-
let release;
|
|
884
|
-
try {
|
|
885
|
-
release = await import_proper_lockfile2.default.lock(swarmDir, {
|
|
886
|
-
retries: { retries: 5, minTimeout: 100, maxTimeout: 500 },
|
|
887
|
-
stale: 5000
|
|
888
|
-
});
|
|
889
|
-
const quarantinedEntries = await readKnowledge(quarantinePath);
|
|
890
|
-
const entryToRestore = quarantinedEntries.find((e) => e.id === entryId);
|
|
891
|
-
if (!entryToRestore) {
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
const remaining = quarantinedEntries.filter((e) => e.id !== entryId);
|
|
895
|
-
const {
|
|
896
|
-
quarantine_reason,
|
|
897
|
-
quarantined_at,
|
|
898
|
-
reported_by,
|
|
899
|
-
original_status,
|
|
900
|
-
status: _quarantineStatus,
|
|
901
|
-
...rest
|
|
902
|
-
} = entryToRestore;
|
|
903
|
-
const original = { ...rest, status: original_status ?? "candidate" };
|
|
904
|
-
const validation = validateLesson(original.lesson, [], {
|
|
905
|
-
category: original.category,
|
|
906
|
-
scope: original.scope,
|
|
907
|
-
confidence: original.confidence
|
|
908
|
-
});
|
|
909
|
-
if (!validation.valid) {
|
|
910
|
-
warn(`[knowledge-validator] restoreEntry: entry ${entryId} failed re-validation: ${validation.reason}`);
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
const jsonlContent = remaining.length > 0 ? `${remaining.map((e) => JSON.stringify(e)).join(`
|
|
914
|
-
`)}
|
|
915
|
-
` : "";
|
|
916
|
-
await atomicWriteFile(quarantinePath, jsonlContent);
|
|
917
|
-
await appendFile2(knowledgePath, `${JSON.stringify(original)}
|
|
918
|
-
`, "utf-8");
|
|
919
|
-
const rejectedEntries = await readKnowledge(rejectedPath);
|
|
920
|
-
const filtered = rejectedEntries.filter((e) => e.id !== entryId);
|
|
921
|
-
const rejectedContent = filtered.length > 0 ? `${filtered.map((e) => JSON.stringify(e)).join(`
|
|
922
|
-
`)}
|
|
923
|
-
` : "";
|
|
924
|
-
await atomicWriteFile(rejectedPath, rejectedContent);
|
|
925
|
-
} finally {
|
|
926
|
-
if (release) {
|
|
927
|
-
await release();
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// src/services/skill-generator.ts
|
|
26
|
+
import { existsSync as existsSync2, unlinkSync } from "fs";
|
|
27
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename2, unlink, writeFile as writeFile3 } from "fs/promises";
|
|
28
|
+
import * as path3 from "path";
|
|
933
29
|
init_logger();
|
|
934
30
|
|
|
935
31
|
// src/services/skill-changelog.ts
|
|
936
32
|
init_logger();
|
|
937
|
-
import { appendFile
|
|
938
|
-
import * as
|
|
33
|
+
import { appendFile, mkdir, readFile, writeFile } from "fs/promises";
|
|
34
|
+
import * as path from "path";
|
|
939
35
|
var MAX_CHANGELOG_ENTRIES_PER_SKILL = 200;
|
|
940
36
|
function resolveSkillChangelogPath(directory, slug) {
|
|
941
37
|
if (slug.includes("..") || slug.includes("/") || slug.includes("\\")) {
|
|
942
38
|
throw new Error(`Invalid skill slug: ${slug} \u2014 must not contain "..", "/" or "\\"`);
|
|
943
39
|
}
|
|
944
|
-
return
|
|
40
|
+
return path.join(directory, ".swarm", "skill-changelogs", `${slug}.jsonl`);
|
|
945
41
|
}
|
|
946
42
|
async function appendSkillChangelog(directory, slug, entry) {
|
|
947
43
|
const filePath = resolveSkillChangelogPath(directory, slug);
|
|
948
|
-
const dirPath =
|
|
949
|
-
await
|
|
950
|
-
await
|
|
44
|
+
const dirPath = path.dirname(filePath);
|
|
45
|
+
await mkdir(dirPath, { recursive: true });
|
|
46
|
+
await appendFile(filePath, `${JSON.stringify(entry)}
|
|
951
47
|
`, "utf-8");
|
|
952
48
|
try {
|
|
953
|
-
const content = await
|
|
49
|
+
const content = await readFile(filePath, "utf-8");
|
|
954
50
|
const lines = content.split(`
|
|
955
51
|
`).filter((line) => line.trim().length > 0);
|
|
956
52
|
if (lines.length > MAX_CHANGELOG_ENTRIES_PER_SKILL) {
|
|
@@ -966,18 +62,18 @@ async function appendSkillChangelog(directory, slug, entry) {
|
|
|
966
62
|
|
|
967
63
|
// src/services/skill-evaluator.ts
|
|
968
64
|
import { createHash } from "crypto";
|
|
969
|
-
import { existsSync
|
|
65
|
+
import { existsSync } from "fs";
|
|
970
66
|
import {
|
|
971
67
|
lstat,
|
|
972
|
-
mkdir as
|
|
68
|
+
mkdir as mkdir2,
|
|
973
69
|
readdir,
|
|
974
|
-
readFile as
|
|
70
|
+
readFile as readFile2,
|
|
975
71
|
realpath,
|
|
976
72
|
rename,
|
|
977
|
-
stat
|
|
73
|
+
stat,
|
|
978
74
|
writeFile as writeFile2
|
|
979
75
|
} from "fs/promises";
|
|
980
|
-
import * as
|
|
76
|
+
import * as path2 from "path";
|
|
981
77
|
var SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
982
78
|
var MAX_EVAL_FILES = 50;
|
|
983
79
|
var MAX_EVAL_FILE_BYTES = 64 * 1024;
|
|
@@ -1043,15 +139,15 @@ function casesFromParsedJson(parsed, fileLabel) {
|
|
|
1043
139
|
return cases;
|
|
1044
140
|
}
|
|
1045
141
|
function evalRoot(directory, slug) {
|
|
1046
|
-
return
|
|
142
|
+
return path2.join(directory, ".swarm", "skills", "evals", slug);
|
|
1047
143
|
}
|
|
1048
144
|
function isInsidePath(root, target) {
|
|
1049
|
-
const resolvedRoot =
|
|
1050
|
-
const resolvedTarget =
|
|
1051
|
-
return resolvedTarget === resolvedRoot || resolvedTarget.startsWith(resolvedRoot +
|
|
145
|
+
const resolvedRoot = path2.resolve(root);
|
|
146
|
+
const resolvedTarget = path2.resolve(target);
|
|
147
|
+
return resolvedTarget === resolvedRoot || resolvedTarget.startsWith(resolvedRoot + path2.sep);
|
|
1052
148
|
}
|
|
1053
149
|
function rejectedEditsPath(directory) {
|
|
1054
|
-
return
|
|
150
|
+
return path2.join(directory, ".swarm", "skills", "rejected-edits.jsonl");
|
|
1055
151
|
}
|
|
1056
152
|
async function loadEvalSet(directory, slug) {
|
|
1057
153
|
if (!isValidSlug(slug)) {
|
|
@@ -1063,7 +159,7 @@ async function loadEvalSet(directory, slug) {
|
|
|
1063
159
|
};
|
|
1064
160
|
}
|
|
1065
161
|
const root = evalRoot(directory, slug);
|
|
1066
|
-
if (!
|
|
162
|
+
if (!existsSync(root)) {
|
|
1067
163
|
return { status: "missing", files: [], cases: [] };
|
|
1068
164
|
}
|
|
1069
165
|
let realRoot;
|
|
@@ -1104,9 +200,9 @@ async function loadEvalSet(directory, slug) {
|
|
|
1104
200
|
const cases = [];
|
|
1105
201
|
const fileLabels = [];
|
|
1106
202
|
for (const file of files) {
|
|
1107
|
-
const fullPath =
|
|
1108
|
-
const resolved =
|
|
1109
|
-
const resolvedRoot =
|
|
203
|
+
const fullPath = path2.join(root, file);
|
|
204
|
+
const resolved = path2.resolve(fullPath);
|
|
205
|
+
const resolvedRoot = path2.resolve(root);
|
|
1110
206
|
if (!isInsidePath(resolvedRoot, resolved)) {
|
|
1111
207
|
return {
|
|
1112
208
|
status: "invalid",
|
|
@@ -1130,7 +226,7 @@ async function loadEvalSet(directory, slug) {
|
|
|
1130
226
|
};
|
|
1131
227
|
}
|
|
1132
228
|
readPath = realFile;
|
|
1133
|
-
info = await
|
|
229
|
+
info = await stat(realFile);
|
|
1134
230
|
} else {
|
|
1135
231
|
if (!linkInfo.isFile())
|
|
1136
232
|
continue;
|
|
@@ -1156,7 +252,7 @@ async function loadEvalSet(directory, slug) {
|
|
|
1156
252
|
}
|
|
1157
253
|
let parsed;
|
|
1158
254
|
try {
|
|
1159
|
-
parsed = JSON.parse(await
|
|
255
|
+
parsed = JSON.parse(await readFile2(readPath, "utf-8"));
|
|
1160
256
|
} catch (err) {
|
|
1161
257
|
return {
|
|
1162
258
|
status: "invalid",
|
|
@@ -1165,7 +261,7 @@ async function loadEvalSet(directory, slug) {
|
|
|
1165
261
|
reason: `eval file invalid JSON: ${file}: ${err instanceof Error ? err.message : String(err)}`
|
|
1166
262
|
};
|
|
1167
263
|
}
|
|
1168
|
-
fileLabels.push(
|
|
264
|
+
fileLabels.push(path2.relative(directory, fullPath).replace(/\\/g, "/"));
|
|
1169
265
|
cases.push(...casesFromParsedJson(parsed, file));
|
|
1170
266
|
if (cases.length >= MAX_EVAL_CASES)
|
|
1171
267
|
break;
|
|
@@ -1283,7 +379,7 @@ async function evaluateSkillChange(req) {
|
|
|
1283
379
|
};
|
|
1284
380
|
}
|
|
1285
381
|
async function atomicWrite(filePath, content) {
|
|
1286
|
-
await
|
|
382
|
+
await mkdir2(path2.dirname(filePath), { recursive: true });
|
|
1287
383
|
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
1288
384
|
await writeFile2(tmp, content, "utf-8");
|
|
1289
385
|
await rename(tmp, filePath);
|
|
@@ -1306,7 +402,7 @@ async function appendRejectedSkillEdit(req, evaluation) {
|
|
|
1306
402
|
};
|
|
1307
403
|
let prior = [];
|
|
1308
404
|
try {
|
|
1309
|
-
prior = (await
|
|
405
|
+
prior = (await readFile2(filePath, "utf-8")).split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
1310
406
|
} catch {
|
|
1311
407
|
prior = [];
|
|
1312
408
|
}
|
|
@@ -1323,7 +419,7 @@ async function isRejectedSkillContent(directory, slug, content) {
|
|
|
1323
419
|
const normalizedHash = hashNormalizedContent(content);
|
|
1324
420
|
let raw = "";
|
|
1325
421
|
try {
|
|
1326
|
-
raw = await
|
|
422
|
+
raw = await readFile2(rejectedEditsPath(directory), "utf-8");
|
|
1327
423
|
} catch {
|
|
1328
424
|
return false;
|
|
1329
425
|
}
|
|
@@ -1360,10 +456,10 @@ function isValidSlug2(slug) {
|
|
|
1360
456
|
return SLUG_PATTERN2.test(slug);
|
|
1361
457
|
}
|
|
1362
458
|
function proposalPath(directory, slug) {
|
|
1363
|
-
return
|
|
459
|
+
return path3.join(directory, ".swarm", "skills", "proposals", `${slug}.md`);
|
|
1364
460
|
}
|
|
1365
461
|
function activePath(directory, slug) {
|
|
1366
|
-
return
|
|
462
|
+
return path3.join(directory, ".opencode", "skills", "generated", slug, "SKILL.md");
|
|
1367
463
|
}
|
|
1368
464
|
function activeRepoRelativePath(slug) {
|
|
1369
465
|
return `.opencode/skills/generated/${slug}/SKILL.md`;
|
|
@@ -1375,7 +471,7 @@ var HIGH_PRIORITY_SKILL_MIN_CONFIDENCE = 0.6;
|
|
|
1375
471
|
async function selectCandidateEntries(directory, opts) {
|
|
1376
472
|
const swarm = await readKnowledge(resolveSwarmKnowledgePath(directory));
|
|
1377
473
|
const hivePath = resolveHiveKnowledgePath();
|
|
1378
|
-
const hive =
|
|
474
|
+
const hive = existsSync2(hivePath) ? await readKnowledge(hivePath) : [];
|
|
1379
475
|
const all = [...swarm, ...hive];
|
|
1380
476
|
const counterRollups = await readKnowledgeCounterRollups(directory);
|
|
1381
477
|
const selected = [];
|
|
@@ -1588,7 +684,7 @@ function escapeMarkdown(s) {
|
|
|
1588
684
|
return s.replace(/[\r\n]+/g, " ").slice(0, 280);
|
|
1589
685
|
}
|
|
1590
686
|
async function atomicWrite2(p, content) {
|
|
1591
|
-
await
|
|
687
|
+
await mkdir3(path3.dirname(p), { recursive: true });
|
|
1592
688
|
const tmp = `${p}.tmp-${process.pid}-${Date.now()}`;
|
|
1593
689
|
await writeFile3(tmp, content, "utf-8");
|
|
1594
690
|
await rename2(tmp, p);
|
|
@@ -1605,7 +701,7 @@ async function generateSkills(req) {
|
|
|
1605
701
|
const idSet = new Set(req.sourceKnowledgeIds);
|
|
1606
702
|
const swarm = await readKnowledge(resolveSwarmKnowledgePath(req.directory));
|
|
1607
703
|
const hivePath = resolveHiveKnowledgePath();
|
|
1608
|
-
const hive =
|
|
704
|
+
const hive = existsSync2(hivePath) ? await readKnowledge(hivePath) : [];
|
|
1609
705
|
const rollups = await readKnowledgeCounterRollups(req.directory);
|
|
1610
706
|
pool = [...swarm, ...hive].filter((e) => idSet.has(e.id) && e.status !== "archived").map((e) => ({
|
|
1611
707
|
...e,
|
|
@@ -1637,7 +733,7 @@ async function generateSkills(req) {
|
|
|
1637
733
|
continue;
|
|
1638
734
|
}
|
|
1639
735
|
const targetPath = req.mode === "active" ? activePath(req.directory, cluster.slug) : proposalPath(req.directory, cluster.slug);
|
|
1640
|
-
const repoRel =
|
|
736
|
+
const repoRel = path3.relative(req.directory, targetPath).replace(/\\/g, "/");
|
|
1641
737
|
if (!validateSkillPath(repoRel)) {
|
|
1642
738
|
result.skipped.push({
|
|
1643
739
|
slug: cluster.slug,
|
|
@@ -1646,8 +742,8 @@ async function generateSkills(req) {
|
|
|
1646
742
|
continue;
|
|
1647
743
|
}
|
|
1648
744
|
let preserved = false;
|
|
1649
|
-
if (req.mode === "active" &&
|
|
1650
|
-
const existing = await
|
|
745
|
+
if (req.mode === "active" && existsSync2(targetPath) && !req.force) {
|
|
746
|
+
const existing = await readFile3(targetPath, "utf-8");
|
|
1651
747
|
if (!existing.includes("generated by opencode-swarm skill-generator")) {
|
|
1652
748
|
preserved = true;
|
|
1653
749
|
result.skipped.push({
|
|
@@ -1669,9 +765,9 @@ async function generateSkills(req) {
|
|
|
1669
765
|
if (req.evaluate) {
|
|
1670
766
|
let incumbentContent;
|
|
1671
767
|
const existingActivePath = activePath(req.directory, cluster.slug);
|
|
1672
|
-
if (
|
|
768
|
+
if (existsSync2(existingActivePath)) {
|
|
1673
769
|
try {
|
|
1674
|
-
incumbentContent = await
|
|
770
|
+
incumbentContent = await readFile3(existingActivePath, "utf-8");
|
|
1675
771
|
} catch {
|
|
1676
772
|
incumbentContent = undefined;
|
|
1677
773
|
}
|
|
@@ -1791,7 +887,7 @@ async function stampSourceEntries(directory, slug, ids) {
|
|
|
1791
887
|
await rewriteKnowledge(swarmPath, swarm);
|
|
1792
888
|
stamped.push(...found);
|
|
1793
889
|
const hivePath = resolveHiveKnowledgePath();
|
|
1794
|
-
if (!
|
|
890
|
+
if (!existsSync2(hivePath)) {
|
|
1795
891
|
for (const id of ids) {
|
|
1796
892
|
if (!found.has(id))
|
|
1797
893
|
missing.push(id);
|
|
@@ -1936,7 +1032,7 @@ async function activateProposal(directory, slug, force = false, options = {}) {
|
|
|
1936
1032
|
}
|
|
1937
1033
|
const from = proposalPath(directory, cleanSlug);
|
|
1938
1034
|
const to = activePath(directory, cleanSlug);
|
|
1939
|
-
if (!
|
|
1035
|
+
if (!existsSync2(from)) {
|
|
1940
1036
|
return {
|
|
1941
1037
|
activated: false,
|
|
1942
1038
|
from,
|
|
@@ -1944,8 +1040,8 @@ async function activateProposal(directory, slug, force = false, options = {}) {
|
|
|
1944
1040
|
reason: `proposal not found: ${from}`
|
|
1945
1041
|
};
|
|
1946
1042
|
}
|
|
1947
|
-
if (
|
|
1948
|
-
const existing = await
|
|
1043
|
+
if (existsSync2(to) && !force) {
|
|
1044
|
+
const existing = await readFile3(to, "utf-8");
|
|
1949
1045
|
if (!existing.includes("generated by opencode-swarm skill-generator")) {
|
|
1950
1046
|
return {
|
|
1951
1047
|
activated: false,
|
|
@@ -1957,7 +1053,7 @@ async function activateProposal(directory, slug, force = false, options = {}) {
|
|
|
1957
1053
|
}
|
|
1958
1054
|
let proposalContent;
|
|
1959
1055
|
try {
|
|
1960
|
-
proposalContent = await
|
|
1056
|
+
proposalContent = await readFile3(from, "utf-8");
|
|
1961
1057
|
} catch (readErr) {
|
|
1962
1058
|
return {
|
|
1963
1059
|
activated: false,
|
|
@@ -1978,9 +1074,9 @@ async function activateProposal(directory, slug, force = false, options = {}) {
|
|
|
1978
1074
|
let evaluation;
|
|
1979
1075
|
if (options.evaluate) {
|
|
1980
1076
|
let incumbentContent;
|
|
1981
|
-
if (
|
|
1077
|
+
if (existsSync2(to)) {
|
|
1982
1078
|
try {
|
|
1983
|
-
incumbentContent = await
|
|
1079
|
+
incumbentContent = await readFile3(to, "utf-8");
|
|
1984
1080
|
} catch {
|
|
1985
1081
|
incumbentContent = undefined;
|
|
1986
1082
|
}
|
|
@@ -2046,24 +1142,24 @@ async function activateProposal(directory, slug, force = false, options = {}) {
|
|
|
2046
1142
|
}
|
|
2047
1143
|
}
|
|
2048
1144
|
async function findSkillsBySourceKnowledgeId(directory, sourceId) {
|
|
2049
|
-
const activeDir =
|
|
1145
|
+
const activeDir = path3.join(directory, ".opencode", "skills", "generated");
|
|
2050
1146
|
const fs = await import("fs/promises");
|
|
2051
|
-
if (!
|
|
1147
|
+
if (!existsSync2(activeDir))
|
|
2052
1148
|
return [];
|
|
2053
1149
|
const entries = await fs.readdir(activeDir, { withFileTypes: true });
|
|
2054
1150
|
const matches = [];
|
|
2055
1151
|
for (const e of entries) {
|
|
2056
1152
|
if (!e.isDirectory())
|
|
2057
1153
|
continue;
|
|
2058
|
-
const skillDir =
|
|
2059
|
-
const retiredMarker =
|
|
2060
|
-
if (
|
|
1154
|
+
const skillDir = path3.join(activeDir, e.name);
|
|
1155
|
+
const retiredMarker = path3.join(skillDir, "retired.marker");
|
|
1156
|
+
if (existsSync2(retiredMarker))
|
|
2061
1157
|
continue;
|
|
2062
|
-
const staleMarker =
|
|
2063
|
-
if (
|
|
1158
|
+
const staleMarker = path3.join(skillDir, "stale.marker");
|
|
1159
|
+
if (existsSync2(staleMarker))
|
|
2064
1160
|
continue;
|
|
2065
|
-
const skillPath =
|
|
2066
|
-
if (!
|
|
1161
|
+
const skillPath = path3.join(skillDir, "SKILL.md");
|
|
1162
|
+
if (!existsSync2(skillPath))
|
|
2067
1163
|
continue;
|
|
2068
1164
|
let content;
|
|
2069
1165
|
try {
|
|
@@ -2079,8 +1175,8 @@ async function findSkillsBySourceKnowledgeId(directory, sourceId) {
|
|
|
2079
1175
|
return matches;
|
|
2080
1176
|
}
|
|
2081
1177
|
async function findStaleSkillsBySourceKnowledgeId(directory, archivedIds) {
|
|
2082
|
-
const activeDir =
|
|
2083
|
-
if (!
|
|
1178
|
+
const activeDir = path3.join(directory, ".opencode", "skills", "generated");
|
|
1179
|
+
if (!existsSync2(activeDir))
|
|
2084
1180
|
return [];
|
|
2085
1181
|
const fs = await import("fs/promises");
|
|
2086
1182
|
const entries = await fs.readdir(activeDir, { withFileTypes: true });
|
|
@@ -2088,15 +1184,15 @@ async function findStaleSkillsBySourceKnowledgeId(directory, archivedIds) {
|
|
|
2088
1184
|
for (const e of entries) {
|
|
2089
1185
|
if (!e.isDirectory())
|
|
2090
1186
|
continue;
|
|
2091
|
-
const skillDir =
|
|
2092
|
-
const retiredMarker =
|
|
2093
|
-
if (
|
|
1187
|
+
const skillDir = path3.join(activeDir, e.name);
|
|
1188
|
+
const retiredMarker = path3.join(skillDir, "retired.marker");
|
|
1189
|
+
if (existsSync2(retiredMarker))
|
|
2094
1190
|
continue;
|
|
2095
|
-
const staleMarker =
|
|
2096
|
-
if (!
|
|
1191
|
+
const staleMarker = path3.join(skillDir, "stale.marker");
|
|
1192
|
+
if (!existsSync2(staleMarker))
|
|
2097
1193
|
continue;
|
|
2098
|
-
const skillPath =
|
|
2099
|
-
if (!
|
|
1194
|
+
const skillPath = path3.join(skillDir, "SKILL.md");
|
|
1195
|
+
if (!existsSync2(skillPath))
|
|
2100
1196
|
continue;
|
|
2101
1197
|
let content;
|
|
2102
1198
|
try {
|
|
@@ -2121,10 +1217,10 @@ async function listSkills(directory) {
|
|
|
2121
1217
|
active: [],
|
|
2122
1218
|
stale: []
|
|
2123
1219
|
};
|
|
2124
|
-
const proposalsDir =
|
|
2125
|
-
const activeDir =
|
|
1220
|
+
const proposalsDir = path3.join(directory, ".swarm", "skills", "proposals");
|
|
1221
|
+
const activeDir = path3.join(directory, ".opencode", "skills", "generated");
|
|
2126
1222
|
const fs = await import("fs/promises");
|
|
2127
|
-
if (
|
|
1223
|
+
if (existsSync2(proposalsDir)) {
|
|
2128
1224
|
const entries = await fs.readdir(proposalsDir);
|
|
2129
1225
|
for (const f of entries) {
|
|
2130
1226
|
if (!f.endsWith(".md"))
|
|
@@ -2132,20 +1228,20 @@ async function listSkills(directory) {
|
|
|
2132
1228
|
const slug = f.replace(/\.md$/, "");
|
|
2133
1229
|
result.proposals.push({
|
|
2134
1230
|
slug,
|
|
2135
|
-
path:
|
|
1231
|
+
path: path3.join(proposalsDir, f)
|
|
2136
1232
|
});
|
|
2137
1233
|
}
|
|
2138
1234
|
}
|
|
2139
|
-
if (
|
|
1235
|
+
if (existsSync2(activeDir)) {
|
|
2140
1236
|
const entries = await fs.readdir(activeDir, { withFileTypes: true });
|
|
2141
1237
|
for (const e of entries) {
|
|
2142
1238
|
if (!e.isDirectory())
|
|
2143
1239
|
continue;
|
|
2144
|
-
const retiredMarker =
|
|
2145
|
-
if (
|
|
1240
|
+
const retiredMarker = path3.join(activeDir, e.name, "retired.marker");
|
|
1241
|
+
if (existsSync2(retiredMarker))
|
|
2146
1242
|
continue;
|
|
2147
|
-
const staleMarker =
|
|
2148
|
-
if (
|
|
1243
|
+
const staleMarker = path3.join(activeDir, e.name, "stale.marker");
|
|
1244
|
+
if (existsSync2(staleMarker)) {
|
|
2149
1245
|
let reason = "stale";
|
|
2150
1246
|
try {
|
|
2151
1247
|
const content = await fs.readFile(staleMarker, "utf-8");
|
|
@@ -2154,8 +1250,8 @@ async function listSkills(directory) {
|
|
|
2154
1250
|
result.stale.push({ slug: e.name, reason });
|
|
2155
1251
|
continue;
|
|
2156
1252
|
}
|
|
2157
|
-
const skillPath =
|
|
2158
|
-
if (
|
|
1253
|
+
const skillPath = path3.join(activeDir, e.name, "SKILL.md");
|
|
1254
|
+
if (existsSync2(skillPath)) {
|
|
2159
1255
|
result.active.push({
|
|
2160
1256
|
slug: e.name,
|
|
2161
1257
|
path: skillPath
|
|
@@ -2179,7 +1275,7 @@ async function autoApplyProposals(directory, llmDelegate) {
|
|
|
2179
1275
|
}
|
|
2180
1276
|
let content;
|
|
2181
1277
|
try {
|
|
2182
|
-
content = await
|
|
1278
|
+
content = await readFile3(proposal.path, "utf-8");
|
|
2183
1279
|
} catch {
|
|
2184
1280
|
result.skipped.push(proposal.slug);
|
|
2185
1281
|
continue;
|
|
@@ -2238,14 +1334,14 @@ async function inspectSkill(directory, slug, prefer = "auto") {
|
|
|
2238
1334
|
if (prefer === "proposal" || prefer === "auto")
|
|
2239
1335
|
candidates.push({ p: proposalPath(directory, cleanSlug), m: "draft" });
|
|
2240
1336
|
for (const c of candidates) {
|
|
2241
|
-
if (
|
|
2242
|
-
const content = await
|
|
1337
|
+
if (existsSync2(c.p)) {
|
|
1338
|
+
const content = await readFile3(c.p, "utf-8");
|
|
2243
1339
|
const result = { found: true, path: c.p, content, mode: c.m };
|
|
2244
1340
|
const fm = parseDraftFrontmatter(content);
|
|
2245
1341
|
if (fm && fm.sourceKnowledgeIds.length > 0) {
|
|
2246
1342
|
const swarm = await readKnowledge(resolveSwarmKnowledgePath(directory));
|
|
2247
1343
|
const hivePath = resolveHiveKnowledgePath();
|
|
2248
|
-
const hive =
|
|
1344
|
+
const hive = existsSync2(hivePath) ? await readKnowledge(hivePath) : [];
|
|
2249
1345
|
const allEntries = [...swarm, ...hive];
|
|
2250
1346
|
const entryMap = new Map(allEntries.map((e) => [e.id, e]));
|
|
2251
1347
|
result.source_knowledge_status = fm.sourceKnowledgeIds.map((id) => {
|
|
@@ -2259,11 +1355,11 @@ async function inspectSkill(directory, slug, prefer = "auto") {
|
|
|
2259
1355
|
});
|
|
2260
1356
|
}
|
|
2261
1357
|
if (c.m === "active") {
|
|
2262
|
-
const skillDir =
|
|
2263
|
-
const staleMarker =
|
|
2264
|
-
if (
|
|
1358
|
+
const skillDir = path3.join(directory, ".opencode", "skills", "generated", cleanSlug);
|
|
1359
|
+
const staleMarker = path3.join(skillDir, "stale.marker");
|
|
1360
|
+
if (existsSync2(staleMarker)) {
|
|
2265
1361
|
try {
|
|
2266
|
-
result.stale_reason = await
|
|
1362
|
+
result.stale_reason = await readFile3(staleMarker, "utf-8");
|
|
2267
1363
|
} catch {}
|
|
2268
1364
|
}
|
|
2269
1365
|
}
|
|
@@ -2278,26 +1374,26 @@ async function retireSkill(directory, slug, reason) {
|
|
|
2278
1374
|
return {
|
|
2279
1375
|
retired: false,
|
|
2280
1376
|
path: activePath(directory, cleanSlug),
|
|
2281
|
-
markerPath:
|
|
1377
|
+
markerPath: path3.join(directory, ".opencode", "skills", "generated", cleanSlug, "retired.marker"),
|
|
2282
1378
|
reason: "invalid slug"
|
|
2283
1379
|
};
|
|
2284
1380
|
}
|
|
2285
1381
|
const skillPath = activePath(directory, cleanSlug);
|
|
2286
|
-
if (!
|
|
1382
|
+
if (!existsSync2(skillPath)) {
|
|
2287
1383
|
return {
|
|
2288
1384
|
retired: false,
|
|
2289
1385
|
path: skillPath,
|
|
2290
|
-
markerPath:
|
|
1386
|
+
markerPath: path3.join(directory, ".opencode", "skills", "generated", cleanSlug, "retired.marker"),
|
|
2291
1387
|
reason: "active skill not found"
|
|
2292
1388
|
};
|
|
2293
1389
|
}
|
|
2294
|
-
const markerDir =
|
|
2295
|
-
const markerPath =
|
|
1390
|
+
const markerDir = path3.join(directory, ".opencode", "skills", "generated", cleanSlug);
|
|
1391
|
+
const markerPath = path3.join(markerDir, "retired.marker");
|
|
2296
1392
|
const markerContent = JSON.stringify({
|
|
2297
1393
|
retiredAt: new Date().toISOString(),
|
|
2298
1394
|
reason: reason ?? "manual_retire"
|
|
2299
1395
|
});
|
|
2300
|
-
await
|
|
1396
|
+
await mkdir3(markerDir, { recursive: true });
|
|
2301
1397
|
await writeFile3(markerPath, markerContent, "utf-8");
|
|
2302
1398
|
return {
|
|
2303
1399
|
retired: true,
|
|
@@ -2307,11 +1403,11 @@ async function retireSkill(directory, slug, reason) {
|
|
|
2307
1403
|
};
|
|
2308
1404
|
}
|
|
2309
1405
|
async function markSkillStale(skillDir, reason) {
|
|
2310
|
-
await
|
|
2311
|
-
await writeFile3(
|
|
1406
|
+
await mkdir3(skillDir, { recursive: true });
|
|
1407
|
+
await writeFile3(path3.join(skillDir, "stale.marker"), reason, "utf-8");
|
|
2312
1408
|
}
|
|
2313
1409
|
async function clearSkillStale(skillDir) {
|
|
2314
|
-
const markerPath =
|
|
1410
|
+
const markerPath = path3.join(skillDir, "stale.marker");
|
|
2315
1411
|
try {
|
|
2316
1412
|
await unlink(markerPath);
|
|
2317
1413
|
} catch (err) {
|
|
@@ -2323,10 +1419,10 @@ async function clearSkillStale(skillDir) {
|
|
|
2323
1419
|
}
|
|
2324
1420
|
async function retireOrMarkStale(directory, skillDir, archivedKnowledgeIds) {
|
|
2325
1421
|
const fs = await import("fs/promises");
|
|
2326
|
-
const skillPath =
|
|
2327
|
-
if (!
|
|
1422
|
+
const skillPath = path3.join(skillDir, "SKILL.md");
|
|
1423
|
+
if (!existsSync2(skillPath)) {
|
|
2328
1424
|
await markSkillStale(skillDir, "source knowledge archived, SKILL.md missing");
|
|
2329
|
-
const slug2 =
|
|
1425
|
+
const slug2 = path3.basename(skillDir);
|
|
2330
1426
|
return { action: "stale", slug: slug2, skillDir };
|
|
2331
1427
|
}
|
|
2332
1428
|
let content;
|
|
@@ -2334,18 +1430,18 @@ async function retireOrMarkStale(directory, skillDir, archivedKnowledgeIds) {
|
|
|
2334
1430
|
content = await fs.readFile(skillPath, "utf-8");
|
|
2335
1431
|
} catch {
|
|
2336
1432
|
await markSkillStale(skillDir, "source knowledge archived, SKILL.md unreadable");
|
|
2337
|
-
const slug2 =
|
|
1433
|
+
const slug2 = path3.basename(skillDir);
|
|
2338
1434
|
return { action: "stale", slug: slug2, skillDir };
|
|
2339
1435
|
}
|
|
2340
1436
|
const fm = parseDraftFrontmatter(content);
|
|
2341
1437
|
const sourceIds = fm?.sourceKnowledgeIds ?? [];
|
|
2342
1438
|
if (sourceIds.length === 0) {
|
|
2343
1439
|
await markSkillStale(skillDir, "source knowledge archived, no source_knowledge_ids in frontmatter");
|
|
2344
|
-
const slug2 =
|
|
1440
|
+
const slug2 = path3.basename(skillDir);
|
|
2345
1441
|
return { action: "stale", slug: slug2, skillDir };
|
|
2346
1442
|
}
|
|
2347
1443
|
const allArchived = sourceIds.every((id) => archivedKnowledgeIds.has(id));
|
|
2348
|
-
const slug =
|
|
1444
|
+
const slug = path3.basename(skillDir);
|
|
2349
1445
|
if (allArchived) {
|
|
2350
1446
|
await retireSkill(directory, slug, "all source knowledge entries archived or deleted");
|
|
2351
1447
|
return { action: "retire", slug, skillDir };
|
|
@@ -2365,7 +1461,7 @@ async function regenerateSkill(directory, slug, options = {}) {
|
|
|
2365
1461
|
};
|
|
2366
1462
|
}
|
|
2367
1463
|
const skillPath = activePath(directory, cleanSlug);
|
|
2368
|
-
if (!
|
|
1464
|
+
if (!existsSync2(skillPath)) {
|
|
2369
1465
|
return {
|
|
2370
1466
|
regenerated: false,
|
|
2371
1467
|
path: skillPath,
|
|
@@ -2375,7 +1471,7 @@ async function regenerateSkill(directory, slug, options = {}) {
|
|
|
2375
1471
|
}
|
|
2376
1472
|
let existingContent;
|
|
2377
1473
|
try {
|
|
2378
|
-
existingContent = await
|
|
1474
|
+
existingContent = await readFile3(skillPath, "utf-8");
|
|
2379
1475
|
} catch (err) {
|
|
2380
1476
|
return {
|
|
2381
1477
|
regenerated: false,
|
|
@@ -2390,7 +1486,7 @@ async function regenerateSkill(directory, slug, options = {}) {
|
|
|
2390
1486
|
try {
|
|
2391
1487
|
const swarm = await readKnowledge(resolveSwarmKnowledgePath(directory));
|
|
2392
1488
|
const hivePath = resolveHiveKnowledgePath();
|
|
2393
|
-
const hive =
|
|
1489
|
+
const hive = existsSync2(hivePath) ? await readKnowledge(hivePath) : [];
|
|
2394
1490
|
const all = [...swarm, ...hive];
|
|
2395
1491
|
const idSet = new Set(fm.sourceKnowledgeIds);
|
|
2396
1492
|
matchedEntries = all.filter((e) => idSet.has(e.id));
|
|
@@ -2513,7 +1609,7 @@ async function regenerateSkill(directory, slug, options = {}) {
|
|
|
2513
1609
|
try {
|
|
2514
1610
|
await atomicWrite2(skillPath, content);
|
|
2515
1611
|
await stampSourceEntries(directory, cleanSlug, matchedEntries.map((e) => e.id));
|
|
2516
|
-
await clearSkillStale(
|
|
1612
|
+
await clearSkillStale(path3.dirname(activePath(directory, cleanSlug)));
|
|
2517
1613
|
} catch (writeErr) {
|
|
2518
1614
|
return {
|
|
2519
1615
|
regenerated: false,
|
|
@@ -2561,4 +1657,4 @@ var _internals = {
|
|
|
2561
1657
|
unlinkSync
|
|
2562
1658
|
};
|
|
2563
1659
|
|
|
2564
|
-
export {
|
|
1660
|
+
export { appendSkillChangelog, evaluateSkillChange, appendRejectedSkillEdit, sanitizeSlug, isValidSlug2 as isValidSlug, proposalPath, activePath, activeRepoRelativePath, DEFAULT_SKILL_MIN_CONFIDENCE, DEFAULT_SKILL_MIN_CONFIRMATIONS, STRONG_SKILL_OUTCOME_COUNT, HIGH_PRIORITY_SKILL_MIN_CONFIDENCE, selectCandidateEntries, isSkillMaturityEligible, clusterEntries, renderSkillMarkdown, generateSkills, parseDraftFrontmatter, activateProposal, findSkillsBySourceKnowledgeId, findStaleSkillsBySourceKnowledgeId, listSkills, autoApplyProposals, inspectSkill, retireSkill, markSkillStale, clearSkillStale, retireOrMarkStale, regenerateSkill, _internals };
|