opencode-swarm 7.114.7 → 7.114.9
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/ci-fix-monitor/SKILL.md +8 -13
- package/.opencode/skills/commit-pr/SKILL.md +4 -0
- package/.opencode/skills/engineering-conventions/SKILL.md +33 -0
- package/.opencode/skills/execute/SKILL.md +2 -0
- package/.opencode/skills/swarm-ci-monitor/SKILL.md +5 -0
- package/.opencode/skills/swarm-pr-feedback/SKILL.md +2 -0
- package/.opencode/skills/writing-tests/SKILL.md +2 -0
- package/README.md +16 -14
- package/dist/agents/template.d.ts +40 -20
- package/dist/cli/{curator-qshyr2tc.js → curator-5xt89z05.js} +7 -7
- package/dist/cli/{curator-llm-factory-2xgznxyw.js → curator-llm-factory-pnhvf3s3.js} +7 -7
- package/dist/cli/{evidence-summary-service-8g594znj.js → evidence-summary-service-fehwj116.js} +1 -1
- package/dist/cli/{guardrail-explain-4t8fnysh.js → guardrail-explain-7aztgtyh.js} +8 -8
- package/dist/cli/{hive-promoter-5ngbmnrw.js → hive-promoter-q899q2m6.js} +7 -7
- package/dist/cli/{index-z01r5483.js → index-54sevh81.js} +8 -8
- package/dist/cli/{index-7stsmndb.js → index-6f6y2rbp.js} +3 -3
- package/dist/cli/{index-kjbfry6m.js → index-8f2270hc.js} +1 -1
- package/dist/cli/{index-gmjrwz34.js → index-m9m7p49w.js} +1 -1
- package/dist/cli/{index-qrnhvhmg.js → index-n18yy0z1.js} +26 -2
- package/dist/cli/{index-91qesget.js → index-rk6qhyng.js} +2 -2
- package/dist/cli/{index-6ctejje2.js → index-typ408f4.js} +710 -630
- package/dist/cli/{index-vjvdvjd3.js → index-x12hvfpf.js} +3 -3
- package/dist/cli/{index-f6y341yk.js → index-yrrqs4b0.js} +82 -21
- package/dist/cli/index.d.ts +34 -0
- package/dist/cli/index.js +105 -28
- package/dist/cli/{knowledge-escalator-qn7ew687.js → knowledge-escalator-5gwp1ar8.js} +3 -3
- package/dist/cli/{knowledge-events-k2xsz5bh.js → knowledge-events-m304swh6.js} +1 -1
- package/dist/cli/{knowledge-store-ksa1dr2z.js → knowledge-store-9babt8rd.js} +1 -1
- package/dist/cli/{knowledge-validator-2knz0d2t.js → knowledge-validator-t9sym59g.js} +4 -2
- package/dist/cli/{skill-generator-w6qd9mde.js → skill-generator-tav44xpm.js} +4 -4
- package/dist/commands/ci-monitor.d.ts +18 -0
- package/dist/commands/registry.d.ts +8 -0
- package/dist/config/skill-mirrors.d.ts +22 -1
- package/dist/hooks/adversarial-detector.d.ts +6 -0
- package/dist/hooks/delegation-gate.d.ts +19 -0
- package/dist/hooks/guardrails/index.d.ts +2 -2
- package/dist/hooks/knowledge-validator.d.ts +22 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +194 -169
- package/dist/plan/ledger.d.ts +83 -20
- package/dist/types/delegation.d.ts +24 -0
- package/package.json +1 -1
- package/dist/graph/graph-builder.d.ts +0 -39
- package/dist/graph/graph-query.d.ts +0 -42
- package/dist/graph/graph-store.d.ts +0 -27
- package/dist/graph/import-extractor.d.ts +0 -44
- package/dist/graph/index.d.ts +0 -16
- package/dist/graph/symbol-extractor.d.ts +0 -17
- package/dist/graph/types.d.ts +0 -84
- package/dist/sandbox/win32/restricted-token-executor.d.ts +0 -9
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
import {
|
|
3
3
|
ALLOWED_SKILL_PATH_PREFIXES,
|
|
4
4
|
validateSkillPath
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-n18yy0z1.js";
|
|
6
6
|
import {
|
|
7
7
|
effectiveRetrievalOutcomes,
|
|
8
8
|
readKnowledgeCounterRollups
|
|
9
|
-
} from "./index-
|
|
9
|
+
} from "./index-8f2270hc.js";
|
|
10
10
|
import {
|
|
11
11
|
computeOutcomeSignal,
|
|
12
12
|
readKnowledge,
|
|
13
13
|
resolveHiveKnowledgePath,
|
|
14
14
|
resolveSwarmKnowledgePath,
|
|
15
15
|
transactKnowledge
|
|
16
|
-
} from "./index-
|
|
16
|
+
} from "./index-rk6qhyng.js";
|
|
17
17
|
import {
|
|
18
18
|
isActiveStatus
|
|
19
19
|
} from "./index-bfwt3abw.js";
|
|
@@ -845,11 +845,16 @@ var init_executor = __esm(() => {
|
|
|
845
845
|
|
|
846
846
|
// src/plan/manager.ts
|
|
847
847
|
import {
|
|
848
|
+
closeSync as closeSync2,
|
|
848
849
|
copyFileSync,
|
|
849
850
|
existsSync as existsSync5,
|
|
850
|
-
|
|
851
|
+
fsyncSync as fsyncSync2,
|
|
852
|
+
mkdirSync as mkdirSync5,
|
|
853
|
+
openSync as openSync2,
|
|
854
|
+
readdirSync as readdirSync2,
|
|
851
855
|
renameSync as renameSync4,
|
|
852
|
-
unlinkSync as unlinkSync3
|
|
856
|
+
unlinkSync as unlinkSync3,
|
|
857
|
+
writeFileSync as writeFileSync6
|
|
853
858
|
} from "fs";
|
|
854
859
|
import * as fsPromises from "fs/promises";
|
|
855
860
|
import * as path9 from "path";
|
|
@@ -2144,6 +2149,16 @@ function getLedgerPath(directory) {
|
|
|
2144
2149
|
function getPlanJsonPath(directory) {
|
|
2145
2150
|
return path8.join(directory, ".swarm", PLAN_JSON_FILENAME);
|
|
2146
2151
|
}
|
|
2152
|
+
function writeFileFsyncedThenRename(tempPath, targetPath, data) {
|
|
2153
|
+
const fd = fs5.openSync(tempPath, "w");
|
|
2154
|
+
try {
|
|
2155
|
+
fs5.writeFileSync(fd, data, "utf8");
|
|
2156
|
+
fs5.fsyncSync(fd);
|
|
2157
|
+
} finally {
|
|
2158
|
+
fs5.closeSync(fd);
|
|
2159
|
+
}
|
|
2160
|
+
fs5.renameSync(tempPath, targetPath);
|
|
2161
|
+
}
|
|
2147
2162
|
function computePlanHash(plan) {
|
|
2148
2163
|
const normalized = {
|
|
2149
2164
|
schema_version: plan.schema_version,
|
|
@@ -2277,8 +2292,7 @@ async function initLedger(directory, planId, initialPlanHash, initialPlan) {
|
|
|
2277
2292
|
const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
|
|
2278
2293
|
const line = `${JSON.stringify(event)}
|
|
2279
2294
|
`;
|
|
2280
|
-
|
|
2281
|
-
fs5.renameSync(tempPath, ledgerPath);
|
|
2295
|
+
writeFileFsyncedThenRename(tempPath, ledgerPath, line);
|
|
2282
2296
|
}
|
|
2283
2297
|
async function appendLedgerEvent(directory, eventInput, options) {
|
|
2284
2298
|
return withEvidenceLock(directory, LEDGER_LOCK_PATH, "plan-ledger", "append-ledger-event", async () => {
|
|
@@ -2305,13 +2319,11 @@ async function appendLedgerEvent(directory, eventInput, options) {
|
|
|
2305
2319
|
const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
|
|
2306
2320
|
const line = `${JSON.stringify(event)}
|
|
2307
2321
|
`;
|
|
2308
|
-
if (fs5.existsSync(ledgerPath)) {
|
|
2309
|
-
const existingContent = fs5.readFileSync(ledgerPath, "utf8");
|
|
2310
|
-
fs5.writeFileSync(tempPath, existingContent + line, "utf8");
|
|
2311
|
-
} else {
|
|
2322
|
+
if (!fs5.existsSync(ledgerPath)) {
|
|
2312
2323
|
throw new Error("Ledger not initialized. Call initLedger() first.");
|
|
2313
2324
|
}
|
|
2314
|
-
fs5.
|
|
2325
|
+
const existingContent = fs5.readFileSync(ledgerPath, "utf8");
|
|
2326
|
+
writeFileFsyncedThenRename(tempPath, ledgerPath, existingContent + line);
|
|
2315
2327
|
return event;
|
|
2316
2328
|
});
|
|
2317
2329
|
}
|
|
@@ -2358,14 +2370,22 @@ async function takeSnapshotEvent(directory, plan, options) {
|
|
|
2358
2370
|
payload: snapshotPayload
|
|
2359
2371
|
}, { planHashAfter: options?.planHashAfter });
|
|
2360
2372
|
}
|
|
2361
|
-
async function replayFromLedger(directory,
|
|
2373
|
+
async function replayFromLedger(directory, options) {
|
|
2374
|
+
const { plan } = await replayFromLedgerWithStatus(directory, options);
|
|
2375
|
+
return plan;
|
|
2376
|
+
}
|
|
2377
|
+
async function replayFromLedgerWithStatus(directory, _options) {
|
|
2362
2378
|
const { events, truncated, badSuffix } = await readLedgerEventsWithIntegrity(directory);
|
|
2363
2379
|
if (events.length === 0) {
|
|
2364
|
-
return null;
|
|
2380
|
+
return { plan: null, truncated, badSuffix };
|
|
2365
2381
|
}
|
|
2366
2382
|
if (truncated && badSuffix !== null) {
|
|
2367
2383
|
await quarantineLedgerSuffix(directory, badSuffix);
|
|
2368
2384
|
}
|
|
2385
|
+
const plan = reconstructPlanFromEvents(directory, events);
|
|
2386
|
+
return { plan, truncated, badSuffix };
|
|
2387
|
+
}
|
|
2388
|
+
function reconstructPlanFromEvents(directory, events) {
|
|
2369
2389
|
const targetPlanId = events[0].plan_id;
|
|
2370
2390
|
const relevantEvents = events.filter((e) => e.plan_id === targetPlanId);
|
|
2371
2391
|
{
|
|
@@ -2531,11 +2551,35 @@ async function readLedgerEventsWithIntegrity(directory) {
|
|
|
2531
2551
|
}
|
|
2532
2552
|
}
|
|
2533
2553
|
async function quarantineLedgerSuffix(directory, badSuffix) {
|
|
2554
|
+
let salvagedCount = 0;
|
|
2555
|
+
for (const line of badSuffix.split(`
|
|
2556
|
+
`)) {
|
|
2557
|
+
if (line.trim() === "")
|
|
2558
|
+
continue;
|
|
2559
|
+
try {
|
|
2560
|
+
JSON.parse(line);
|
|
2561
|
+
salvagedCount++;
|
|
2562
|
+
} catch {}
|
|
2563
|
+
}
|
|
2534
2564
|
try {
|
|
2535
|
-
const
|
|
2565
|
+
const hash = crypto2.createHash("sha256").update(badSuffix, "utf8").digest("hex").slice(0, 12);
|
|
2566
|
+
const swarmDir = path8.join(directory, ".swarm");
|
|
2567
|
+
try {
|
|
2568
|
+
const existing = fs5.readdirSync(swarmDir).filter((name) => name.startsWith("plan-ledger.quarantine.") && name.endsWith(`.${hash}`));
|
|
2569
|
+
for (const name of existing) {
|
|
2570
|
+
const existingPath = path8.join(swarmDir, name);
|
|
2571
|
+
if (fs5.readFileSync(existingPath, "utf8") === badSuffix) {
|
|
2572
|
+
return { path: existingPath, salvagedCount };
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
} catch {}
|
|
2576
|
+
const quarantinePath = path8.join(swarmDir, `plan-ledger.quarantine.${Date.now()}.${hash}`);
|
|
2536
2577
|
fs5.writeFileSync(quarantinePath, badSuffix, "utf8");
|
|
2537
|
-
console.warn(`[ledger] Corrupted suffix quarantined to ${path8.relative(directory, quarantinePath)}`);
|
|
2538
|
-
|
|
2578
|
+
console.warn(`[ledger] Corrupted suffix quarantined to ${path8.relative(directory, quarantinePath)} (salvageable events: ${salvagedCount})`);
|
|
2579
|
+
return { path: quarantinePath, salvagedCount };
|
|
2580
|
+
} catch {
|
|
2581
|
+
return { path: null, salvagedCount };
|
|
2582
|
+
}
|
|
2539
2583
|
}
|
|
2540
2584
|
async function loadLastApprovedPlan(directory, expectedPlanId) {
|
|
2541
2585
|
const events = await readLedgerEvents(directory);
|
|
@@ -2868,7 +2912,15 @@ async function loadPlan(directory, cache) {
|
|
|
2868
2912
|
} else {
|
|
2869
2913
|
warn("[loadPlan] plan.json is stale (hash mismatch with ledger) \u2014 rebuilding from ledger. If this recurs, run /swarm reset-session to clear stale session state.");
|
|
2870
2914
|
try {
|
|
2871
|
-
const rebuilt = await
|
|
2915
|
+
const { plan: rebuilt, truncated } = await replayFromLedgerWithStatus(directory);
|
|
2916
|
+
if (truncated) {
|
|
2917
|
+
const runtimeStale = validated;
|
|
2918
|
+
runtimeStale._ledgerReplayStale = true;
|
|
2919
|
+
runtimeStale._ledgerReplayStaleReason = "Ledger truncated: a malformed line stopped replay before the tail, so the ledger could only be reconstructed up to the corruption. plan.json was PRESERVED (not rolled back to the prefix-only projection) and the corrupted suffix was quarantined to .swarm/plan-ledger.quarantine.*. Verify state, then run /swarm reset-session if this persists.";
|
|
2920
|
+
ledgerStaleWorkspaces.add(resolvedWorkspace);
|
|
2921
|
+
criticalWarn("[loadPlan] Ledger truncated (poison line detected) \u2014 preserving plan.json instead of rolling back to the prefix-only ledger projection. Durable post-poison events remain in plan.json. Corrupted suffix quarantined to .swarm/plan-ledger.quarantine.*. Run /swarm reset-session after verifying state if this persists.");
|
|
2922
|
+
return runtimeStale;
|
|
2923
|
+
}
|
|
2872
2924
|
if (rebuilt) {
|
|
2873
2925
|
await rebuildPlan(directory, rebuilt, {
|
|
2874
2926
|
reason: "ledger_hash_mismatch_recovery"
|
|
@@ -3190,7 +3242,7 @@ async function savePlan(directory, plan, options) {
|
|
|
3190
3242
|
}
|
|
3191
3243
|
const MAX_ARCHIVED_SIBLINGS = 5;
|
|
3192
3244
|
try {
|
|
3193
|
-
const allFiles =
|
|
3245
|
+
const allFiles = readdirSync2(swarmDir2);
|
|
3194
3246
|
const archivedSiblings = allFiles.filter((f) => f.startsWith("plan-ledger.archived-") && f.endsWith(".jsonl")).sort();
|
|
3195
3247
|
if (archivedSiblings.length > MAX_ARCHIVED_SIBLINGS) {
|
|
3196
3248
|
const toRemove = archivedSiblings.slice(0, archivedSiblings.length - MAX_ARCHIVED_SIBLINGS);
|
|
@@ -3407,8 +3459,17 @@ async function rebuildPlan(directory, plan, options) {
|
|
|
3407
3459
|
const swarmDir = path9.join(directory, ".swarm");
|
|
3408
3460
|
const planPath = path9.join(swarmDir, "plan.json");
|
|
3409
3461
|
const mdPath = path9.join(swarmDir, "plan.md");
|
|
3462
|
+
mkdirSync5(swarmDir, { recursive: true });
|
|
3410
3463
|
const tempPlanPath = path9.join(swarmDir, `plan.json.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
|
|
3411
|
-
|
|
3464
|
+
{
|
|
3465
|
+
const fd = openSync2(tempPlanPath, "w");
|
|
3466
|
+
try {
|
|
3467
|
+
writeFileSync6(fd, JSON.stringify(targetPlan, null, 2), "utf8");
|
|
3468
|
+
fsyncSync2(fd);
|
|
3469
|
+
} finally {
|
|
3470
|
+
closeSync2(fd);
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3412
3473
|
renameSync4(tempPlanPath, planPath);
|
|
3413
3474
|
try {
|
|
3414
3475
|
const markerPath = path9.join(swarmDir, ".plan-write-marker");
|
|
@@ -3805,8 +3866,8 @@ function migrateLegacyPlan(planContent, swarmId) {
|
|
|
3805
3866
|
|
|
3806
3867
|
// src/evidence/manager.ts
|
|
3807
3868
|
import {
|
|
3808
|
-
mkdirSync as
|
|
3809
|
-
readdirSync as
|
|
3869
|
+
mkdirSync as mkdirSync6,
|
|
3870
|
+
readdirSync as readdirSync3,
|
|
3810
3871
|
realpathSync,
|
|
3811
3872
|
rmSync,
|
|
3812
3873
|
statSync as statSync2
|
|
@@ -4244,7 +4305,7 @@ async function saveEvidence(directory, taskId, evidence) {
|
|
|
4244
4305
|
if (bundleJson.length > EVIDENCE_MAX_JSON_BYTES) {
|
|
4245
4306
|
throw new Error(`Evidence bundle size (${bundleJson.length} bytes) exceeds maximum (${EVIDENCE_MAX_JSON_BYTES} bytes)`);
|
|
4246
4307
|
}
|
|
4247
|
-
|
|
4308
|
+
mkdirSync6(evidenceDir, { recursive: true });
|
|
4248
4309
|
const tempPath = path10.join(evidenceDir, `evidence.json.tmp.${Date.now()}.${process.pid}`);
|
|
4249
4310
|
try {
|
|
4250
4311
|
await bunWrite(tempPath, bundleJson);
|
|
@@ -4351,7 +4412,7 @@ async function listEvidenceTaskIds(directory) {
|
|
|
4351
4412
|
}
|
|
4352
4413
|
let entries;
|
|
4353
4414
|
try {
|
|
4354
|
-
entries =
|
|
4415
|
+
entries = readdirSync3(evidenceBasePath);
|
|
4355
4416
|
} catch {
|
|
4356
4417
|
return [];
|
|
4357
4418
|
}
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -15,6 +15,40 @@ export declare function isSafeCachePath(p: string): boolean;
|
|
|
15
15
|
* grandparent structure validation to prevent misconfigured nested paths.
|
|
16
16
|
*/
|
|
17
17
|
export declare function isSafeLockFilePath(p: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Safety guard for the `uninstall --clean` deletion of PROMPTS_DIR
|
|
20
|
+
* (`<CONFIG_DIR>/opencode-swarm`, a DIRECTORY removed recursively).
|
|
21
|
+
*
|
|
22
|
+
* This is a SEPARATE guard from isSafeCachePath — it is NOT reused because
|
|
23
|
+
* isSafeCachePath's layer-4 requires parent ∈ {packages, node_modules} and
|
|
24
|
+
* grandparent === 'opencode', which the prompts directory (parent basename is
|
|
25
|
+
* the config dir's basename, typically 'opencode') does not satisfy, so
|
|
26
|
+
* isSafeCachePath would false-REJECT a legitimate PROMPTS_DIR.
|
|
27
|
+
*
|
|
28
|
+
* Defense in depth:
|
|
29
|
+
* 1. Canonicalize via safeRealpathSync (realpathSync with ENOENT→fallback)
|
|
30
|
+
* so the path a symlinked leaf actually resolves to is what we validate.
|
|
31
|
+
* 2. Refuse root, home, or shorter-than-home paths.
|
|
32
|
+
* 3. Require ≥ 3 non-empty segments (rejects pathological
|
|
33
|
+
* XDG_CONFIG_HOME='/' which yields '/opencode/opencode-swarm', 2 segments).
|
|
34
|
+
* 4. Require basename === 'opencode-swarm' AND the parent directory's
|
|
35
|
+
* basename to equal the config dir's basename (canonical layout is
|
|
36
|
+
* `<CONFIG_DIR>/opencode-swarm`).
|
|
37
|
+
*/
|
|
38
|
+
export declare function isSafePromptsDir(p: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Safety guard for the `uninstall --clean` deletion of PLUGIN_CONFIG_PATH
|
|
41
|
+
* (`<CONFIG_DIR>/opencode-swarm.json`, a FILE removed with unlinkSync).
|
|
42
|
+
*
|
|
43
|
+
* File-oriented sibling of isSafePromptsDir. isSafePromptsDir would
|
|
44
|
+
* false-REJECT this path because the leaf here is the `.json` FILE, not the
|
|
45
|
+
* `opencode-swarm` directory — hence a dedicated guard.
|
|
46
|
+
*
|
|
47
|
+
* Defense in depth: canonicalize via safeRealpathSync; refuse root/home/
|
|
48
|
+
* shorter-than-home; require basename === 'opencode-swarm.json' AND the
|
|
49
|
+
* parent directory's basename to equal the config dir's basename.
|
|
50
|
+
*/
|
|
51
|
+
export declare function isSafePluginConfigPath(p: string): boolean;
|
|
18
52
|
/**
|
|
19
53
|
* Recursively delete every known opencode plugin cache location for
|
|
20
54
|
* opencode-swarm. Returns paths actually cleared and paths that errored.
|
package/dist/cli/index.js
CHANGED
|
@@ -7,22 +7,22 @@ import {
|
|
|
7
7
|
getPluginLockFilePaths,
|
|
8
8
|
package_default,
|
|
9
9
|
resolveCommand
|
|
10
|
-
} from "./index-
|
|
10
|
+
} from "./index-typ408f4.js";
|
|
11
11
|
import"./index-yfedche9.js";
|
|
12
12
|
import"./index-c8s9a3zh.js";
|
|
13
13
|
import"./index-9bsmzfk3.js";
|
|
14
14
|
import {
|
|
15
15
|
DEFAULT_AGENT_CONFIGS
|
|
16
16
|
} from "./index-a45jq4b7.js";
|
|
17
|
-
import"./index-
|
|
17
|
+
import"./index-yrrqs4b0.js";
|
|
18
18
|
import"./index-scww5b77.js";
|
|
19
19
|
import"./index-ga4ta3cr.js";
|
|
20
20
|
import"./index-80jkseqw.js";
|
|
21
|
-
import"./index-
|
|
22
|
-
import"./index-
|
|
23
|
-
import"./index-
|
|
24
|
-
import"./index-
|
|
25
|
-
import"./index-
|
|
21
|
+
import"./index-x12hvfpf.js";
|
|
22
|
+
import"./index-n18yy0z1.js";
|
|
23
|
+
import"./index-6f6y2rbp.js";
|
|
24
|
+
import"./index-8f2270hc.js";
|
|
25
|
+
import"./index-rk6qhyng.js";
|
|
26
26
|
import"./index-bfwt3abw.js";
|
|
27
27
|
import"./index-g40r5d08.js";
|
|
28
28
|
import"./index-vxv732ex.js";
|
|
@@ -49,6 +49,21 @@ import"./index-a76rekgs.js";
|
|
|
49
49
|
import * as fs from "fs";
|
|
50
50
|
import * as os from "os";
|
|
51
51
|
import * as path from "path";
|
|
52
|
+
|
|
53
|
+
// src/tools/repo-graph/safe-realpath.ts
|
|
54
|
+
import { realpathSync } from "fs";
|
|
55
|
+
function safeRealpathSync(targetPath, fallback, realpathResolver = realpathSync) {
|
|
56
|
+
try {
|
|
57
|
+
return realpathResolver(targetPath);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/cli/index.ts
|
|
52
67
|
var { version } = package_default;
|
|
53
68
|
var CONFIG_DIR = getPluginConfigDir();
|
|
54
69
|
var OPENCODE_CONFIG_PATH = path.join(CONFIG_DIR, "opencode.json");
|
|
@@ -56,14 +71,17 @@ var PLUGIN_CONFIG_PATH = path.join(CONFIG_DIR, "opencode-swarm.json");
|
|
|
56
71
|
var PROMPTS_DIR = path.join(CONFIG_DIR, "opencode-swarm");
|
|
57
72
|
var OPENCODE_PLUGIN_CACHE_PATHS = getPluginCachePaths();
|
|
58
73
|
var OPENCODE_PLUGIN_LOCK_FILE_PATHS = getPluginLockFilePaths();
|
|
74
|
+
function segmentDepthBelowRoot(resolved) {
|
|
75
|
+
const { root } = path.parse(resolved);
|
|
76
|
+
return resolved.slice(root.length).split(path.sep).filter((s) => s.length > 0).length;
|
|
77
|
+
}
|
|
59
78
|
function isSafeCachePath(p) {
|
|
60
79
|
const resolved = path.resolve(p);
|
|
61
80
|
const home = path.resolve(os.homedir());
|
|
62
81
|
if (resolved === "/" || resolved === home || resolved.length <= home.length) {
|
|
63
82
|
return false;
|
|
64
83
|
}
|
|
65
|
-
|
|
66
|
-
if (segments.length < 4) {
|
|
84
|
+
if (segmentDepthBelowRoot(resolved) < 4) {
|
|
67
85
|
return false;
|
|
68
86
|
}
|
|
69
87
|
const leaf = path.basename(resolved);
|
|
@@ -86,8 +104,7 @@ function isSafeLockFilePath(p) {
|
|
|
86
104
|
if (resolved === "/" || resolved === home || resolved.length <= home.length) {
|
|
87
105
|
return false;
|
|
88
106
|
}
|
|
89
|
-
|
|
90
|
-
if (segments.length < 4) {
|
|
107
|
+
if (segmentDepthBelowRoot(resolved) < 4) {
|
|
91
108
|
return false;
|
|
92
109
|
}
|
|
93
110
|
const leaf = path.basename(resolved);
|
|
@@ -104,6 +121,48 @@ function isSafeLockFilePath(p) {
|
|
|
104
121
|
}
|
|
105
122
|
return true;
|
|
106
123
|
}
|
|
124
|
+
function isSafePromptsDir(p) {
|
|
125
|
+
const canonical = safeRealpathSync(p, p);
|
|
126
|
+
if (canonical === null) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const resolved = path.resolve(canonical);
|
|
130
|
+
const home = path.resolve(os.homedir());
|
|
131
|
+
if (resolved === "/" || resolved === home || resolved.length <= home.length) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (segmentDepthBelowRoot(resolved) < 3) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (path.basename(resolved) !== "opencode-swarm") {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (path.basename(path.dirname(resolved)) !== path.basename(CONFIG_DIR)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
function isSafePluginConfigPath(p) {
|
|
146
|
+
const canonical = safeRealpathSync(p, p);
|
|
147
|
+
if (canonical === null) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const resolved = path.resolve(canonical);
|
|
151
|
+
const home = path.resolve(os.homedir());
|
|
152
|
+
if (resolved === "/" || resolved === home || resolved.length <= home.length) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if (segmentDepthBelowRoot(resolved) < 3) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
if (path.basename(resolved) !== "opencode-swarm.json") {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (path.basename(path.dirname(resolved)) !== path.basename(CONFIG_DIR)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
107
166
|
function ensureDir(dir) {
|
|
108
167
|
if (!fs.existsSync(dir)) {
|
|
109
168
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -270,15 +329,18 @@ function evictPluginCaches() {
|
|
|
270
329
|
for (const cachePath of OPENCODE_PLUGIN_CACHE_PATHS) {
|
|
271
330
|
if (!fs.existsSync(cachePath))
|
|
272
331
|
continue;
|
|
273
|
-
|
|
274
|
-
|
|
332
|
+
const canonical = safeRealpathSync(cachePath, cachePath);
|
|
333
|
+
if (canonical === null || !fs.existsSync(canonical))
|
|
334
|
+
continue;
|
|
335
|
+
if (!isSafeCachePath(canonical)) {
|
|
336
|
+
failed.push(`${canonical} (refused: failed safety check)`);
|
|
275
337
|
continue;
|
|
276
338
|
}
|
|
277
339
|
try {
|
|
278
|
-
fs.rmSync(
|
|
279
|
-
cleared.push(
|
|
340
|
+
fs.rmSync(canonical, { recursive: true, force: true });
|
|
341
|
+
cleared.push(canonical);
|
|
280
342
|
} catch (err) {
|
|
281
|
-
failed.push(`${
|
|
343
|
+
failed.push(`${canonical} (${err instanceof Error ? err.message : String(err)})`);
|
|
282
344
|
}
|
|
283
345
|
}
|
|
284
346
|
return { cleared, failed };
|
|
@@ -289,19 +351,22 @@ function evictLockFiles() {
|
|
|
289
351
|
for (const lockPath of OPENCODE_PLUGIN_LOCK_FILE_PATHS) {
|
|
290
352
|
if (!fs.existsSync(lockPath))
|
|
291
353
|
continue;
|
|
292
|
-
|
|
293
|
-
|
|
354
|
+
const canonical = safeRealpathSync(lockPath, lockPath);
|
|
355
|
+
if (canonical === null || !fs.existsSync(canonical))
|
|
356
|
+
continue;
|
|
357
|
+
if (!isSafeLockFilePath(canonical)) {
|
|
358
|
+
failed.push(`${canonical} (refused: failed safety check)`);
|
|
294
359
|
continue;
|
|
295
360
|
}
|
|
296
361
|
try {
|
|
297
|
-
fs.unlinkSync(
|
|
298
|
-
cleared.push(
|
|
362
|
+
fs.unlinkSync(canonical);
|
|
363
|
+
cleared.push(canonical);
|
|
299
364
|
} catch (err) {
|
|
300
365
|
const code = err?.code;
|
|
301
366
|
if (code === "EISDIR") {
|
|
302
|
-
failed.push(`${
|
|
367
|
+
failed.push(`${canonical} (path is a directory, not a file)`);
|
|
303
368
|
} else {
|
|
304
|
-
failed.push(`${
|
|
369
|
+
failed.push(`${canonical} (${err instanceof Error ? err.message : String(err)})`);
|
|
305
370
|
}
|
|
306
371
|
}
|
|
307
372
|
}
|
|
@@ -346,14 +411,24 @@ async function uninstall() {
|
|
|
346
411
|
if (process.argv.includes("--clean")) {
|
|
347
412
|
let cleaned = false;
|
|
348
413
|
if (fs.existsSync(PLUGIN_CONFIG_PATH)) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
414
|
+
const canonical = safeRealpathSync(PLUGIN_CONFIG_PATH, PLUGIN_CONFIG_PATH);
|
|
415
|
+
if (canonical === null || !isSafePluginConfigPath(canonical)) {
|
|
416
|
+
console.log(`\u2717 Refused to remove plugin config (failed safety check): ${canonical ?? PLUGIN_CONFIG_PATH}`);
|
|
417
|
+
} else {
|
|
418
|
+
fs.unlinkSync(canonical);
|
|
419
|
+
console.log(`\u2713 Removed plugin config: ${canonical}`);
|
|
420
|
+
cleaned = true;
|
|
421
|
+
}
|
|
352
422
|
}
|
|
353
423
|
if (fs.existsSync(PROMPTS_DIR)) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
424
|
+
const canonical = safeRealpathSync(PROMPTS_DIR, PROMPTS_DIR);
|
|
425
|
+
if (canonical === null || !isSafePromptsDir(canonical)) {
|
|
426
|
+
console.log(`\u2717 Refused to remove custom prompts (failed safety check): ${canonical ?? PROMPTS_DIR}`);
|
|
427
|
+
} else {
|
|
428
|
+
fs.rmSync(canonical, { recursive: true });
|
|
429
|
+
console.log(`\u2713 Removed custom prompts: ${canonical}`);
|
|
430
|
+
cleaned = true;
|
|
431
|
+
}
|
|
357
432
|
}
|
|
358
433
|
if (!cleaned) {
|
|
359
434
|
console.log("\u2713 No config files to clean up");
|
|
@@ -474,6 +549,8 @@ Valid commands: ${VALID_COMMANDS.join(", ")}`);
|
|
|
474
549
|
}
|
|
475
550
|
export {
|
|
476
551
|
run,
|
|
552
|
+
isSafePromptsDir,
|
|
553
|
+
isSafePluginConfigPath,
|
|
477
554
|
isSafeLockFilePath,
|
|
478
555
|
isSafeCachePath,
|
|
479
556
|
evictPluginCaches,
|
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
maybeEscalateOnViolation,
|
|
9
9
|
maybeQuarantineOnContradiction,
|
|
10
10
|
readRecentEscalations
|
|
11
|
-
} from "./index-
|
|
12
|
-
import"./index-
|
|
13
|
-
import"./index-
|
|
11
|
+
} from "./index-6f6y2rbp.js";
|
|
12
|
+
import"./index-8f2270hc.js";
|
|
13
|
+
import"./index-rk6qhyng.js";
|
|
14
14
|
import"./index-bfwt3abw.js";
|
|
15
15
|
import"./index-g40r5d08.js";
|
|
16
16
|
import"./index-jtqkh8jf.js";
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
resolveKnowledgeCounterBaselinePath,
|
|
26
26
|
resolveKnowledgeEventsPath,
|
|
27
27
|
resolveLegacyApplicationLogPath
|
|
28
|
-
} from "./index-
|
|
28
|
+
} from "./index-8f2270hc.js";
|
|
29
29
|
import"./index-g40r5d08.js";
|
|
30
30
|
import"./index-zgwm4ryv.js";
|
|
31
31
|
import"./index-0p660a87.js";
|
|
@@ -16,14 +16,15 @@ import {
|
|
|
16
16
|
quarantineEntry,
|
|
17
17
|
resolveUnactionablePath,
|
|
18
18
|
restoreEntry,
|
|
19
|
+
scanContentSafety,
|
|
19
20
|
unarchiveEntry,
|
|
20
21
|
validateActionability,
|
|
21
22
|
validateActionableFields,
|
|
22
23
|
validateLesson,
|
|
23
24
|
validateSkillCandidatePath,
|
|
24
25
|
validateSkillPath
|
|
25
|
-
} from "./index-
|
|
26
|
-
import"./index-
|
|
26
|
+
} from "./index-n18yy0z1.js";
|
|
27
|
+
import"./index-rk6qhyng.js";
|
|
27
28
|
import"./index-bfwt3abw.js";
|
|
28
29
|
import"./index-g40r5d08.js";
|
|
29
30
|
import"./index-jtqkh8jf.js";
|
|
@@ -41,6 +42,7 @@ export {
|
|
|
41
42
|
validateActionableFields,
|
|
42
43
|
validateActionability,
|
|
43
44
|
unarchiveEntry,
|
|
45
|
+
scanContentSafety,
|
|
44
46
|
restoreEntry,
|
|
45
47
|
resolveUnactionablePath,
|
|
46
48
|
quarantineEntry,
|
|
@@ -33,10 +33,10 @@ import {
|
|
|
33
33
|
sanitizeSlug,
|
|
34
34
|
selectCandidateEntries,
|
|
35
35
|
writeEvalStub
|
|
36
|
-
} from "./index-
|
|
37
|
-
import"./index-
|
|
38
|
-
import"./index-
|
|
39
|
-
import"./index-
|
|
36
|
+
} from "./index-x12hvfpf.js";
|
|
37
|
+
import"./index-n18yy0z1.js";
|
|
38
|
+
import"./index-8f2270hc.js";
|
|
39
|
+
import"./index-rk6qhyng.js";
|
|
40
40
|
import"./index-bfwt3abw.js";
|
|
41
41
|
import"./index-g40r5d08.js";
|
|
42
42
|
import"./index-jtqkh8jf.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle /swarm ci-monitor command.
|
|
3
|
+
*
|
|
4
|
+
* Triggers the architect to enter MODE: CI_MONITOR — drives an already
|
|
5
|
+
* human-reviewed, approved PR to green and merged. Accepts a PR reference in
|
|
6
|
+
* the same three formats as /swarm pr-review and /swarm pr-feedback (full
|
|
7
|
+
* URL, owner/repo#N, or a bare PR number resolved against the origin
|
|
8
|
+
* remote). No free-text instructions are accepted: this is a mechanical,
|
|
9
|
+
* deliberately-invoked closeout flow, not a review or feedback session, and
|
|
10
|
+
* it performs a merge — trailing tokens after the PR reference are rejected
|
|
11
|
+
* with an explicit error rather than silently forwarded into the MODE
|
|
12
|
+
* signal, closing off any accidental or injected free-text channel into a
|
|
13
|
+
* merge-capable mode.
|
|
14
|
+
*
|
|
15
|
+
* PR-reference parsing and sanitization are shared with /swarm pr-review and
|
|
16
|
+
* /swarm pr-feedback via ./pr-ref.ts.
|
|
17
|
+
*/
|
|
18
|
+
export declare function handleCiMonitorCommand(directory: string, args: string[]): string;
|
|
@@ -512,6 +512,14 @@ export declare const COMMAND_REGISTRY: {
|
|
|
512
512
|
readonly category: "agent";
|
|
513
513
|
readonly toolPolicy: "none";
|
|
514
514
|
};
|
|
515
|
+
readonly 'ci-monitor': {
|
|
516
|
+
readonly handler: (ctx: CommandContext) => CommandResult;
|
|
517
|
+
readonly description: "Drive an already-reviewed, approved PR to green and merged (monitor CI, fix, merge) [pr]";
|
|
518
|
+
readonly args: "<pr-url|owner/repo#N|N>";
|
|
519
|
+
readonly details: "Triggers MODE: CI_MONITOR — takes an already human-reviewed, approved PR, exhaustively researches every CI failure, fixes it end-to-end, iterates until all required checks are green (max 5 fix cycles), then merges via `gh pr merge` with no merge-strategy flag. Invoke only after human review is complete; the skill re-verifies reviewDecision: APPROVED and mergeable state before doing anything destructive. Distinct from /swarm pr-subscribe, which passively watches a PR without a merge terminal. Supports full GitHub URL, owner/repo#N shorthand, or bare PR number (resolved against origin).";
|
|
520
|
+
readonly category: "agent";
|
|
521
|
+
readonly toolPolicy: "none";
|
|
522
|
+
};
|
|
515
523
|
readonly 'pr subscribe': {
|
|
516
524
|
readonly handler: (ctx: CommandContext) => Promise<string>;
|
|
517
525
|
readonly description: "Subscribe the current session to PR state-change notifications";
|
|
@@ -82,12 +82,33 @@ export declare const OPENCODE_ONLY_ARCHITECT_MODE_SKILLS: Array<{
|
|
|
82
82
|
* byte-identity contract to additional runtime mirrors when present.
|
|
83
83
|
* - `divergent`: both must exist; content intentionally differs per runtime.
|
|
84
84
|
* - `opencode-only`: `.opencode` exists; no `.claude` mirror expected.
|
|
85
|
+
* - `adapter`: a non-architect-MODE skill whose `.opencode` copy is
|
|
86
|
+
* canonical and has no `.claude` copy at all, but does have one or more
|
|
87
|
+
* thin adapter shims (e.g. `.agents/skills/<slug>/`) that reference the
|
|
88
|
+
* canonical file by a fixed substring (`expectedCanonicalRef`). Mirrors
|
|
89
|
+
* the `ADAPTER_ARCHITECT_MODE_SKILLS` check, generalized to skills that
|
|
90
|
+
* are not architect MODE skills (so `identical`/`divergent`, which both
|
|
91
|
+
* require a `.claude` copy to exist, do not fit).
|
|
85
92
|
*/
|
|
86
93
|
export declare const ADDITIONAL_SKILL_MIRROR_CONTRACTS: Array<{
|
|
87
94
|
slug: string;
|
|
88
|
-
kind: 'identical' | 'divergent' | 'opencode-only';
|
|
95
|
+
kind: 'identical' | 'divergent' | 'opencode-only' | 'adapter';
|
|
89
96
|
canonical?: '.claude' | '.opencode';
|
|
90
97
|
extraIdenticalPaths?: string[];
|
|
98
|
+
/**
|
|
99
|
+
* For `divergent` pairs only: a set of Markdown section headings that must be
|
|
100
|
+
* present in BOTH trees (`.opencode` and `.claude`). Divergence is still
|
|
101
|
+
* allowed for genuinely runtime-specific prose (title line, `effort:`
|
|
102
|
+
* frontmatter, "(Claude Code)" phrasing), but these designated safety
|
|
103
|
+
* sections are parity-enforced by scripts/drift-check.ts so a whole safety
|
|
104
|
+
* guard cannot silently exist in one tree and be absent from the other.
|
|
105
|
+
* Match is a substring test, so a heading may omit a trailing parenthetical.
|
|
106
|
+
* Fixes the M13 skill-mirror safety-drift defect: the divergent branch
|
|
107
|
+
* previously did existence-only checks with no content comparison.
|
|
108
|
+
*/
|
|
109
|
+
sharedSafetyHeadings?: string[];
|
|
110
|
+
adapterPaths?: string[];
|
|
111
|
+
expectedCanonicalRef?: string;
|
|
91
112
|
reason: string;
|
|
92
113
|
}>;
|
|
93
114
|
/**
|
|
@@ -38,6 +38,12 @@ export declare function handleDebuggingSpiral(match: AdversarialPatternMatch, ta
|
|
|
38
38
|
checkpointCreated: boolean;
|
|
39
39
|
message: string;
|
|
40
40
|
}>;
|
|
41
|
+
/**
|
|
42
|
+
* Test seam: current number of distinct sessions tracked for spiral detection.
|
|
43
|
+
* Used by unit tests to assert the FIFO key-count cap holds; the map itself stays
|
|
44
|
+
* module-private so production callers cannot mutate it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function recentToolCallSessionCount(): number;
|
|
41
47
|
/**
|
|
42
48
|
* Record a tool call for debugging spiral detection.
|
|
43
49
|
* Call this from toolAfter to track repetitive patterns.
|