@andyqiu/codeforge 0.6.6 → 0.6.8

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 +134 -13
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10677,7 +10677,9 @@ var ArgsSchema4 = z4.object({
10677
10677
  source: z4.enum(["reviewer", "codeforge-fallback"]).optional().describe("写入来源;默认 'reviewer',codeforge 补写时传 'codeforge-fallback'"),
10678
10678
  reviewerAgent: z4.string().optional().describe("写入 agent name(默认 'reviewer';fallback 时为 'codeforge')"),
10679
10679
  sessionId: z4.string().optional().describe("reviewer 子 session id(boomerang 溯源用,可选)"),
10680
- model: z4.string().optional().describe("审批模型 id(审计用,可选)")
10680
+ model: z4.string().optional().describe("审批模型 id(审计用,可选)"),
10681
+ coveredSha: z4.string().optional().describe("approval 写入时 worktree HEAD sha;reviewer 调此工具时传入(git -C <worktreePath> rev-parse HEAD)。pre-check 强绑定核心字段,缺失则 pre-check 不命中。ADR:merge-approval-pre-check"),
10682
+ reviewTarget: z4.string().optional().describe("本次审阅的 review_target 值(reviewer.md 词表:code / code:typescript / code:python / code:csharp-lua-c / plan_only / adr / docs / decision_only)。pre-check 仅放行 startsWith('code') 的值;缺失或其他值均不命中。ADR:merge-approval-pre-check")
10681
10683
  });
10682
10684
  var _approvalStore = null;
10683
10685
  function getApprovalStore() {
@@ -10711,6 +10713,8 @@ async function execute4(input) {
10711
10713
  decisionLine: args.decisionLine ?? args.verdict,
10712
10714
  notes: args.notes,
10713
10715
  createdAt: now,
10716
+ ...args.coveredSha ? { coveredSha: args.coveredSha } : {},
10717
+ ...args.reviewTarget ? { reviewTarget: args.reviewTarget } : {},
10714
10718
  escapeHatch: null
10715
10719
  };
10716
10720
  const file = await approvals.record(meta);
@@ -12375,7 +12379,11 @@ async function pruneOrphanWorktrees(mainRoot, opts = {}) {
12375
12379
  // lib/merge-gate.ts
12376
12380
  import { promises as fs11 } from "node:fs";
12377
12381
  import * as path14 from "node:path";
12378
- var DEFAULT_MERGE_GATE_CONFIG = { enabled: true };
12382
+ var DEFAULT_MERGE_GATE_CONFIG = {
12383
+ enabled: true,
12384
+ approvalPreCheck: true,
12385
+ preCheckTtlSeconds: 3600
12386
+ };
12379
12387
  var CONFIG_REL = ".codeforge/merge-gate.json";
12380
12388
  async function loadMergeGate(mainRoot) {
12381
12389
  const file = path14.join(mainRoot, CONFIG_REL);
@@ -12387,29 +12395,40 @@ async function loadMergeGate(mainRoot) {
12387
12395
  if (e.code === "ENOENT")
12388
12396
  return { ...DEFAULT_MERGE_GATE_CONFIG };
12389
12397
  console.warn(`[merge-gate] 读取 ${CONFIG_REL} 失败,fail-safe 退化为 enabled=false: ${e.message}`);
12390
- return { enabled: false };
12398
+ return { enabled: false, approvalPreCheck: false };
12391
12399
  }
12392
12400
  let parsed;
12393
12401
  try {
12394
12402
  parsed = JSON.parse(raw);
12395
12403
  } catch (err) {
12396
12404
  console.warn(`[merge-gate] ${CONFIG_REL} JSON 解析失败,fail-safe 退化为 enabled=false: ${err instanceof Error ? err.message : String(err)}`);
12397
- return { enabled: false };
12405
+ return { enabled: false, approvalPreCheck: false };
12398
12406
  }
12399
12407
  if (!parsed || typeof parsed !== "object") {
12400
12408
  console.warn(`[merge-gate] ${CONFIG_REL} 顶层非 object,fail-safe 退化为 enabled=false`);
12401
- return { enabled: false };
12409
+ return { enabled: false, approvalPreCheck: false };
12402
12410
  }
12403
12411
  const obj = parsed;
12404
12412
  const enabled = typeof obj["enabled"] === "boolean" ? obj["enabled"] : DEFAULT_MERGE_GATE_CONFIG.enabled;
12405
- return { enabled };
12413
+ const approvalPreCheck = typeof obj["approvalPreCheck"] === "boolean" ? obj["approvalPreCheck"] : DEFAULT_MERGE_GATE_CONFIG.approvalPreCheck ?? false;
12414
+ let preCheckTtlSeconds = DEFAULT_MERGE_GATE_CONFIG.preCheckTtlSeconds ?? 3600;
12415
+ const rawTtl = obj["preCheckTtlSeconds"];
12416
+ if (typeof rawTtl === "number" && rawTtl > 0) {
12417
+ if (rawTtl > 86400) {
12418
+ console.warn(`[merge-gate] preCheckTtlSeconds=${rawTtl} 超过 24h 上界,截断为 86400`);
12419
+ preCheckTtlSeconds = 86400;
12420
+ } else {
12421
+ preCheckTtlSeconds = rawTtl;
12422
+ }
12423
+ }
12424
+ return { enabled, approvalPreCheck, preCheckTtlSeconds };
12406
12425
  }
12407
12426
 
12408
12427
  // lib/merge-loop.ts
12409
12428
  var DEFAULT_MERGE_LOOP_CONFIG = {
12410
12429
  maxReviewLoops: 3,
12411
12430
  autoCoder: true,
12412
- reviewTimeoutMs: 180000,
12431
+ reviewTimeoutMs: 360000,
12413
12432
  coderTimeoutMs: 600000,
12414
12433
  abortDirtyStrategy: "checkpoint"
12415
12434
  };
@@ -12447,6 +12466,43 @@ async function runMergeLoop(opts) {
12447
12466
  });
12448
12467
  return { status: "force_merged", commitSha: sha, loops: 0 };
12449
12468
  }
