@andyqiu/codeforge 0.5.10 → 0.5.12

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 (2) hide show
  1. package/dist/index.js +295 -17
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13228,6 +13228,15 @@ async function markPlanReadOk(opts) {
13228
13228
  return true;
13229
13229
  });
13230
13230
  }
13231
+ async function touchEntryUpdatedAt(opts) {
13232
+ return await mutateRegistry(opts.mainRoot, (reg) => {
13233
+ const entry = reg.entries.find((e) => e.sessionId === opts.sessionId);
13234
+ if (!entry || entry.status !== "active")
13235
+ return false;
13236
+ entry.updatedAt = new Date().toISOString();
13237
+ return true;
13238
+ });
13239
+ }
13231
13240
  async function mergeSessionBack(opts) {
13232
13241
  const mainRoot = path13.resolve(opts.mainRoot);
13233
13242
  const entry = await getSessionWorktree(opts.sessionId, mainRoot);
@@ -13262,12 +13271,25 @@ async function mergeSessionBack(opts) {
13262
13271
  }
13263
13272
  const hasDevOnce = await packageHasScript(mainRoot, "dev:once");
13264
13273
  if (hasDevOnce) {
13265
- try {
13266
- await runCmd("npm", ["run", "dev:once"], mainRoot);
13267
- } catch (err) {
13268
- await runGit2(mainRoot, ["reset", "--hard", "HEAD"]).catch(() => {});
13269
- const msg = err instanceof Error ? err.message : String(err);
13270
- throw new Error(`dev:once 失败已 reset 主仓: ${msg}`);
13274
+ const stagedRaw = await runGit2(mainRoot, [
13275
+ "diff",
13276
+ "--cached",
13277
+ "--name-only",
13278
+ "--diff-filter=ACMR"
13279
+ ]);
13280
+ const stagedPaths = stagedRaw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
13281
+ const canSkipDevOnce = await shouldSkipDevOnce(mainRoot, stagedPaths);
13282
+ if (canSkipDevOnce) {
13283
+ const sourceCount = stagedPaths.filter((p) => /^(plugins|lib|src)\//.test(p) && !/\.(md|test\.ts)$/.test(p)).length;
13284
+ console.log(`[session-worktree] skip dev:once: dist 已是最新(${sourceCount} staged 源文件 mtime <= dist mtime)`);
13285
+ } else {
13286
+ try {
13287
+ await runCmd("npm", ["run", "dev:once"], mainRoot);
13288
+ } catch (err) {
13289
+ await runGit2(mainRoot, ["reset", "--hard", "HEAD"]).catch(() => {});
13290
+ const msg = err instanceof Error ? err.message : String(err);
13291
+ throw new Error(`dev:once 失败已 reset 主仓: ${msg}`);
13292
+ }
13271
13293
  }
13272
13294
  } else {
13273
13295
  console.log(`[session-worktree] skip dev:once: not configured in ${mainRoot}/package.json`);
@@ -13275,7 +13297,9 @@ async function mergeSessionBack(opts) {
13275
13297
  const squashedRaw = await runGit2(wt, ["log", "--format=%s", `${baseSha}..HEAD`]);
13276
13298
  const squashedCommits = squashedRaw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
13277
13299
  const message = opts.commitMessage ?? buildMergeMessage(opts.sessionId, branch, baseSha, squashedCommits);
13278
- await runGit2(mainRoot, ["commit", "-m", message]);
13300
+ await runGitWithEnv(mainRoot, ["commit", "-m", message], {
13301
+ SKIP_DEV_SYNC_CHECK: "1"
13302
+ });
13279
13303
  const newSha = (await runGit2(mainRoot, ["rev-parse", "HEAD"])).trim();
13280
13304
  try {
13281
13305
  await removeWorktree({ root: mainRoot, worktree_path: wt, force: true });
@@ -13376,6 +13400,24 @@ function runGit2(cwd, args, timeoutMs = 1e4) {
13376
13400
  });
13377
13401
  });
13378
13402
  }
13403
+ function runGitWithEnv(cwd, args, envOverrides, timeoutMs = 1e4) {
13404
+ const inheritedEnv = process["env"];
13405
+ return new Promise((resolve11, reject) => {
13406
+ execFile3("git", args, {
13407
+ cwd,
13408
+ timeout: timeoutMs,
13409
+ windowsHide: true,
13410
+ encoding: "utf8",
13411
+ env: Object.assign({}, inheritedEnv, envOverrides)
13412
+ }, (err, stdout, stderr) => {
13413
+ if (err) {
13414
+ reject(new Error(`git ${args.join(" ")} (cwd=${cwd}) 失败: ${stderr?.trim() || err.message}`));
13415
+ return;
13416
+ }
13417
+ resolve11(stdout);
13418
+ });
13419
+ });
13420
+ }
13379
13421
  async function packageHasScript(mainRoot, scriptName) {
13380
13422
  try {
13381
13423
  const pkgPath = path13.join(mainRoot, "package.json");
@@ -13388,7 +13430,31 @@ async function packageHasScript(mainRoot, scriptName) {
13388
13430
  return false;
13389
13431
  }
13390
13432
  }
13391
- function runCmd(cmd, args, cwd, timeoutMs = 120000) {
13433
+ async function shouldSkipDevOnce(mainRoot, stagedPaths) {
13434
+ let distMtimeSec;
13435
+ try {
13436
+ const st = await fs10.stat(path13.join(mainRoot, "dist/index.js"));
13437
+ distMtimeSec = Math.floor(st.mtimeMs / 1000);
13438
+ } catch {
13439
+ return false;
13440
+ }
13441
+ const relevant = stagedPaths.filter((p) => /^(plugins|lib|src)\//.test(p) && !/\.(md|test\.ts)$/.test(p));
13442
+ if (relevant.length === 0)
13443
+ return true;
13444
+ for (const rel of relevant) {
13445
+ try {
13446
+ const abs = path13.join(mainRoot, rel);
13447
+ const st = await fs10.stat(abs);
13448
+ const srcMtimeSec = Math.floor(st.mtimeMs / 1000);
13449
+ if (srcMtimeSec > distMtimeSec)
13450
+ return false;
13451
+ } catch {
13452
+ return false;
13453
+ }
13454
+ }
13455
+ return true;
13456
+ }
13457
+ function runCmd(cmd, args, cwd, timeoutMs = 300000) {
13392
13458
  return new Promise((resolve11, reject) => {
13393
13459
  execFile3(cmd, args, { cwd, timeout: timeoutMs, windowsHide: true, encoding: "utf8" }, (err, stdout, stderr) => {
13394
13460
  if (err) {
@@ -13413,6 +13479,7 @@ Codeforge-Base: ${baseSha.slice(0, 12)}`;
13413
13479
  return subject + body + footer;
13414
13480
  }
13415
13481
  var ORPHAN_GRACE_MS = 60000;
13482
+ var SEMANTIC_ORPHAN_MIN_AGE_MS = 24 * 60 * 60000;
13416
13483
  async function pruneDiscardedRegistryEntries(mainRoot, opts = {}) {
13417
13484
  const keepRecent = opts.keepRecent ?? 50;
13418
13485
  if (keepRecent < 0) {
@@ -13434,7 +13501,7 @@ async function pruneDiscardedRegistryEntries(mainRoot, opts = {}) {
13434
13501
  return { pruned, kept: kept.length };
13435
13502
  });
13436
13503
  }
13437
- async function pruneOrphanWorktrees(mainRoot) {
13504
+ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
13438
13505
  const resolved = path13.resolve(mainRoot);
13439
13506
  const cleaned = [];
13440
13507
  const failed = [];
@@ -13494,6 +13561,60 @@ async function pruneOrphanWorktrees(mainRoot) {
13494
13561
  }
13495
13562
  }
13496
13563
  });
13564
+ if (opts.isSessionAlive) {
13565
+ const minAge = opts.semanticOrphanMinAgeMs ?? SEMANTIC_ORPHAN_MIN_AGE_MS;
13566
+ const probe = opts.isSessionAlive;
13567
+ await mutateRegistry(resolved, async (reg2) => {
13568
+ const now = Date.now();
13569
+ for (const entry of reg2.entries) {
13570
+ if (entry.status !== "active")
13571
+ continue;
13572
+ const updatedMs = Date.parse(entry.updatedAt);
13573
+ if (!Number.isFinite(updatedMs) || now - updatedMs < minAge) {
13574
+ skipped++;
13575
+ continue;
13576
+ }
13577
+ let aliveResult;
13578
+ try {
13579
+ aliveResult = await probe(entry.sessionId);
13580
+ } catch {
13581
+ skipped++;
13582
+ continue;
13583
+ }
13584
+ if (aliveResult.source === "unknown") {
13585
+ skipped++;
13586
+ continue;
13587
+ }
13588
+ if (aliveResult.alive) {
13589
+ skipped++;
13590
+ continue;
13591
+ }
13592
+ try {
13593
+ await removeWorktree({
13594
+ root: resolved,
13595
+ worktree_path: entry.worktreePath,
13596
+ force: true
13597
+ });
13598
+ } catch (err) {
13599
+ failed.push({
13600
+ worktreePath: entry.worktreePath,
13601
+ error: `D 类 removeWorktree 失败: ${err instanceof Error ? err.message : String(err)}`
13602
+ });
13603
+ continue;
13604
+ }
13605
+ try {
13606
+ await runGit2(resolved, ["branch", "-D", entry.branch]);
13607
+ } catch {}
13608
+ entry.status = "discarded";
13609
+ entry.updatedAt = new Date().toISOString();
13610
+ cleaned.push({
13611
+ sessionId: entry.sessionId,
13612
+ worktreePath: entry.worktreePath,
13613
+ reason: `D 类语义孤儿 (opencode session ${aliveResult.source}: dead)`
13614
+ });
13615
+ }
13616
+ });
13617
+ }
13497
13618
  const codeforgeWorktreeRoot = path13.resolve(path13.join(resolved, DEFAULT_WORKTREE_SUBDIR));
13498
13619
  const fsWorktreePaths = [];
13499
13620
  try {
@@ -21232,7 +21353,11 @@ var RISK_PATTERNS = [
21232
21353
  kinds: ["bash", "other"],
21233
21354
  re: /\b(DROP\s+(DATABASE|TABLE)|TRUNCATE\s+TABLE|DROP\s+SCHEMA)\b/i
21234
21355
  },
21235
- { tag: "write_secrets", re: /(\.env(?:\.\w+)?|id_[edr]sa|\.ssh\/id_|\.pem|\.p12|secret\.json)/i },
21356
+ {
21357
+ tag: "write_secrets",
21358
+ re: /(\.env(?:\.\w+)?|id_[edr]sa|\.ssh\/id_|\.pem|\.p12|secret\.json)/i,
21359
+ matchOn: ["command", "filePath", "path"]
21360
+ },
21236
21361
  {
21237
21362
  tag: "write_etc",
21238
21363
  kinds: ["bash", "edit"],
@@ -21313,11 +21438,11 @@ function classifyTool(tool2) {
21313
21438
  }
21314
21439
  function evaluateRisk(tool2, args) {
21315
21440
  const kind = classifyTool(tool2);
21316
- const haystack = buildHaystack(args);
21317
21441
  const hits = [];
21318
21442
  for (const pattern of RISK_PATTERNS) {
21319
21443
  if (pattern.kinds && !pattern.kinds.includes(kind))
21320
21444
  continue;
21445
+ const haystack = buildHaystackFor(args, pattern.matchOn);
21321
21446
  const m = haystack.match(pattern.re);
21322
21447
  if (m) {
21323
21448
  hits.push({
@@ -21338,6 +21463,19 @@ function buildHaystack(args) {
21338
21463
  return String(args);
21339
21464
  }
21340
21465
  }
21466
+ function buildHaystackFor(args, matchOn) {
21467
+ if (!matchOn || matchOn.length === 0) {
21468
+ return buildHaystack(args);
21469
+ }
21470
+ const parts = [];
21471
+ for (const key of matchOn) {
21472
+ if (key in args) {
21473
+ const val = args[key];
21474
+ parts.push(typeof val === "string" ? val : JSON.stringify(val));
21475
+ }
21476
+ }
21477
+ return parts.join(" ");
21478
+ }
21341
21479
 
21342
21480
  // lib/file-regex-acl.ts
21343
21481
  import * as path23 from "node:path";
@@ -21636,7 +21774,7 @@ import * as zlib from "node:zlib";
21636
21774
  // lib/version-injected.ts
21637
21775
  function getInjectedVersion() {
21638
21776
  try {
21639
- const v = "0.5.10";
21777
+ const v = "0.5.12";
21640
21778
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
21641
21779
  return v;
21642
21780
  }
@@ -22916,9 +23054,9 @@ var handler24 = workflowEngineServer;
22916
23054
  import path27 from "node:path";
22917
23055
  var PLUGIN_NAME25 = "session-worktree-guard";
22918
23056
  logLifecycle(PLUGIN_NAME25, "import", {});
22919
- var WRITE_INTENT_RE = />(?!=)|\btee\b|\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bln\b/;
23057
+ var WRITE_INTENT_RE = />(?![=&])|\btee\b|\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bln\b/;
22920
23058
  var READ_ONLY_COMMANDS = /^\s*(?:ls|cat|head|tail|grep|rg|find|fd|wc|stat|file|which|whereis|echo|pwd|cd|pushd|popd|env|printenv|type|less|more|sort|uniq|awk|tr|cut|jq|date|whoami|id|uname|git(?:\s+-C\s+\S+)?\s+(?:log|show|diff|status|branch|tag|remote|config\s+--get|rev-parse|rev-list|ls-files|ls-tree|cat-file|describe|reflog|blame|shortlog|name-rev|symbolic-ref|merge-base|worktree\s+list|stash\s+list|stash\s+show))\b/;
22921
- var SIDE_EFFECT_TOKEN_RE = />(?!=)|\|\s*tee\b|\btee\b/;
23059
+ var SIDE_EFFECT_TOKEN_RE = />(?![=&])|\|\s*tee\b|\btee\b/;
22922
23060
  function isReadOnlyBashCommand(command) {
22923
23061
  if (!READ_ONLY_COMMANDS.test(command))
22924
23062
  return false;
@@ -22939,6 +23077,8 @@ function buildGitVcsWriteRegex(mainRoot) {
22939
23077
  return new RegExp(`git\\b[^\\n]*(?:-C\\s+|--work-tree[=\\s])${esc}`);
22940
23078
  }
22941
23079
  var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
23080
+ var TOUCH_THROTTLE_MS = 5 * 60000;
23081
+ var _touchCache = new Map;
22942
23082
  var CLASS_B_CALLER_WHITELIST = new Set([
22943
23083
  "codeforge",
22944
23084
  "reviewer",
@@ -22970,6 +23110,20 @@ function commandContainsMainRoot(command, mainRoot) {
22970
23110
  const re = new RegExp(`${escapeRegex2(mainRoot)}(?=[\\s'"\`)]|$)`);
22971
23111
  return re.test(command);
22972
23112
  }
23113
+ function commandContainsMainRootExcludingWorktree(command, mainRoot, worktreePath) {
23114
+ if (!worktreePath || worktreePath === mainRoot) {
23115
+ return commandContainsMainRoot(command, mainRoot);
23116
+ }
23117
+ const wtpIdx = command.indexOf(worktreePath);
23118
+ if (wtpIdx !== -1) {
23119
+ const afterWtp = command.slice(wtpIdx + worktreePath.length);
23120
+ if (/(?:^|[/\\])\.\.(?:[/\\\s'";|<>]|$)/.test(afterWtp)) {
23121
+ return true;
23122
+ }
23123
+ }
23124
+ const sanitized = command.split(worktreePath).join("");
23125
+ return commandContainsMainRoot(sanitized, mainRoot);
23126
+ }
22973
23127
  function detectBashWriteIntent(command, mainRoot) {
22974
23128
  if (isReadOnlyBashCommand(command))
22975
23129
  return false;
@@ -23109,6 +23263,22 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
23109
23263
  }
23110
23264
  }
23111
23265
  const worktreePath = entry.worktreePath;
23266
+ if (entry.status === "active" && isWriteOperation(toolName, argsObj, mainRoot)) {
23267
+ const last = _touchCache.get(entry.sessionId) ?? 0;
23268
+ const nowMs = Date.now();
23269
+ if (nowMs - last > TOUCH_THROTTLE_MS) {
23270
+ _touchCache.set(entry.sessionId, nowMs);
23271
+ touchEntryUpdatedAt({
23272
+ sessionId: entry.sessionId,
23273
+ mainRoot
23274
+ }).catch((err) => {
23275
+ log14.warn("touchEntryUpdatedAt 失败 (已忽略)", {
23276
+ sessionId: entry?.sessionId,
23277
+ error: err instanceof Error ? err.message : String(err)
23278
+ });
23279
+ });
23280
+ }
23281
+ }
23112
23282
  if (toolName === "session_merge") {
23113
23283
  const action = argsObj["action"];
23114
23284
  if (action === "merge") {
@@ -23227,7 +23397,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
23227
23397
  }
23228
23398
  if (toolName === "bash") {
23229
23399
  const command = argsObj["command"];
23230
- if (typeof command === "string" && commandContainsMainRoot(command, mainRoot) && detectBashWriteIntent(command, mainRoot)) {
23400
+ if (typeof command === "string" && commandContainsMainRootExcludingWorktree(command, mainRoot, worktreePath) && detectBashWriteIntent(command, mainRoot)) {
23231
23401
  const caller = await resolveAgentForGuard({ sessionID: input.sessionID, agent: input.agent }, ctx.client, log14);
23232
23402
  if (caller !== null && CLASS_B_CALLER_WHITELIST.has(caller)) {
23233
23403
  log14.debug?.(`[class-b-whitelist] allow caller=${caller}`, { sessionId, tool: toolName, command: command.slice(0, 200) });
@@ -23337,6 +23507,103 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
23337
23507
  };
23338
23508
  var handler25 = sessionWorktreeGuardPlugin;
23339
23509
 
23510
+ // lib/opencode-session-probe.ts
23511
+ import * as path28 from "node:path";
23512
+ import * as os6 from "node:os";
23513
+ import { createRequire as createRequire2 } from "node:module";
23514
+ var requireFromHere = createRequire2(import.meta.url);
23515
+ var DEFAULT_LIVENESS_MS = 6 * 60 * 60000;
23516
+ var DEFAULT_DB_PATH = path28.join(os6.homedir(), ".local/share/opencode/opencode.db");
23517
+ function createSessionProbe(opts = {}) {
23518
+ const dbPath = opts.dbPath ?? DEFAULT_DB_PATH;
23519
+ const httpBaseUrl = opts.httpBaseUrl ?? process.env["OPENCODE_SERVER_URL"];
23520
+ const livenessWindowMs = opts.livenessWindowMs ?? DEFAULT_LIVENESS_MS;
23521
+ const timeoutMs = opts.timeoutMs ?? 1500;
23522
+ let db = null;
23523
+ let dbInitTried = false;
23524
+ let dbStmt = null;
23525
+ async function tryOpenDb() {
23526
+ if (dbInitTried)
23527
+ return db != null;
23528
+ dbInitTried = true;
23529
+ try {
23530
+ const mod = requireFromHere("node:sqlite");
23531
+ try {
23532
+ db = new mod.DatabaseSync(dbPath, { readOnly: true });
23533
+ } catch {
23534
+ db = null;
23535
+ dbStmt = null;
23536
+ return false;
23537
+ }
23538
+ dbStmt = db.prepare("SELECT time_updated, time_archived FROM session WHERE id = ? LIMIT 1");
23539
+ return true;
23540
+ } catch {
23541
+ db = null;
23542
+ dbStmt = null;
23543
+ return false;
23544
+ }
23545
+ }
23546
+ async function probeHttp(sessionId) {
23547
+ if (!httpBaseUrl)
23548
+ return null;
23549
+ try {
23550
+ const ctrl = new AbortController;
23551
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
23552
+ const res = await fetch(`${httpBaseUrl.replace(/\/$/, "")}/session`, {
23553
+ signal: ctrl.signal
23554
+ }).finally(() => clearTimeout(t));
23555
+ if (!res.ok)
23556
+ return null;
23557
+ const list = await res.json();
23558
+ const hit = Array.isArray(list) && list.some((s) => s.id === sessionId);
23559
+ return { alive: hit, source: "http" };
23560
+ } catch {
23561
+ return null;
23562
+ }
23563
+ }
23564
+ async function probeSqlite(sessionId) {
23565
+ if (!await tryOpenDb() || !dbStmt)
23566
+ return null;
23567
+ try {
23568
+ const row = dbStmt.get(sessionId);
23569
+ if (!row) {
23570
+ return { alive: false, source: "sqlite" };
23571
+ }
23572
+ if (row.time_archived != null) {
23573
+ return {
23574
+ alive: false,
23575
+ source: "sqlite",
23576
+ time_archived: row.time_archived
23577
+ };
23578
+ }
23579
+ const now = Date.now();
23580
+ const tu = Number(row.time_updated) || 0;
23581
+ const alive = now - tu < livenessWindowMs;
23582
+ return { alive, source: "sqlite", time_updated: tu, time_archived: null };
23583
+ } catch {
23584
+ return null;
23585
+ }
23586
+ }
23587
+ return {
23588
+ async isSessionAlive(sessionId) {
23589
+ const http = await probeHttp(sessionId);
23590
+ if (http)
23591
+ return http;
23592
+ const sql = await probeSqlite(sessionId);
23593
+ if (sql)
23594
+ return sql;
23595
+ return { alive: true, source: "unknown" };
23596
+ },
23597
+ close() {
23598
+ try {
23599
+ db?.close?.();
23600
+ } catch {}
23601
+ db = null;
23602
+ dbStmt = null;
23603
+ }
23604
+ };
23605
+ }
23606
+
23340
23607
  // plugins/worktree-lifecycle.ts
23341
23608
  var PLUGIN_NAME26 = "worktree-lifecycle";
23342
23609
  logLifecycle(PLUGIN_NAME26, "import", {});
@@ -23347,6 +23614,7 @@ var PRUNE_INTERVAL_MS = 30 * 60000;
23347
23614
  var lastIdleToastAt = new Map;
23348
23615
  var pruneRunning = false;
23349
23616
  var _pruneTimer;
23617
+ var _probe = null;
23350
23618
  var log15 = makePluginLogger(PLUGIN_NAME26);
23351
23619
  var worktreeLifecyclePlugin = async (ctx) => {
23352
23620
  const mainRoot = ctx.directory;
@@ -23358,9 +23626,17 @@ var worktreeLifecyclePlugin = async (ctx) => {
23358
23626
  return {};
23359
23627
  }
23360
23628
  const client = ctx.client;
23629
+ if (_probe) {
23630
+ try {
23631
+ _probe.close();
23632
+ } catch {}
23633
+ }
23634
+ _probe = createSessionProbe();
23361
23635
  setImmediate(() => {
23362
23636
  safeAsync(PLUGIN_NAME26, "activate.pruneOrphan", async () => {
23363
- const result = await pruneOrphanWorktrees(mainRoot);
23637
+ const result = await pruneOrphanWorktrees(mainRoot, {
23638
+ isSessionAlive: _probe.isSessionAlive
23639
+ });
23364
23640
  if (result.cleaned.length > 0 || result.failed.length > 0) {
23365
23641
  log15.info(`[pruneOrphan] cleaned=${result.cleaned.length} failed=${result.failed.length} skipped=${result.skipped}`);
23366
23642
  safeWriteLog(PLUGIN_NAME26, {
@@ -23386,7 +23662,9 @@ var worktreeLifecyclePlugin = async (ctx) => {
23386
23662
  pruneRunning = true;
23387
23663
  safeAsync(PLUGIN_NAME26, "interval.pruneOrphan", async () => {
23388
23664
  try {
23389
- const result = await pruneOrphanWorktrees(mainRoot);
23665
+ const result = await pruneOrphanWorktrees(mainRoot, {
23666
+ isSessionAlive: _probe.isSessionAlive
23667
+ });
23390
23668
  if (result.cleaned.length > 0 || result.failed.length > 0) {
23391
23669
  log15.info(`[pruneOrphan interval] cleaned=${result.cleaned.length} failed=${result.failed.length} skipped=${result.skipped}`);
23392
23670
  safeWriteLog(PLUGIN_NAME26, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andyqiu/codeforge",
3
- "version": "0.5.10",
3
+ "version": "0.5.12",
4
4
  "description": "CodeForge — opencode 的零侵入扩展包",
5
5
  "type": "module",
6
6
  "private": false,