@andyqiu/codeforge 0.5.2 → 0.5.4

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/bin/codeforge.mjs CHANGED
@@ -316,7 +316,8 @@ function cmdUpgrade(args) {
316
316
  log(`步骤 2/2:codeforge install --global`)
317
317
 
318
318
  if (!dryRun) {
319
- const code = installOpencode({ scope: "global", dryRun: false, extraArgs: [] })
319
+ const skipBuildFlag = process.platform === "win32" ? "-SkipBuild" : "--skip-build"
320
+ const code = installOpencode({ scope: "global", dryRun: false, extraArgs: [skipBuildFlag] })
320
321
  if (code !== 0) {
321
322
  err(`install --global 失败 (exit=${code})`)
322
323
  return 1
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: autonomy
3
+ description: 查看或切换当前 session 的自主度档位(step/semi/full/auto)
4
+ ---
5
+
6
+ 查看或切换当前 session 的自主执行模式(autonomy)。
7
+
8
+ ## 用法
9
+
10
+ - `/autonomy` — 显示当前档位和 budget 状态
11
+ - `/autonomy step` — 每个工具调用都要确认
12
+ - `/autonomy semi` — 危险工具确认,安全工具自动(默认)
13
+ - `/autonomy full` — 全自动工具执行(reviewer APPROVE 后仍需 `/merge` 手动触发)
14
+ - `/autonomy auto` — 全自动 + APPROVE 自动 merge(**最小干预模式**)
15
+
16
+ ## ⚠️ auto 模式须知
17
+
18
+ 切到 `auto` 等同 plandex `auto` 档位 —— **最小干预**而非零干预:
19
+
20
+ - reviewer **APPROVE** 后自动合并,无需手动 `/merge`
21
+ - **双重验证**:必须 reviewer 摘要 Decision=APPROVE **且** `approval-store` 存在对应 APPROVE 记录,缺一不可
22
+ - 任一失败 → fail-safe(不 merge,emit `autonomous.decision.parse_error` event + toast)
23
+ - **REQUEST_CHANGES** 自动循环(≤ 3 次),仍由 codeforge agent 派 coder 修
24
+ - **BLOCK** 通过 channels 通知(需配置 `.codeforge/channels.json`)
25
+ - channels 全失败时 fallback 写 `<runtimeDir>/sessions/autonomous-blocks.ndjson`
26
+ - **仅根 session 生效**(子 session 一律跳过,防 orchestrator 嵌套)
27
+
28
+ ### 默认预算上限(任一耗尽自动降回 semi)
29
+
30
+ | 维度 | 默认值 |
31
+ |---|---|
32
+ | 总 tokens | 50,000 |
33
+ | 总 USD | $3 |
34
+ | Wall clock | 30 分钟 |
35
+ | auto merge 次数 | 5 |
36
+ | REQUEST_CHANGES 循环 | 3 |
37
+
38
+ ### 逃生口
39
+
40
+ - `/pause` — 立刻退回 semi(不打断在途 merge)
41
+ - `/discard-session` — 不可逆,硬丢 worktree + branch
42
+ - budget 耗尽 — 自动降回 semi
43
+ - BLOCK — 强制通知 + 降档
44
+
45
+ ## 行为对比
46
+
47
+ | 场景 | step | semi | full | auto |
48
+ |---|---|---|---|---|
49
+ | bash / edit 执行 | confirm | confirm | auto | auto |
50
+ | read / search 执行 | confirm | auto | auto | auto |
51
+ | reviewer APPROVE 后 | 手动 `/merge` | 手动 `/merge` | 手动 `/merge` | **自动 merge** |
52
+ | reviewer BLOCK 后 | 用户处理 | 用户处理 | 用户处理 | **channels 通知 + 降档** |
53
+
54
+ ## 配置存储
55
+
56
+ `<runtimeDir>/autonomy/<sessionId>.json`(withFileLock 跨进程并发安全)
57
+
58
+ ## 关联 ADR
59
+
60
+ - [autonomous-mode](../docs/adr/autonomous-mode.md) — 主 ADR(设计、双重验证、五维预算)
61
+ - [plandex-three-autonomy-modes](../docs/adr/plandex-three-autonomy-modes.md) — 三档基线
62
+ - [worktree-session-isolation](../docs/adr/worktree-session-isolation.md) — merge 闭环
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: pause
3
+ description: 立刻暂停当前 session 的 auto 模式,降回 semi
4
+ ---
5
+
6
+ 暂停当前 session 的自主执行模式(auto),降回 semi 模式。
7
+
8
+ **行为说明**:
9
+
10
+ - 如果按下时**正在进行自动 merge**,当次 merge 会**继续跑完**(`session_merge` 是原子事务:worktree commit + squash + commit + cleanup 一气呵成,无法中段抢断)
11
+ - `paused` 标志设置后,driver 会在**下一次** `task.completed` / `session.idle` 事件时生效
12
+ - 如需**立刻放弃**所有改动,请使用 `/discard-session`(不可逆,硬丢 worktree)
13
+
14
+ **恢复 auto 模式**:使用 `/autonomy auto`(会自动清除 paused 标志,允许 driver 重新激活)
15
+
16
+ **配置存储**:`<runtimeDir>/autonomy/<sessionId>.json`,跨进程并发安全(withFileLock)
17
+
18
+ **关联 ADR**:[autonomous-mode](../docs/adr/autonomous-mode.md)
package/dist/index.js CHANGED
@@ -76,6 +76,9 @@ function planFilePath(absRoot, title) {
76
76
  const slug = titleToSlug(title);
77
77
  return normalize2(path2.join(dir, `${ts}-${slug}.md`));
78
78
  }
79
+ function subagentLogPath(absRoot, parentID, childID) {
80
+ return normalize2(path2.join(runtimeDir(absRoot, { ensure: false }), "subagents", parentID, `${childID}.log`));
81
+ }
79
82
  function normalize2(p) {
80
83
  return path2.normalize(path2.resolve(p));
81
84
  }
@@ -168,6 +171,20 @@ function globalConfigDir(env = process.env) {
168
171
  function projectConfigDir(root = process.cwd()) {
169
172
  return path3.join(root, ".codeforge");
170
173
  }
174
+ function getCodeforgeConfig(opts = {}) {
175
+ const root = opts.root ?? process.cwd();
176
+ const env = opts.env ?? process.env;
177
+ const cacheKey = `codeforge:${root}`;
178
+ const cached = cacheGet(cacheKey);
179
+ if (cached !== undefined)
180
+ return cached;
181
+ const builtin = {};
182
+ const globalCfg = readJsonObject(path3.join(globalConfigDir(env), "codeforge.json"));
183
+ const projectCfg = readJsonObject(path3.join(projectConfigDir(root), "codeforge.json"));
184
+ const merged = { ...builtin, ...globalCfg, ...projectCfg };
185
+ cacheSet(cacheKey, merged);
186
+ return merged;
187
+ }
171
188
  function getKhConfig(opts = {}) {
172
189
  const root = opts.root ?? process.cwd();
173
190
  const env = opts.env ?? process.env;
@@ -1021,6 +1038,14 @@ var init_autonomy = __esm(() => {
1021
1038
  read: "auto",
1022
1039
  search: "auto",
1023
1040
  other: "auto"
1041
+ },
1042
+ auto: {
1043
+ bash: "auto",
1044
+ edit: "auto",
1045
+ webfetch: "auto",
1046
+ read: "auto",
1047
+ search: "auto",
1048
+ other: "auto"
1024
1049
  }
1025
1050
  };
1026
1051
  RISK_PATTERNS = [
@@ -13361,6 +13386,20 @@ async function markInterruptedDirty(opts) {
13361
13386
  }
13362
13387
  });
13363
13388
  }
13389
+ async function getCurrentWorktreeHead(worktreePath) {
13390
+ try {
13391
+ return (await runGit2(path13.resolve(worktreePath), ["rev-parse", "HEAD"])).trim();
13392
+ } catch {
13393
+ return "";
13394
+ }
13395
+ }
13396
+ async function getCurrentMainHead(mainRoot) {
13397
+ try {
13398
+ return (await runGit2(path13.resolve(mainRoot), ["rev-parse", "HEAD"])).trim();
13399
+ } catch {
13400
+ return "";
13401
+ }
13402
+ }
13364
13403
  async function isWorktreeDirty(worktreePath) {
13365
13404
  try {
13366
13405
  const out = (await runGit2(worktreePath, ["status", "--porcelain"])).trim();
@@ -17797,6 +17836,10 @@ var modelFallbackServer = async (ctx) => {
17797
17836
  var handler12 = modelFallbackServer;
17798
17837
 
17799
17838
  // plugins/subtask-heartbeat.ts
17839
+ import { promises as fsPromises } from "node:fs";
17840
+ import * as path18 from "node:path";
17841
+ init_runtime_paths();
17842
+ init_global_config();
17800
17843
  var PLUGIN_NAME13 = "subtask-heartbeat";
17801
17844
  logLifecycle(PLUGIN_NAME13, "import", {});
17802
17845
  var HEARTBEAT_INTERVAL_MS2 = 30000;
@@ -17807,11 +17850,75 @@ var PENDING_TASK_TTL_MS = 60000;
17807
17850
  var PENDING_TASK_MAX_PARENTS = 64;
17808
17851
  var PENDING_TASK_MAX_PER_PARENT = 16;
17809
17852
  var DESCRIPTION_MAX_LEN = 60;
17853
+ var SESSION_PARENT_MAP_TTL_MS = 30 * 60000;
17854
+ var SESSION_PARENT_MAP_MAX_SIZE = 256;
17855
+ var PARENT_PARSE_FAIL_MAX_LOG = 10;
17810
17856
  var inflight3 = new Map;
17811
17857
  var pendingTask = new Map;
17858
+ var sessionParentMap = new Map;
17859
+ var _parentParseFailLogged = 0;
17812
17860
  function _snapshotInflight() {
17813
17861
  return [...inflight3.values()].map((r) => ({ ...r }));
17814
17862
  }
17863
+ function recordSessionParent(childID, parentID, now = Date.now()) {
17864
+ if (!childID || !parentID)
17865
+ return;
17866
+ if (sessionParentMap.has(childID)) {
17867
+ sessionParentMap.set(childID, { parentID, ts: now });
17868
+ return;
17869
+ }
17870
+ if (sessionParentMap.size >= SESSION_PARENT_MAP_MAX_SIZE) {
17871
+ let oldestKey = null;
17872
+ let oldestTs = Number.POSITIVE_INFINITY;
17873
+ for (const [k, v] of sessionParentMap.entries()) {
17874
+ if (v.ts < oldestTs) {
17875
+ oldestTs = v.ts;
17876
+ oldestKey = k;
17877
+ }
17878
+ }
17879
+ if (oldestKey !== null)
17880
+ sessionParentMap.delete(oldestKey);
17881
+ }
17882
+ sessionParentMap.set(childID, { parentID, ts: now });
17883
+ }
17884
+ function lookupParentSessionId(childID, now = Date.now()) {
17885
+ const entry = sessionParentMap.get(childID);
17886
+ if (!entry)
17887
+ return;
17888
+ if (now - entry.ts > SESSION_PARENT_MAP_TTL_MS) {
17889
+ sessionParentMap.delete(childID);
17890
+ return;
17891
+ }
17892
+ return entry.parentID;
17893
+ }
17894
+ function deleteSessionParent(childID) {
17895
+ sessionParentMap.delete(childID);
17896
+ }
17897
+ function sweepExpiredSessionParents(now = Date.now()) {
17898
+ let removed = 0;
17899
+ for (const [k, v] of [...sessionParentMap.entries()]) {
17900
+ if (now - v.ts > SESSION_PARENT_MAP_TTL_MS) {
17901
+ sessionParentMap.delete(k);
17902
+ removed++;
17903
+ }
17904
+ }
17905
+ return removed;
17906
+ }
17907
+ function detectUnparsedParentID(event) {
17908
+ if (!event || typeof event !== "object")
17909
+ return false;
17910
+ const e = event;
17911
+ if (e.type !== "session.created")
17912
+ return false;
17913
+ if (extractCreatedChild(event))
17914
+ return false;
17915
+ try {
17916
+ const json = JSON.stringify(event);
17917
+ return /\bparent[_-]?[iI][dD]\b/.test(json);
17918
+ } catch {
17919
+ return false;
17920
+ }
17921
+ }
17815
17922
  function getInflightSnapshot() {
17816
17923
  return _snapshotInflight();
17817
17924
  }
@@ -17928,7 +18035,8 @@ function registerInflight(payload, now = Date.now()) {
17928
18035
  description: payload.description ?? null,
17929
18036
  startedAt: now,
17930
18037
  lastBeatAt: now,
17931
- lastTool: null
18038
+ lastTool: null,
18039
+ pendingCalls: new Map
17932
18040
  };
17933
18041
  inflight3.set(payload.childID, r);
17934
18042
  return r;
@@ -18027,6 +18135,94 @@ function buildEndToast(r, type, now = Date.now()) {
18027
18135
  variant
18028
18136
  };
18029
18137
  }
18138
+ function sanitizeInputSummary(toolName, args) {
18139
+ if (!args || typeof args !== "object")
18140
+ return `${toolName} (no args)`;
18141
+ const a = args;
18142
+ const PATH_KEYS = [
18143
+ "path",
18144
+ "file",
18145
+ "filePath",
18146
+ "file_path",
18147
+ "filepath",
18148
+ "target"
18149
+ ];
18150
+ for (const k of PATH_KEYS) {
18151
+ const v = a[k];
18152
+ if (typeof v === "string" && v.length > 0 && v.length <= 200) {
18153
+ return `${toolName} ${v}`;
18154
+ }
18155
+ }
18156
+ const cmd = a["command"];
18157
+ if (typeof cmd === "string" && cmd.length > 0) {
18158
+ const head = cmd.replace(/\s+/g, " ").trim().slice(0, 60);
18159
+ return `${toolName} $ ${head}`;
18160
+ }
18161
+ return `${toolName} (no path)`;
18162
+ }
18163
+ function buildBeforeLogLine(toolName, args, now = Date.now()) {
18164
+ return JSON.stringify({
18165
+ ts: Math.floor(now / 1000),
18166
+ tool: toolName,
18167
+ phase: "before",
18168
+ input_summary: sanitizeInputSummary(toolName, args)
18169
+ });
18170
+ }
18171
+ function buildAfterLogLine(toolName, ok, durationMs, now = Date.now()) {
18172
+ return JSON.stringify({
18173
+ ts: Math.floor(now / 1000),
18174
+ tool: toolName,
18175
+ phase: "after",
18176
+ ok,
18177
+ duration_ms: durationMs
18178
+ });
18179
+ }
18180
+ function buildSuccessNotice(r, logPath, now = Date.now()) {
18181
+ const agent = r.agent ? titleCase(r.agent) : "subagent";
18182
+ const elapsed = fmtElapsed(now - r.startedAt);
18183
+ return [`✅ ${agent} 完成(${elapsed})`, `\uD83D\uDCC4 完整日志: ${logPath}`].join(`
18184
+ `);
18185
+ }
18186
+ function buildFailureNotice(r, endedType, logPath, worktreePath, now = Date.now()) {
18187
+ const agent = r.agent ? titleCase(r.agent) : "subagent";
18188
+ const elapsed = fmtElapsed(now - r.startedAt);
18189
+ const verb = endedType === "session.deleted" ? "被取消" : "失败";
18190
+ const lines = [
18191
+ `❌ ${agent} ${verb}(${endedType}, ${elapsed})`,
18192
+ `\uD83D\uDCC4 完整日志: ${logPath}`
18193
+ ];
18194
+ if (worktreePath)
18195
+ lines.push(`\uD83D\uDD0D worktree 保留: ${worktreePath}`);
18196
+ lines.push(`\uD83D\uDCA1 排查: cat ${logPath} | tail -50`);
18197
+ return lines.join(`
18198
+ `);
18199
+ }
18200
+ async function appendSubagentLog(filePath, line, log7) {
18201
+ try {
18202
+ await fsPromises.mkdir(path18.dirname(filePath), { recursive: true });
18203
+ await fsPromises.appendFile(filePath, line + `
18204
+ `, "utf8");
18205
+ } catch (err) {
18206
+ log7?.debug?.("appendSubagentLog 失败(已隔离)", {
18207
+ error: err instanceof Error ? err.message : String(err),
18208
+ file: filePath
18209
+ });
18210
+ }
18211
+ }
18212
+ function isLogPersistenceEnabled(cwd) {
18213
+ try {
18214
+ const cfg = getCodeforgeConfig({ root: cwd });
18215
+ const runtime = cfg.runtime;
18216
+ if (runtime && typeof runtime === "object") {
18217
+ const v = runtime.subagent_log_persistence;
18218
+ if (v === false)
18219
+ return false;
18220
+ }
18221
+ return true;
18222
+ } catch {
18223
+ return true;
18224
+ }
18225
+ }
18030
18226
  function normalizeVariant2(raw) {
18031
18227
  if (raw === "info" || raw === "warning")
18032
18228
  return "default";
@@ -18061,12 +18257,17 @@ var subtaskHeartbeatServer = async (ctx) => {
18061
18257
  intervalMs: HEARTBEAT_INTERVAL_MS2
18062
18258
  });
18063
18259
  const client = ctx.client;
18260
+ const cwd = ctx.directory;
18064
18261
  const interval = setInterval(() => {
18065
18262
  safeAsync(PLUGIN_NAME13, "interval", async () => {
18066
18263
  const swept = sweepExpiredPendingTasks();
18067
18264
  if (swept > 0) {
18068
18265
  safeWriteLog(PLUGIN_NAME13, { hook: "interval", pending_task_swept: swept });
18069
18266
  }
18267
+ const sweptParents = sweepExpiredSessionParents();
18268
+ if (sweptParents > 0) {
18269
+ safeWriteLog(PLUGIN_NAME13, { hook: "interval", session_parent_swept: sweptParents });
18270
+ }
18070
18271
  const beats = pickHeartbeats();
18071
18272
  if (beats.length === 0)
18072
18273
  return;
@@ -18091,8 +18292,29 @@ var subtaskHeartbeatServer = async (ctx) => {
18091
18292
  return {
18092
18293
  event: async ({ event }) => {
18093
18294
  await safeAsync(PLUGIN_NAME13, "event", async () => {
18295
+ try {
18296
+ if (detectUnparsedParentID(event) && _parentParseFailLogged < PARENT_PARSE_FAIL_MAX_LOG) {
18297
+ _parentParseFailLogged++;
18298
+ log7.warn("session.created 含 parentID 关键字但 extractCreatedChild 解析失败,可能 SDK schema 变更", {
18299
+ sample_count: _parentParseFailLogged,
18300
+ hint: "如频繁出现请检查 plugins/subtask-heartbeat.ts::extractCreatedChild 是否适配新 SDK"
18301
+ });
18302
+ safeWriteLog(PLUGIN_NAME13, {
18303
+ hook: "event",
18304
+ type: "session.created.unparsed-parent-id",
18305
+ sample_count: _parentParseFailLogged
18306
+ });
18307
+ }
18308
+ } catch {}
18094
18309
  const created = extractCreatedChild(event);
18095
18310
  if (created) {
18311
+ try {
18312
+ recordSessionParent(created.childID, created.parentID);
18313
+ } catch (err) {
18314
+ log7.warn("recordSessionParent 抛错(已隔离)", {
18315
+ error: err instanceof Error ? err.message : String(err)
18316
+ });
18317
+ }
18096
18318
  const pending = dequeuePendingTask(created.parentID);
18097
18319
  const record = registerInflight({
18098
18320
  childID: created.childID,
@@ -18122,6 +18344,11 @@ var subtaskHeartbeatServer = async (ctx) => {
18122
18344
  }
18123
18345
  const ended = extractEndedSessionID(event);
18124
18346
  if (ended) {
18347
+ if (ended.type === "session.deleted") {
18348
+ try {
18349
+ deleteSessionParent(ended.sessionID);
18350
+ } catch {}
18351
+ }
18125
18352
  const r = clearInflight2(ended.sessionID);
18126
18353
  if (r) {
18127
18354
  const t = buildEndToast(r, ended.type);
@@ -18134,6 +18361,32 @@ var subtaskHeartbeatServer = async (ctx) => {
18134
18361
  toast_sent: sent,
18135
18362
  end_toast_message: t.message
18136
18363
  });
18364
+ const logPath = subagentLogPath(cwd, r.parentID, r.childID);
18365
+ const isSuccess = ended.type === "session.idle";
18366
+ let text;
18367
+ if (isSuccess) {
18368
+ text = buildSuccessNotice(r, logPath);
18369
+ } else {
18370
+ let worktreePath = null;
18371
+ try {
18372
+ const entry = await getSessionWorktree(r.childID, cwd);
18373
+ worktreePath = entry?.worktreePath ?? null;
18374
+ } catch {}
18375
+ text = buildFailureNotice(r, ended.type, logPath, worktreePath);
18376
+ }
18377
+ const ocClient = ctx.client;
18378
+ const noticeSent = await sendParentNotice(ocClient, r.parentID, text, {
18379
+ directory: cwd,
18380
+ log: (lvl, msg, data) => log7[lvl === "info" ? "info" : "warn"](msg, data)
18381
+ });
18382
+ safeWriteLog(PLUGIN_NAME13, {
18383
+ hook: "event",
18384
+ type: `${ended.type}.notice`,
18385
+ child: r.childID,
18386
+ parent: r.parentID,
18387
+ notice_sent: noticeSent,
18388
+ success: isSuccess
18389
+ });
18137
18390
  }
18138
18391
  }
18139
18392
  });
@@ -18167,8 +18420,44 @@ var subtaskHeartbeatServer = async (ctx) => {
18167
18420
  });
18168
18421
  }
18169
18422
  }
18170
- if (inflight3.has(input.sessionID)) {
18423
+ const rec = inflight3.get(input.sessionID);
18424
+ if (rec) {
18171
18425
  recordToolBeat(input.sessionID, input.tool);
18426
+ if (!rec.pendingCalls)
18427
+ rec.pendingCalls = new Map;
18428
+ if (typeof input.callID === "string") {
18429
+ rec.pendingCalls.set(input.callID, Date.now());
18430
+ }
18431
+ if (isLogPersistenceEnabled(cwd)) {
18432
+ const args = output?.args ?? null;
18433
+ const line = buildBeforeLogLine(input.tool, args);
18434
+ const file = subagentLogPath(cwd, rec.parentID, rec.childID);
18435
+ appendSubagentLog(file, line, log7);
18436
+ }
18437
+ }
18438
+ });
18439
+ },
18440
+ "tool.execute.after": async (input, _output) => {
18441
+ if (inflight3.size === 0)
18442
+ return;
18443
+ await safeAsync(PLUGIN_NAME13, "tool.execute.after", async () => {
18444
+ if (!input || typeof input.sessionID !== "string" || typeof input.tool !== "string")
18445
+ return;
18446
+ const rec = inflight3.get(input.sessionID);
18447
+ if (!rec)
18448
+ return;
18449
+ let durationMs = 0;
18450
+ if (rec.pendingCalls && typeof input.callID === "string") {
18451
+ const startedAt = rec.pendingCalls.get(input.callID);
18452
+ if (typeof startedAt === "number") {
18453
+ durationMs = Date.now() - startedAt;
18454
+ rec.pendingCalls.delete(input.callID);
18455
+ }
18456
+ }
18457
+ if (isLogPersistenceEnabled(cwd)) {
18458
+ const line = buildAfterLogLine(input.tool, true, durationMs);
18459
+ const file = subagentLogPath(cwd, rec.parentID, rec.childID);
18460
+ appendSubagentLog(file, line, log7);
18172
18461
  }
18173
18462
  });
18174
18463
  }
@@ -18325,20 +18614,20 @@ function loadAgentToolsMap(rootDir, opts = {}) {
18325
18614
  for (const entry of entries) {
18326
18615
  if (!entry.endsWith(".md"))
18327
18616
  continue;
18328
- const path18 = join15(dir, entry);
18617
+ const path19 = join15(dir, entry);
18329
18618
  let content;
18330
18619
  try {
18331
- content = reader(path18);
18620
+ content = reader(path19);
18332
18621
  } catch (err) {
18333
18622
  log8.warn(`agent.md 读取失败(已跳过)`, {
18334
- path: path18,
18623
+ path: path19,
18335
18624
  error: err instanceof Error ? err.message : String(err)
18336
18625
  });
18337
18626
  continue;
18338
18627
  }
18339
18628
  const parsed = parseAgentFrontmatter(content);
18340
18629
  if (!parsed) {
18341
- log8.warn(`agent frontmatter 解析失败(已跳过)`, { path: path18 });
18630
+ log8.warn(`agent frontmatter 解析失败(已跳过)`, { path: path19 });
18342
18631
  continue;
18343
18632
  }
18344
18633
  if (result.has(parsed.name))
@@ -18531,7 +18820,7 @@ var handler16 = async (_ctx3) => {
18531
18820
  // lib/event-stream.ts
18532
18821
  import { promises as fs15 } from "node:fs";
18533
18822
  init_runtime_paths();
18534
- import * as path18 from "node:path";
18823
+ import * as path19 from "node:path";
18535
18824
  async function loadSession(id, opts = {}) {
18536
18825
  const file = resolveSessionFile(id, opts);
18537
18826
  const raw = await fs15.readFile(file, "utf8");
@@ -18551,7 +18840,7 @@ async function listSessions(opts = {}) {
18551
18840
  for (const e of entries) {
18552
18841
  if (!e.isFile() || !e.name.endsWith(".jsonl"))
18553
18842
  continue;
18554
- const file = path18.join(dir, e.name);
18843
+ const file = path19.join(dir, e.name);
18555
18844
  const id = e.name.replace(/\.jsonl$/, "");
18556
18845
  try {
18557
18846
  const stat = await fs15.stat(file);
@@ -18578,11 +18867,11 @@ async function listSessions(opts = {}) {
18578
18867
  return out;
18579
18868
  }
18580
18869
  function resolveDir(opts = {}) {
18581
- const root = path18.resolve(opts.root ?? process.cwd());
18582
- return opts.sessions_dir ? path18.resolve(root, opts.sessions_dir) : path18.join(runtimeDir(root), "sessions");
18870
+ const root = path19.resolve(opts.root ?? process.cwd());
18871
+ return opts.sessions_dir ? path19.resolve(root, opts.sessions_dir) : path19.join(runtimeDir(root), "sessions");
18583
18872
  }
18584
18873
  function resolveSessionFile(id, opts = {}) {
18585
- return path18.join(resolveDir(opts), `${id}.jsonl`);
18874
+ return path19.join(resolveDir(opts), `${id}.jsonl`);
18586
18875
  }
18587
18876
  function parseJsonl(id, raw) {
18588
18877
  const events = [];
@@ -18939,7 +19228,7 @@ var handler17 = sessionRecoveryServer;
18939
19228
 
18940
19229
  // plugins/subtasks.ts
18941
19230
  import { promises as fs16 } from "node:fs";
18942
- import * as path19 from "node:path";
19231
+ import * as path20 from "node:path";
18943
19232
 
18944
19233
  // lib/parallel-merge.ts
18945
19234
  init_autonomy();
@@ -19764,7 +20053,7 @@ function sleep2(ms) {
19764
20053
  init_runtime_paths();
19765
20054
  var PLUGIN_NAME18 = "subtasks";
19766
20055
  function getLogFile(root = process.cwd()) {
19767
- return path19.join(runtimeDir(root), "logs", "subtasks.log");
20056
+ return path20.join(runtimeDir(root), "logs", "subtasks.log");
19768
20057
  }
19769
20058
  var VERB_RE = /^([a-zA-Z]{3,12})/;
19770
20059
  var CN_VERBS = [
@@ -20069,7 +20358,7 @@ async function writeLog(level, msg, data) {
20069
20358
  `;
20070
20359
  try {
20071
20360
  const logFile = getLogFile();
20072
- await fs16.mkdir(path19.dirname(logFile), { recursive: true });
20361
+ await fs16.mkdir(path20.dirname(logFile), { recursive: true });
20073
20362
  await fs16.appendFile(logFile, line, "utf8");
20074
20363
  } catch {}
20075
20364
  }
@@ -20587,11 +20876,11 @@ var handler20 = tokenManagerServer;
20587
20876
 
20588
20877
  // plugins/tool-policy.ts
20589
20878
  import { promises as fs17 } from "node:fs";
20590
- import * as path21 from "node:path";
20879
+ import * as path22 from "node:path";
20591
20880
  init_autonomy();
20592
20881
 
20593
20882
  // lib/file-regex-acl.ts
20594
- import * as path20 from "node:path";
20883
+ import * as path21 from "node:path";
20595
20884
  function compileRule(r) {
20596
20885
  if (r instanceof RegExp)
20597
20886
  return r;
@@ -20657,7 +20946,7 @@ function normalizePath2(p) {
20657
20946
  let s = p.replace(/\\/g, "/");
20658
20947
  if (s.startsWith("./"))
20659
20948
  s = s.slice(2);
20660
- s = path20.posix.normalize(s);
20949
+ s = path21.posix.normalize(s);
20661
20950
  return s;
20662
20951
  }
20663
20952
  function checkFileAccess(acl, file, op) {
@@ -20767,9 +21056,9 @@ function decideToolCall(ctx, cfg = {}, currentAgent) {
20767
21056
  action = "deny";
20768
21057
  return { action, reasons, autonomy: a, acl: aclResults };
20769
21058
  }
20770
- var POLICY_PATH = path21.join(".codeforge", "policy.json");
21059
+ var POLICY_PATH = path22.join(".codeforge", "policy.json");
20771
21060
  async function loadPolicy(root = process.cwd()) {
20772
- const file = path21.join(root, POLICY_PATH);
21061
+ const file = path22.join(root, POLICY_PATH);
20773
21062
  try {
20774
21063
  const raw = await fs17.readFile(file, "utf8");
20775
21064
  const data = JSON.parse(raw);
@@ -20898,7 +21187,7 @@ import {
20898
21187
  writeFileSync as writeFileSync2
20899
21188
  } from "node:fs";
20900
21189
  import { homedir as homedir7, tmpdir } from "node:os";
20901
- import { dirname as dirname10, join as join19 } from "node:path";
21190
+ import { dirname as dirname11, join as join19 } from "node:path";
20902
21191
  import { fileURLToPath } from "node:url";
20903
21192
  import * as https from "node:https";
20904
21193
  import * as zlib from "node:zlib";
@@ -20906,7 +21195,7 @@ import * as zlib from "node:zlib";
20906
21195
  // lib/version-injected.ts
20907
21196
  function getInjectedVersion() {
20908
21197
  try {
20909
- const v = "0.5.2";
21198
+ const v = "0.5.4";
20910
21199
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
20911
21200
  return v;
20912
21201
  }
@@ -20995,7 +21284,7 @@ function readLocalVersion() {
20995
21284
  return injected;
20996
21285
  try {
20997
21286
  const here = fileURLToPath(import.meta.url);
20998
- const root = dirname10(dirname10(here));
21287
+ const root = dirname11(dirname11(here));
20999
21288
  const pkg = JSON.parse(readFileSync5(join19(root, "package.json"), "utf8"));
21000
21289
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
21001
21290
  } catch {
@@ -21024,7 +21313,7 @@ function readCache(file) {
21024
21313
  }
21025
21314
  function writeCache(file, entry) {
21026
21315
  try {
21027
- mkdirSync3(dirname10(file), { recursive: true });
21316
+ mkdirSync3(dirname11(file), { recursive: true });
21028
21317
  writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
21029
21318
  } catch {}
21030
21319
  }
@@ -21210,7 +21499,7 @@ function extractTarToDir(tarBuf, destRoot) {
21210
21499
  if (typeFlag === "0" || typeFlag === "" || typeFlag === "\x00") {
21211
21500
  const fileBuf = tarBuf.subarray(offset, offset + size);
21212
21501
  const dest = join19(destRoot, fullName);
21213
- mkdirSync3(dirname10(dest), { recursive: true });
21502
+ mkdirSync3(dirname11(dest), { recursive: true });
21214
21503
  writeFileSync2(dest, fileBuf);
21215
21504
  } else if (typeFlag === "5") {
21216
21505
  mkdirSync3(join19(destRoot, fullName), { recursive: true });
@@ -21266,7 +21555,7 @@ function atomicReplaceBundle(opts) {
21266
21555
  if (!existsSync4(source)) {
21267
21556
  throw new Error(`atomic_source_missing: ${source}`);
21268
21557
  }
21269
- mkdirSync3(dirname10(target), { recursive: true });
21558
+ mkdirSync3(dirname11(target), { recursive: true });
21270
21559
  const newPath = `${target}.new`;
21271
21560
  const backupPath = `${target}.bak.${oldVersion}`;
21272
21561
  let strategy = "rename";
@@ -21318,7 +21607,7 @@ function cleanupOldBackups(target, keep) {
21318
21607
  if (keep <= 0)
21319
21608
  return;
21320
21609
  try {
21321
- const dir = dirname10(target);
21610
+ const dir = dirname11(target);
21322
21611
  const base = target.substring(dir.length + 1);
21323
21612
  const prefix = `${base}.bak.`;
21324
21613
  const all = readdirSync3(dir).filter((f) => f.startsWith(prefix)).map((f) => {
@@ -21369,7 +21658,7 @@ function loadCompatibility(opts) {
21369
21658
  function inferPluginRoot() {
21370
21659
  try {
21371
21660
  const here = fileURLToPath(import.meta.url);
21372
- return dirname10(dirname10(here));
21661
+ return dirname11(dirname11(here));
21373
21662
  } catch {
21374
21663
  return null;
21375
21664
  }
@@ -21632,11 +21921,11 @@ async function postToast(ctx, message) {
21632
21921
  var handler22 = updateCheckerServer;
21633
21922
 
21634
21923
  // plugins/workflow-engine.ts
21635
- import * as path23 from "node:path";
21924
+ import * as path24 from "node:path";
21636
21925
 
21637
21926
  // lib/workflow-loader.ts
21638
21927
  import { promises as fs18 } from "node:fs";
21639
- import * as path22 from "node:path";
21928
+ import * as path23 from "node:path";
21640
21929
  import { z as z32 } from "zod";
21641
21930
  var ActionSchema = z32.object({
21642
21931
  tool: z32.string().min(1, "action.tool 不能为空"),
@@ -21749,7 +22038,7 @@ async function loadWorkflowsFromDir(dir) {
21749
22038
  continue;
21750
22039
  if (!/\.ya?ml$/i.test(name))
21751
22040
  continue;
21752
- const full = path22.join(dir, name);
22041
+ const full = path23.join(dir, name);
21753
22042
  const r = await loadWorkflowFromFile(full);
21754
22043
  if (r.ok)
21755
22044
  loaded.push(r);
@@ -22139,7 +22428,7 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
22139
22428
  }
22140
22429
  var workflowEngineServer = async (ctx) => {
22141
22430
  const directory = ctx.directory ?? process.cwd();
22142
- const workflowsDir = path23.join(directory, "workflows");
22431
+ const workflowsDir = path24.join(directory, "workflows");
22143
22432
  ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME23}] preload workflows failed`, {
22144
22433
  error: err instanceof Error ? err.message : String(err)
22145
22434
  }));
@@ -22183,7 +22472,7 @@ var workflowEngineServer = async (ctx) => {
22183
22472
  var handler23 = workflowEngineServer;
22184
22473
 
22185
22474
  // plugins/session-worktree-guard.ts
22186
- import path24 from "node:path";
22475
+ import path25 from "node:path";
22187
22476
  var PLUGIN_NAME24 = "session-worktree-guard";
22188
22477
  logLifecycle(PLUGIN_NAME24, "import", {});
22189
22478
  var WRITE_INTENT_RE = />(?!=)|\btee\b|\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bln\b/;
@@ -22212,7 +22501,7 @@ var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
22212
22501
  function rewritePath(value, mainRoot, worktreeRoot) {
22213
22502
  if (!value)
22214
22503
  return null;
22215
- const resolved = path24.isAbsolute(value) ? value : path24.resolve(mainRoot, value);
22504
+ const resolved = path25.isAbsolute(value) ? value : path25.resolve(mainRoot, value);
22216
22505
  if (resolved === mainRoot)
22217
22506
  return worktreeRoot;
22218
22507
  const prefix = mainRoot.endsWith("/") ? mainRoot : mainRoot + "/";
@@ -22295,6 +22584,29 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
22295
22584
  if (!isWriteOperation(toolName, argsObj, mainRoot)) {
22296
22585
  return;
22297
22586
  }
22587
+ try {
22588
+ const parentId = lookupParentSessionId(sessionId);
22589
+ if (parentId) {
22590
+ const parentEntry = await getSessionWorktree(parentId, mainRoot);
22591
+ if (parentEntry && parentEntry.status === "active") {
22592
+ entry = parentEntry;
22593
+ log13.debug?.(`[child-inherit] session ${sessionId} 继承父 ${parentId} 的 worktree`, { parentSessionId: parentId, worktreePath: parentEntry.worktreePath });
22594
+ safeWriteLog(PLUGIN_NAME24, {
22595
+ hook: "tool.execute.before",
22596
+ tool: toolName,
22597
+ sessionID: input.sessionID,
22598
+ action: "child-inherit",
22599
+ parent_session_id: parentId,
22600
+ parent_branch: parentEntry.branch,
22601
+ parent_worktree: parentEntry.worktreePath
22602
+ });
22603
+ }
22604
+ }
22605
+ } catch (lookupErr) {
22606
+ log13.debug?.("[child-inherit] lookupParentSessionId 抛错(已隔离,退回 lazy-bind)", { error: lookupErr instanceof Error ? lookupErr.message : String(lookupErr) });
22607
+ }
22608
+ }
22609
+ if (!entry) {
22298
22610
  try {
22299
22611
  entry = await bindSessionWorktree({ sessionId, mainRoot });
22300
22612
  log13.info(`[lazy-bind] auto-created worktree for session ${sessionId}`, { branch: entry.branch, path: entry.worktreePath });
@@ -22337,8 +22649,16 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
22337
22649
  }
22338
22650
  }
22339
22651
  if (isWriteOp) {
22340
- const reason = `[session-worktree-guard] DENIED: 当前 session 要求先调用 plan_read(plan_id="${entry.requiredPlanId}") 再执行写操作`;
22341
- log13.warn(reason, { tool: toolName, sessionId, requiredPlanId: entry.requiredPlanId });
22652
+ const inherited = entry.sessionId !== sessionId;
22653
+ const reasonBase = `[session-worktree-guard] DENIED: 当前 session 要求先调用 plan_read(plan_id="${entry.requiredPlanId}") 再执行写操作`;
22654
+ const reason = inherited ? `${reasonBase}
22655
+ [gate-deny] child session=${sessionId} 继承父 entry 但父 session planReadOk=false,父 session=${entry.sessionId} 需先调 plan_read(plan_id="${entry.requiredPlanId}")` : reasonBase;
22656
+ log13.warn(reason, {
22657
+ tool: toolName,
22658
+ sessionId,
22659
+ requiredPlanId: entry.requiredPlanId,
22660
+ inheritedFromParent: inherited ? entry.sessionId : undefined
22661
+ });
22342
22662
  safeWriteLog(PLUGIN_NAME24, {
22343
22663
  hook: "tool.execute.before",
22344
22664
  tool: toolName,
@@ -22497,6 +22817,31 @@ var worktreeLifecyclePlugin = async (ctx) => {
22497
22817
  });
22498
22818
  return;
22499
22819
  }
22820
+ try {
22821
+ const mainHead = await getCurrentMainHead(mainRoot);
22822
+ const worktreeHead = mainHead ? await getCurrentWorktreeHead(entry.worktreePath) : "";
22823
+ if (worktreeHead && worktreeHead === mainHead) {
22824
+ const fastDirty = await isWorktreeDirty(entry.worktreePath);
22825
+ if (!fastDirty) {
22826
+ await discardSession({ sessionId: ended.sessionID, mainRoot });
22827
+ safeWriteLog(PLUGIN_NAME25, {
22828
+ hook: "event",
22829
+ type: ended.type,
22830
+ sessionID: ended.sessionID,
22831
+ action: "discard-empty-fast-path",
22832
+ branch: entry.branch,
22833
+ worktreePath: entry.worktreePath
22834
+ });
22835
+ lastIdleToastAt.delete(ended.sessionID);
22836
+ return;
22837
+ }
22838
+ }
22839
+ } catch (err) {
22840
+ log14.warn(`[lifecycle] empty-worktree fast-path 检测失败 (回退到常规路径)`, {
22841
+ sessionId: ended.sessionID,
22842
+ error: err instanceof Error ? err.message : String(err)
22843
+ });
22844
+ }
22500
22845
  let hadDirty = false;
22501
22846
  try {
22502
22847
  hadDirty = await isWorktreeDirty(entry.worktreePath);
@@ -22646,6 +22991,7 @@ function createCodeforgeServer(opts) {
22646
22991
  const commandExecuteBeforeBucket = [];
22647
22992
  const chatParamsBucket = [];
22648
22993
  const toolExecuteBeforeBucket = [];
22994
+ const toolExecuteAfterBucket = [];
22649
22995
  const chatMessagesTransformBucket = [];
22650
22996
  const chatSystemTransformBucket = [];
22651
22997
  const eventBucket = [];
@@ -22659,6 +23005,8 @@ function createCodeforgeServer(opts) {
22659
23005
  chatParamsBucket.push(h["chat.params"]);
22660
23006
  if (h["tool.execute.before"])
22661
23007
  toolExecuteBeforeBucket.push(h["tool.execute.before"]);
23008
+ if (h["tool.execute.after"])
23009
+ toolExecuteAfterBucket.push(h["tool.execute.after"]);
22662
23010
  if (h["experimental.chat.messages.transform"]) {
22663
23011
  chatMessagesTransformBucket.push(h["experimental.chat.messages.transform"]);
22664
23012
  }
@@ -22675,6 +23023,7 @@ function createCodeforgeServer(opts) {
22675
23023
  "command.execute.before": makeSerialHook("command.execute.before", commandExecuteBeforeBucket),
22676
23024
  "chat.params": makeSerialHook("chat.params", chatParamsBucket),
22677
23025
  "tool.execute.before": makeSerialHook("tool.execute.before", toolExecuteBeforeBucket),
23026
+ "tool.execute.after": makeSerialHook("tool.execute.after", toolExecuteAfterBucket),
22678
23027
  "experimental.chat.messages.transform": makeSerialHook("experimental.chat.messages.transform", chatMessagesTransformBucket),
22679
23028
  "experimental.chat.system.transform": makeSerialHook("experimental.chat.system.transform", chatSystemTransformBucket),
22680
23029
  event: async (envelope) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andyqiu/codeforge",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "CodeForge — opencode 的零侵入扩展包",
5
5
  "type": "module",
6
6
  "private": false,