@andyqiu/codeforge 0.5.9 → 0.5.11

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 +178 -41
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13262,12 +13262,25 @@ async function mergeSessionBack(opts) {
13262
13262
  }
13263
13263
  const hasDevOnce = await packageHasScript(mainRoot, "dev:once");
13264
13264
  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}`);
13265
+ const stagedRaw = await runGit2(mainRoot, [
13266
+ "diff",
13267
+ "--cached",
13268
+ "--name-only",
13269
+ "--diff-filter=ACMR"
13270
+ ]);
13271
+ const stagedPaths = stagedRaw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
13272
+ const canSkipDevOnce = await shouldSkipDevOnce(mainRoot, stagedPaths);
13273
+ if (canSkipDevOnce) {
13274
+ const sourceCount = stagedPaths.filter((p) => /^(plugins|lib|src)\//.test(p) && !/\.(md|test\.ts)$/.test(p)).length;
13275
+ console.log(`[session-worktree] skip dev:once: dist 已是最新(${sourceCount} staged 源文件 mtime <= dist mtime)`);
13276
+ } else {
13277
+ try {
13278
+ await runCmd("npm", ["run", "dev:once"], mainRoot);
13279
+ } catch (err) {
13280
+ await runGit2(mainRoot, ["reset", "--hard", "HEAD"]).catch(() => {});
13281
+ const msg = err instanceof Error ? err.message : String(err);
13282
+ throw new Error(`dev:once 失败已 reset 主仓: ${msg}`);
13283
+ }
13271
13284
  }
13272
13285
  } else {
13273
13286
  console.log(`[session-worktree] skip dev:once: not configured in ${mainRoot}/package.json`);
@@ -13275,7 +13288,9 @@ async function mergeSessionBack(opts) {
13275
13288
  const squashedRaw = await runGit2(wt, ["log", "--format=%s", `${baseSha}..HEAD`]);
13276
13289
  const squashedCommits = squashedRaw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
13277
13290
  const message = opts.commitMessage ?? buildMergeMessage(opts.sessionId, branch, baseSha, squashedCommits);
13278
- await runGit2(mainRoot, ["commit", "-m", message]);
13291
+ await runGitWithEnv(mainRoot, ["commit", "-m", message], {
13292
+ SKIP_DEV_SYNC_CHECK: "1"
13293
+ });
13279
13294
  const newSha = (await runGit2(mainRoot, ["rev-parse", "HEAD"])).trim();
13280
13295
  try {
13281
13296
  await removeWorktree({ root: mainRoot, worktree_path: wt, force: true });
@@ -13376,6 +13391,24 @@ function runGit2(cwd, args, timeoutMs = 1e4) {
13376
13391
  });
13377
13392
  });
13378
13393
  }
13394
+ function runGitWithEnv(cwd, args, envOverrides, timeoutMs = 1e4) {
13395
+ const inheritedEnv = process["env"];
13396
+ return new Promise((resolve11, reject) => {
13397
+ execFile3("git", args, {
13398
+ cwd,
13399
+ timeout: timeoutMs,
13400
+ windowsHide: true,
13401
+ encoding: "utf8",
13402
+ env: Object.assign({}, inheritedEnv, envOverrides)
13403
+ }, (err, stdout, stderr) => {
13404
+ if (err) {
13405
+ reject(new Error(`git ${args.join(" ")} (cwd=${cwd}) 失败: ${stderr?.trim() || err.message}`));
13406
+ return;
13407
+ }
13408
+ resolve11(stdout);
13409
+ });
13410
+ });
13411
+ }
13379
13412
  async function packageHasScript(mainRoot, scriptName) {
13380
13413
  try {
13381
13414
  const pkgPath = path13.join(mainRoot, "package.json");
@@ -13388,7 +13421,31 @@ async function packageHasScript(mainRoot, scriptName) {
13388
13421
  return false;
13389
13422
  }
13390
13423
  }
13391
- function runCmd(cmd, args, cwd, timeoutMs = 120000) {
13424
+ async function shouldSkipDevOnce(mainRoot, stagedPaths) {
13425
+ let distMtimeSec;
13426
+ try {
13427
+ const st = await fs10.stat(path13.join(mainRoot, "dist/index.js"));
13428
+ distMtimeSec = Math.floor(st.mtimeMs / 1000);
13429
+ } catch {
13430
+ return false;
13431
+ }
13432
+ const relevant = stagedPaths.filter((p) => /^(plugins|lib|src)\//.test(p) && !/\.(md|test\.ts)$/.test(p));
13433
+ if (relevant.length === 0)
13434
+ return true;
13435
+ for (const rel of relevant) {
13436
+ try {
13437
+ const abs = path13.join(mainRoot, rel);
13438
+ const st = await fs10.stat(abs);
13439
+ const srcMtimeSec = Math.floor(st.mtimeMs / 1000);
13440
+ if (srcMtimeSec > distMtimeSec)
13441
+ return false;
13442
+ } catch {
13443
+ return false;
13444
+ }
13445
+ }
13446
+ return true;
13447
+ }
13448
+ function runCmd(cmd, args, cwd, timeoutMs = 300000) {
13392
13449
  return new Promise((resolve11, reject) => {
13393
13450
  execFile3(cmd, args, { cwd, timeout: timeoutMs, windowsHide: true, encoding: "utf8" }, (err, stdout, stderr) => {
13394
13451
  if (err) {
@@ -13563,8 +13620,8 @@ async function pruneOrphanWorktrees(mainRoot) {
13563
13620
  var DEFAULT_MERGE_LOOP_CONFIG = {
13564
13621
  maxReviewLoops: 3,
13565
13622
  autoCoder: true,
13566
- reviewTimeoutMs: 600000,
13567
- coderTimeoutMs: 1800000,
13623
+ reviewTimeoutMs: 180000,
13624
+ coderTimeoutMs: 600000,
13568
13625
  abortDirtyStrategy: "checkpoint"
13569
13626
  };
13570
13627
  async function runMergeLoop(opts) {
@@ -13614,7 +13671,11 @@ async function runMergeLoop(opts) {
13614
13671
  maxRounds: config.maxReviewLoops,
13615
13672
  ...lastReviewSummary ? { prevSummary: lastReviewSummary } : {},
13616
13673
  ...opts.signal ? { signal: opts.signal } : {}
13617
- }), config.reviewTimeoutMs, `reviewer 第 ${loops} 轮`, opts.signal);
13674
+ }), config.reviewTimeoutMs, `reviewer 第 ${loops} 轮`, opts.signal, {
13675
+ onHeartbeat: (elapsedMs) => {
13676
+ progress("dispatch_review", `reviewer 第 ${loops}/${config.maxReviewLoops} 轮仍在运行,已等待 ${Math.round(elapsedMs / 1000)}s`);
13677
+ }
13678
+ });
13618
13679
  } catch (err) {
13619
13680
  const e = err;
13620
13681
  if (isAbortError2(e)) {
@@ -13683,7 +13744,11 @@ async function runMergeLoop(opts) {
13683
13744
  ...opts.planId ? { planId: opts.planId } : {},
13684
13745
  reviewerSummary: reviewResult.summary,
13685
13746
  ...opts.signal ? { signal: opts.signal } : {}
13686
- }), config.coderTimeoutMs, `coder round ${loops}`, opts.signal);
13747
+ }), config.coderTimeoutMs, `coder round ${loops}`, opts.signal, {
13748
+ onHeartbeat: (elapsedMs) => {
13749
+ progress("dispatch_coder", `coder round ${loops} 仍在运行,已等待 ${Math.round(elapsedMs / 1000)}s`);
13750
+ }
13751
+ });
13687
13752
  progress("wait_coder", `coder 完成: ${coderResult.ok ? "ok" : "fail"} - ${coderResult.summary}`);
13688
13753
  if (!coderResult.ok) {
13689
13754
  return {
@@ -13766,34 +13831,50 @@ async function handleAbortDirty(opts, config, entry) {
13766
13831
  function isAbortError2(err) {
13767
13832
  return err instanceof Error && err.name === "AbortError";
13768
13833
  }
13769
- function withTimeout2(p, ms, label, signal) {
13834
+ function withTimeout2(p, ms, label, signal, hbOpts) {
13770
13835
  return new Promise((resolve11, reject) => {
13771
- const timer = setTimeout(() => {
13836
+ const startedAt = Date.now();
13837
+ let hbTimer = null;
13838
+ let timer;
13839
+ const cleanup = () => {
13840
+ clearTimeout(timer);
13841
+ if (hbTimer)
13842
+ clearInterval(hbTimer);
13843
+ if (signal)
13844
+ signal.removeEventListener("abort", onAbort);
13845
+ };
13846
+ timer = setTimeout(() => {
13847
+ cleanup();
13772
13848
  reject(new Error(`${label} 超时 (${ms}ms)`));
13773
13849
  }, ms);
13850
+ const hbInterval = hbOpts?.heartbeatIntervalMs ?? 30000;
13851
+ const hbCb = hbOpts?.onHeartbeat;
13852
+ if (hbCb) {
13853
+ hbTimer = setInterval(() => {
13854
+ try {
13855
+ hbCb(Date.now() - startedAt);
13856
+ } catch {}
13857
+ }, hbInterval);
13858
+ }
13774
13859
  const onAbort = () => {
13775
- clearTimeout(timer);
13860
+ cleanup();
13776
13861
  const err = new Error(`${label} aborted by signal`);
13777
13862
  err.name = "AbortError";
13778
13863
  reject(err);
13779
13864
  };
13780
13865
  if (signal) {
13781
13866
  if (signal.aborted) {
13782
- clearTimeout(timer);
13867
+ cleanup();
13783
13868
  onAbort();
13784
13869
  return;
13785
13870
  }
13786
13871
  signal.addEventListener("abort", onAbort, { once: true });
13787
13872
  }
13788
13873
  p.then((v) => {
13789
- clearTimeout(timer);
13790
- if (signal)
13791
- signal.removeEventListener("abort", onAbort);
13874
+ cleanup();
13792
13875
  resolve11(v);
13793
13876
  }, (e) => {
13794
- clearTimeout(timer);
13795
- if (signal)
13796
- signal.removeEventListener("abort", onAbort);
13877
+ cleanup();
13797
13878
  reject(e);
13798
13879
  });
13799
13880
  });
@@ -14901,7 +14982,7 @@ class ProductionSpawner {
14901
14982
  prompt,
14902
14983
  title: `[merge-review] sess=${args.sessionId.slice(0, 8)} r=${args.round}/${args.maxRounds}`,
14903
14984
  ...args.signal ? { signal: args.signal } : {},
14904
- timeoutMs: this.opts.reviewerTimeoutMs ?? 600000
14985
+ timeoutMs: this.opts.reviewerTimeoutMs ?? 180000
14905
14986
  }, args.sessionId);
14906
14987
  } catch (err) {
14907
14988
  throw err;
@@ -14933,7 +15014,7 @@ ${r.text.slice(0, 800)}`
14933
15014
  prompt,
