@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.
- package/dist/index.js +134 -13
- 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 = {
|
|
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
|
-
|
|
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:
|
|
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 ??
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|