12469
+ if (mergeGate.enabled && mergeGate.approvalPreCheck) {
12470
+ progress("approval_pre_check", "检查既有 approval 是否可跳过 reviewer");
12471
+ const preStore = opts.__testHooks?.approvalStore ?? ApprovalStore.forProject(opts.mainRoot);
12472
+ const hit = await tryApprovalPreCheck({ opts, entry, mergeGate, store: preStore });
12473
+ if (hit.ok) {
12474
+ await maybeAbort(opts, config, entry);
12475
+ if (typeof preStore.recordEscape === "function") {
12476
+ await preStore.recordEscape({
12477
+ pendingId: `session:${opts.sessionId}`,
12478
+ timestamp: new Date().toISOString(),
12479
+ agent: "codeforge-pre-check",
12480
+ sessionId: opts.sessionId,
12481
+ reason: `approval-pre-check: skipped reviewer (coveredSha=${hit.coveredSha})`,
12482
+ pendingMeta: {
12483
+ target: "worktree",
12484
+ sourceHash: hit.coveredSha,
12485
+ newSize: 0
12486
+ }
12487
+ }).catch((err) => {
12488
+ console.warn(`[merge-loop] pre-check recordEscape 失败 (session=${opts.sessionId}): ${err instanceof Error ? err.message : String(err)}`);
12489
+ });
12490
+ }
12491
+ progress("approval_pre_check", `skip_review | reviewTarget=${hit.reviewTarget} | coveredSha=${hit.coveredSha.slice(0, 12)} | ttlOk`);
12492
+ const { sha } = await mergeSessionBack({
12493
+ sessionId: opts.sessionId,
12494
+ mainRoot: opts.mainRoot
12495
+ });
12496
+ return {
12497
+ status: "skipped_by_approval",
12498
+ commitSha: sha,
12499
+ loops: 0,
12500
+ finalDecision: "APPROVE",
12501
+ lastReviewSummary: `approval-pre-check 命中:coveredSha=${hit.coveredSha}, reviewTarget=${hit.reviewTarget}`
12502
+ };
12503
+ }
12504
+ progress("approval_pre_check", `未命中(${hit.reason}),走 review-loop`);
12505
+ }
12450
12506
  while (true) {
12451
12507
  await maybeAbort(opts, config, entry);
12452
12508
  loops += 1;
@@ -12667,6 +12723,44 @@ async function handleAbortDirty(opts, config, entry) {
12667
12723
  console.warn(`[merge-loop] abort-dirty 处理失败 (session=${opts.sessionId}): ${err instanceof Error ? err.message : String(err)}`);
12668
12724
  }
12669
12725
  }