14934
15015
  title: `[merge-fix] sess=${args.sessionId.slice(0, 8)}`,
14935
15016
  ...args.signal ? { signal: args.signal } : {},
14936
- timeoutMs: this.opts.coderTimeoutMs ?? 1800000
15017
+ timeoutMs: this.opts.coderTimeoutMs ?? 600000
14937
15018
  }, args.sessionId);
14938
15019
  } catch (err) {
14939
15020
  throw err;
@@ -21208,7 +21289,11 @@ var RISK_PATTERNS = [
21208
21289
  kinds: ["bash", "other"],
21209
21290
  re: /\b(DROP\s+(DATABASE|TABLE)|TRUNCATE\s+TABLE|DROP\s+SCHEMA)\b/i
21210
21291
  },
21211
- { tag: "write_secrets", re: /(\.env(?:\.\w+)?|id_[edr]sa|\.ssh\/id_|\.pem|\.p12|secret\.json)/i },
21292
+ {
21293
+ tag: "write_secrets",
21294
+ re: /(\.env(?:\.\w+)?|id_[edr]sa|\.ssh\/id_|\.pem|\.p12|secret\.json)/i,
21295
+ matchOn: ["command", "filePath", "path"]
21296
+ },
21212
21297
  {
21213
21298
  tag: "write_etc",
21214
21299
  kinds: ["bash", "edit"],
@@ -21289,11 +21374,11 @@ function classifyTool(tool2) {
21289
21374
  }
21290
21375
  function evaluateRisk(tool2, args) {
21291
21376
  const kind = classifyTool(tool2);
21292
- const haystack = buildHaystack(args);
21293
21377
  const hits = [];
21294
21378
  for (const pattern of RISK_PATTERNS) {
21295
21379
  if (pattern.kinds && !pattern.kinds.includes(kind))
21296
21380
  continue;
21381
+ const haystack = buildHaystackFor(args, pattern.matchOn);
21297
21382
  const m = haystack.match(pattern.re);
21298
21383
  if (m) {
21299
21384
  hits.push({
@@ -21314,6 +21399,19 @@ function buildHaystack(args) {
21314
21399
  return String(args);
21315
21400
  }
21316
21401
  }
21402
+ function buildHaystackFor(args, matchOn) {
21403
+ if (!matchOn || matchOn.length === 0) {
21404
+ return buildHaystack(args);
21405
+ }
21406
+ const parts = [];
21407
+ for (const key of matchOn) {
21408
+ if (key in args) {
21409
+ const val = args[key];
21410
+ parts.push(typeof val === "string" ? val : JSON.stringify(val));
21411
+ }
21412
+ }
21413
+ return parts.join(" ");
21414
+ }
21317
21415
 
21318
21416
  // lib/file-regex-acl.ts
21319
21417
  import * as path23 from "node:path";
@@ -21612,7 +21710,7 @@ import * as zlib from "node:zlib";
21612
21710
  // lib/version-injected.ts
21613
21711
  function getInjectedVersion() {
21614
21712
  try {
21615
- const v = "0.5.9";
21713
+ const v = "0.5.11";
21616
21714
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
21617
21715
  return v;
21618
21716
  }
@@ -22915,10 +23013,19 @@ function buildGitVcsWriteRegex(mainRoot) {
22915
23013
  return new RegExp(`git\\b[^\\n]*(?:-C\\s+|--work-tree[=\\s])${esc}`);
22916
23014
  }
22917
23015
  var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
23016
+ var CLASS_B_CALLER_WHITELIST = new Set([
23017
+ "codeforge",
23018
+ "reviewer",
23019
+ "general"
23020
+ ]);
22918
23021
  function rewritePath(value, mainRoot, worktreeRoot) {
22919
23022
  if (!value)
22920
23023
  return null;
22921
23024
  const resolved = path27.isAbsolute(value) ? value : path27.resolve(mainRoot, value);
23025
+ const wtPrefix2 = worktreeRoot.endsWith("/") ? worktreeRoot : worktreeRoot + "/";
23026
+ if (resolved === worktreeRoot || resolved.startsWith(wtPrefix2)) {
23027
+ return null;
23028
+ }
22922
23029
  if (resolved === mainRoot)
22923
23030
  return worktreeRoot;
22924
23031
  const prefix = mainRoot.endsWith("/") ? mainRoot : mainRoot + "/";
@@ -22937,6 +23044,20 @@ function commandContainsMainRoot(command, mainRoot) {
22937
23044
  const re = new RegExp(`${escapeRegex2(mainRoot)}(?=[\\s'"\`)]|$)`);
22938
23045
  return re.test(command);
22939
23046
  }
23047
+ function commandContainsMainRootExcludingWorktree(command, mainRoot, worktreePath) {
23048
+ if (!worktreePath || worktreePath === mainRoot) {
23049
+ return commandContainsMainRoot(command, mainRoot);
23050
+ }
23051
+ const wtpIdx = command.indexOf(worktreePath);
23052
+ if (wtpIdx !== -1) {
23053
+ const afterWtp = command.slice(wtpIdx + worktreePath.length);
23054
+ if (/(?:^|[/\\])\.\.(?:[/\\\s'";|<>]|$)/.test(afterWtp)) {
23055
+ return true;
23056
+ }
23057
+ }
23058
+ const sanitized = command.split(worktreePath).join("");
23059
+ return commandContainsMainRoot(sanitized, mainRoot);
23060
+ }
22940
23061
  function detectBashWriteIntent(command, mainRoot) {
22941
23062
  if (isReadOnlyBashCommand(command))
22942
23063
  return false;
@@ -23194,20 +23315,36 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
23194
23315
  }
23195
23316
  if (toolName === "bash") {
23196
23317
  const command = argsObj["command"];
23197
- if (typeof command === "string" && commandContainsMainRoot(command, mainRoot) && detectBashWriteIntent(command, mainRoot)) {
23198
- const snippet = command.length > 60 ? command.slice(0, 60) + "…" : command;
23199
- const reason = `[session-worktree-guard] DENIED: bash.command 含主仓绝对路径写操作 (${snippet}),请在当前 session worktree (${worktreePath}) 内操作`;
23200
- log14.warn(reason, { sessionId, command: command.slice(0, 200) });
23201
- safeWriteLog(PLUGIN_NAME25, {
23202
- hook: "tool.execute.before",
23203
- tool: toolName,
23204
- sessionID: input.sessionID,
23205
- action: "deny",
23206
- source: "bash-write-intent",
23207
- command: command.slice(0, 200)
23208
- });
23209
- denied = new DeniedError(reason);
23210
- return;
23318
+ if (typeof command === "string" && commandContainsMainRootExcludingWorktree(command, mainRoot, worktreePath) && detectBashWriteIntent(command, mainRoot)) {
23319
+ const caller = await resolveAgentForGuard({ sessionID: input.sessionID, agent: input.agent }, ctx.client, log14);
23320
+ if (caller !== null && CLASS_B_CALLER_WHITELIST.has(caller)) {
23321
+ log14.debug?.(`[class-b-whitelist] allow caller=${caller}`, { sessionId, tool: toolName, command: command.slice(0, 200) });
23322
+ safeWriteLog(PLUGIN_NAME25, {
23323
+ hook: "tool.execute.before",
23324
+ tool: toolName,
23325
+ sessionID: input.sessionID,
23326
+ action: "allow-whitelist",
23327
+ source: "class-b-caller-whitelist",
23328
+ caller,
23329
+ command: command.slice(0, 200)
23330
+ });
23331
+ } else {
23332
+ const callerTag = caller === null ? "unresolved" : caller;
23333
+ const snippet = command.length > 60 ? command.slice(0, 60) + "…" : command;
23334
+ const reason = `[session-worktree-guard] DENIED: bash.command 含主仓绝对路径写操作 (${snippet}) [caller=${callerTag}],请在当前 session worktree (${worktreePath}) 内操作`;
23335
+ log14.warn(reason, { sessionId, caller: callerTag, command: command.slice(0, 200) });
23336
+ safeWriteLog(PLUGIN_NAME25, {
23337
+ hook: "tool.execute.before",
23338
+ tool: toolName,
23339
+ sessionID: input.sessionID,
23340
+ action: "deny",
23341
+ source: "bash-write-intent",
23342
+ caller: callerTag,
23343
+ command: command.slice(0, 200)
23344
+ });
23345
+ denied = new DeniedError(reason);
23346
+ return;
23347
+ }
23211
23348
  }
23212
23349
  }
23213
23350
  if (toolName === "write" || toolName === "edit") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andyqiu/codeforge",
3
- "version": "0.5.9",
3
+ "version": "0.5.11",
4
4
  "description": "CodeForge — opencode 的零侵入扩展包",
5
5
  "type": "module",
6
6
  "private": false,