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.
Files changed (50) hide show
  1. package/.opencode/skills/ci-fix-monitor/SKILL.md +8 -13
  2. package/.opencode/skills/commit-pr/SKILL.md +4 -0
  3. package/.opencode/skills/engineering-conventions/SKILL.md +33 -0
  4. package/.opencode/skills/execute/SKILL.md +2 -0
  5. package/.opencode/skills/swarm-ci-monitor/SKILL.md +5 -0
  6. package/.opencode/skills/swarm-pr-feedback/SKILL.md +2 -0
  7. package/.opencode/skills/writing-tests/SKILL.md +2 -0
  8. package/README.md +16 -14
  9. package/dist/agents/template.d.ts +40 -20
  10. package/dist/cli/{curator-qshyr2tc.js → curator-5xt89z05.js} +7 -7
  11. package/dist/cli/{curator-llm-factory-2xgznxyw.js → curator-llm-factory-pnhvf3s3.js} +7 -7
  12. package/dist/cli/{evidence-summary-service-8g594znj.js → evidence-summary-service-fehwj116.js} +1 -1
  13. package/dist/cli/{guardrail-explain-4t8fnysh.js → guardrail-explain-7aztgtyh.js} +8 -8
  14. package/dist/cli/{hive-promoter-5ngbmnrw.js → hive-promoter-q899q2m6.js} +7 -7
  15. package/dist/cli/{index-z01r5483.js → index-54sevh81.js} +8 -8
  16. package/dist/cli/{index-7stsmndb.js → index-6f6y2rbp.js} +3 -3
  17. package/dist/cli/{index-kjbfry6m.js → index-8f2270hc.js} +1 -1
  18. package/dist/cli/{index-gmjrwz34.js → index-m9m7p49w.js} +1 -1
  19. package/dist/cli/{index-qrnhvhmg.js → index-n18yy0z1.js} +26 -2
  20. package/dist/cli/{index-91qesget.js → index-rk6qhyng.js} +2 -2
  21. package/dist/cli/{index-6ctejje2.js → index-typ408f4.js} +710 -630
  22. package/dist/cli/{index-vjvdvjd3.js → index-x12hvfpf.js} +3 -3
  23. package/dist/cli/{index-f6y341yk.js → index-yrrqs4b0.js} +82 -21
  24. package/dist/cli/index.d.ts +34 -0
  25. package/dist/cli/index.js +105 -28
  26. package/dist/cli/{knowledge-escalator-qn7ew687.js → knowledge-escalator-5gwp1ar8.js} +3 -3
  27. package/dist/cli/{knowledge-events-k2xsz5bh.js → knowledge-events-m304swh6.js} +1 -1
  28. package/dist/cli/{knowledge-store-ksa1dr2z.js → knowledge-store-9babt8rd.js} +1 -1
  29. package/dist/cli/{knowledge-validator-2knz0d2t.js → knowledge-validator-t9sym59g.js} +4 -2
  30. package/dist/cli/{skill-generator-w6qd9mde.js → skill-generator-tav44xpm.js} +4 -4
  31. package/dist/commands/ci-monitor.d.ts +18 -0
  32. package/dist/commands/registry.d.ts +8 -0
  33. package/dist/config/skill-mirrors.d.ts +22 -1
  34. package/dist/hooks/adversarial-detector.d.ts +6 -0
  35. package/dist/hooks/delegation-gate.d.ts +19 -0
  36. package/dist/hooks/guardrails/index.d.ts +2 -2
  37. package/dist/hooks/knowledge-validator.d.ts +22 -0
  38. package/dist/index.d.ts +7 -0
  39. package/dist/index.js +194 -169
  40. package/dist/plan/ledger.d.ts +83 -20
  41. package/dist/types/delegation.d.ts +24 -0
  42. package/package.json +1 -1
  43. package/dist/graph/graph-builder.d.ts +0 -39
  44. package/dist/graph/graph-query.d.ts +0 -42
  45. package/dist/graph/graph-store.d.ts +0 -27
  46. package/dist/graph/import-extractor.d.ts +0 -44
  47. package/dist/graph/index.d.ts +0 -16
  48. package/dist/graph/symbol-extractor.d.ts +0 -17
  49. package/dist/graph/types.d.ts +0 -84
  50. 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-qrnhvhmg.js";