12726
+ async function tryApprovalPreCheck(args) {
12727
+ const { opts, entry, mergeGate, store } = args;
12728
+ const isDirty = opts.__testHooks?.isWorktreeDirty ?? isWorktreeDirty;
12729
+ const headOf = opts.__testHooks?.getCurrentWorktreeHead ?? getCurrentWorktreeHead;
12730
+ let dirty;
12731
+ try {
12732
+ dirty = await isDirty(entry.worktreePath);
12733
+ } catch {
12734
+ return { ok: false, reason: "dirty-check-failed" };
12735
+ }
12736
+ if (dirty)
12737
+ return { ok: false, reason: "dirty" };
12738
+ let approval;
12739
+ try {
12740
+ approval = await store.getLatest(`session:${opts.sessionId}`);
12741
+ } catch (err) {
12742
+ console.warn(`[merge-loop] pre-check getLatest 失败 (session=${opts.sessionId}): ${err instanceof Error ? err.message : String(err)}`);
12743
+ return { ok: false, reason: "no-approval" };
12744
+ }
12745
+ if (!approval)
12746
+ return { ok: false, reason: "no-approval" };
12747
+ if (approval.verdict !== "APPROVE" && approval.verdict !== "APPROVE_WITH_NOTES") {
12748
+ return { ok: false, reason: "verdict" };
12749
+ }
12750
+ if (approval.reviewTarget?.startsWith("code") !== true) {
12751
+ return { ok: false, reason: "target" };
12752
+ }
12753
+ if (!approval.coveredSha)
12754
+ return { ok: false, reason: "no-covered-sha" };
12755
+ const head = await headOf(entry.worktreePath);
12756
+ if (approval.coveredSha !== head)
12757
+ return { ok: false, reason: "sha-mismatch" };
12758
+ const ttlSeconds = Math.min(mergeGate.preCheckTtlSeconds ?? 3600, 86400);
12759
+ const age = Date.now() - Date.parse(approval.createdAt);
12760
+ if (!(age <= ttlSeconds * 1000))
12761
+ return { ok: false, reason: "ttl-expired" };
12762
+ return { ok: true, coveredSha: approval.coveredSha, reviewTarget: approval.reviewTarget };
12763
+ }
12670
12764
  function isAbortError(err) {
12671
12765
  return err instanceof Error && err.name === "AbortError";
12672
12766
  }
@@ -14124,7 +14218,7 @@ class ProductionSpawner {
14124
14218
  prompt,
14125
14219
  title: `[merge-review] sess=${args.sessionId.slice(0, 8)} r=${args.round}/${args.maxRounds}`,
14126
14220
  ...args.signal ? { signal: args.signal } : {},
14127
- timeoutMs: this.opts.reviewerTimeoutMs ?? 180000
14221
+ timeoutMs: this.opts.reviewerTimeoutMs ?? 360000
14128
14222
  }, args.sessionId);
14129
14223
  } catch (err) {
14130
14224
  throw err;
@@ -19889,7 +19983,7 @@ import * as zlib from "node:zlib";
19889
19983
  // lib/version-injected.ts
19890
19984
  function getInjectedVersion() {
19891
19985
  try {
19892
- const v = "0.6.6";
19986
+ const v = "0.6.8";
19893
19987
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
19894
19988
  return v;
19895
19989
  }
@@ -21345,8 +21439,7 @@ function formatLazyBindDenyReason(input) {
21345
21439
  var CLASS_B_CALLER_WHITELIST = new Set([
21346
21440
  "codeforge",
21347
21441
  "reviewer",
21348
- "reviewer-lite",
21349
- "general"
21442
+ "reviewer-lite"
21350
21443
  ]);
21351
21444
  var MERGE_CALLER_WHITELIST = new Set([
21352
21445
  "codeforge",
@@ -21558,10 +21651,38 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
21558
21651
  try {
21559
21652
  entry = await getSessionWorktree(sessionId, mainRoot);
21560
21653
  } catch (err) {
21561
- log13.warn(`getSessionWorktree failed (跳过本次检查)`, {
21654
+ const errMsg = err instanceof Error ? err.message : String(err);
21655
+ if (isWriteOperation(toolName, argsObj, mainRoot)) {
21656
+ log13.warn(`[registry-fail-closed] DENY (getSessionWorktree failed)`, {
21657
+ sessionId,
21658
+ mainRoot,
21659
+ tool: toolName,
21660
+ error: errMsg
21661
+ });
21662
+ safeWriteLog(PLUGIN_NAME22, {
21663
+ hook: "tool.execute.before",
21664
+ tool: toolName,
21665
+ sessionID: input.sessionID,
21666
+ action: "deny",
21667
+ source: "registry-query-failed",
21668
+ error: errMsg
21669
+ });
21670
+ denied = new DeniedError(`[session-worktree-guard] DENIED: session ${sessionId} 的 worktree 绑定信息查询失败` + `(${errMsg}),为防止写操作污染主工作区已拒绝执行。请重试;持续失败请检查 registry ` + `文件或设 CODEFORGE_DISABLE_WORKTREE_GUARD=1 显式放弃隔离`);
21671
+ return;
21672
+ }
21673
+ log13.warn(`getSessionWorktree failed (只读放行)`, {
21562
21674
  sessionId,
21563
21675
  mainRoot,
21564
- error: err instanceof Error ? err.message : String(err)
21676
+ tool: toolName,
21677
+ error: errMsg
21678
+ });
21679
+ safeWriteLog(PLUGIN_NAME22, {
21680
+ hook: "tool.execute.before",
21681
+ tool: toolName,
21682
+ sessionID: input.sessionID,
21683
+ action: "skip",
21684
+ source: "registry-query-failed-readonly",
21685
+ is_write: false
21565
21686
  });
21566
21687
  return;
21567
21688
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andyqiu/codeforge",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "CodeForge — opencode 的零侵入扩展包",
5
5
  "type": "module",
6
6
  "private": false,