@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.
- package/dist/index.js +178 -41
- 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
|
-
|
|
13266
|
-
|
|
13267
|
-
|
|
13268
|
-
|
|
13269
|
-
|
|
13270
|
-
|
|
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
|
|
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
|
|
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:
|
|
13567
|
-
coderTimeoutMs:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13790
|
-
if (signal)
|
|
13791
|
-
signal.removeEventListener("abort", onAbort);
|
|
13874
|
+
cleanup();
|
|
13792
13875
|
resolve11(v);
|
|
13793
13876
|
}, (e) => {
|
|
13794
|
-
|
|
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 ??
|
|
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 ??
|
|
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
|
-
{
|
|
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.
|
|
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" &&
|
|
23198
|
-
const
|
|
23199
|
-
|
|
23200
|
-
|
|
23201
|
-
|
|
23202
|
-
|
|
23203
|
-
|
|
23204
|
-
|
|
23205
|
-
|
|
23206
|
-
|
|
23207
|
-
|
|
23208
|
-
|
|
23209
|
-
|
|
23210
|
-
|
|
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") {
|