5
+ } from "./index-n18yy0z1.js";
6
6
  import {
7
7
  effectiveRetrievalOutcomes,
8
8
  readKnowledgeCounterRollups
9
- } from "./index-kjbfry6m.js";
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-91qesget.js";
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
- readdirSync,
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
- fs5.writeFileSync(tempPath, line, "utf8");
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.renameSync(tempPath, ledgerPath);
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, _options) {
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 quarantinePath = path8.join(directory, ".swarm", "plan-ledger.quarantine");
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
- } catch {}
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 replayFromLedger(directory);
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 = readdirSync(swarmDir2);
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
- await bunWrite(tempPlanPath, JSON.stringify(targetPlan, null, 2));
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 mkdirSync5,
3809
- readdirSync as readdirSync2,
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
- mkdirSync5(evidenceDir, { recursive: true });
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 = readdirSync2(evidenceBasePath);
4415
+ entries = readdirSync3(evidenceBasePath);
4355
4416
  } catch {
4356
4417
  return [];
4357
4418
  }
@@ -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-6ctejje2.js";
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-f6y341yk.js";
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-vjvdvjd3.js";
22
- import"./index-qrnhvhmg.js";
23
- import"./index-7stsmndb.js";
24
- import"./index-kjbfry6m.js";
25
- import"./index-91qesget.js";
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
- const segments = resolved.split(path.sep).filter((s) => s.length > 0);
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
- const segments = resolved.split(path.sep).filter((s) => s.length > 0);
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
- if (!isSafeCachePath(cachePath)) {
274
- failed.push(`${cachePath} (refused: failed safety check)`);
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(cachePath, { recursive: true, force: true });
279
- cleared.push(cachePath);
340
+ fs.rmSync(canonical, { recursive: true, force: true });
341
+ cleared.push(canonical);
280
342
  } catch (err) {
281
- failed.push(`${cachePath} (${err instanceof Error ? err.message : String(err)})`);
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
- if (!isSafeLockFilePath(lockPath)) {
293
- failed.push(`${lockPath} (refused: failed safety check)`);
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(lockPath);
298
- cleared.push(lockPath);
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(`${lockPath} (path is a directory, not a file)`);
367
+ failed.push(`${canonical} (path is a directory, not a file)`);
303
368
  } else {
304
- failed.push(`${lockPath} (${err instanceof Error ? err.message : String(err)})`);
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
- fs.unlinkSync(PLUGIN_CONFIG_PATH);
350
- console.log(`\u2713 Removed plugin config: ${PLUGIN_CONFIG_PATH}`);
351
- cleaned = true;
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
- fs.rmSync(PROMPTS_DIR, { recursive: true });
355
- console.log(`\u2713 Removed custom prompts: ${PROMPTS_DIR}`);
356
- cleaned = true;
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-7stsmndb.js";
12
- import"./index-kjbfry6m.js";
13
- import"./index-91qesget.js";
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-kjbfry6m.js";
28
+ } from "./index-8f2270hc.js";
29
29
  import"./index-g40r5d08.js";
30
30
  import"./index-zgwm4ryv.js";
31
31
  import"./index-0p660a87.js";
@@ -34,7 +34,7 @@ import {
34
34
  transactFile,
35
35
  transactKnowledge,
36
36
  wordBigrams
37
- } from "./index-91qesget.js";
37
+ } from "./index-rk6qhyng.js";
38
38
  import"./index-bfwt3abw.js";
39
39
  import"./index-g40r5d08.js";
40
40
  import"./index-jtqkh8jf.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-qrnhvhmg.js";
26
- import"./index-91qesget.js";
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-vjvdvjd3.js";
37
- import"./index-qrnhvhmg.js";
38
- import"./index-kjbfry6m.js";
39
- import"./index-91qesget.js";
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.