@zhushanwen/pi-subagent-workflow 5.0.0-dev.1 → 5.0.1

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.
@@ -246,34 +246,69 @@ function normalizeFixResult(raw) {
246
246
  }
247
247
 
248
248
  /**
249
- * ES3 硬校验(5.3 红线):deferred 只允许 minor。deferred[].severity 显式非 minor
250
- * (critical/major)→ 违规(调用方结构化终止 fix-failure)。缺省 severity 视为 minor
251
- * (放行,由 ES2 软校验记 warning)。wave 3 接入数字 ID 后可升级为对账级判定。
252
- * @returns [{ issue_id, severity }] 违规列表;空数组 = 通过
249
+ * issue ID 归一化(ES3 校验与 fix 阶段对账共用键空间):小写 + 剥尾部 "(...)" 尾注。
250
+ * LLM 产出的 ID 漂移形态:大小写("mf-1"/"MF-1")、尾注("MF-1 (fixed)")。空串返回 ""。
253
251
  */
252
+ function normIssueId(s) {
253
+ return String(s ?? "").toLowerCase().replace(/\s*\([^)]*\)\s*$/, "").trim();
254
+ }
255
+
256
+ /**
257
+ * 在 issues 键空间中查找 issue_id 的归一化匹配键(不存在返回 undefined)。
258
+ * fix 阶段(fix-attempted/deferred 标记)与 ES3 校验共用——精确键查表会把
259
+ * "mf-1"/"MF-1 (fixed)" 等漂移 ID 判为未追踪,导致 fix-attempted → fixed/regressed
260
+ * → needs-redesign 状态链静默失效;deferred 侧漂移则创建幽灵条目(原条目仍 open 阻塞收敛)。
261
+ */
262
+ function findIssueKey(issues, issueId) {
263
+ if (!issues || typeof issueId !== "string" || !issueId) return undefined;
264
+ if (issues[issueId]) return issueId;
265
+ const norm = normIssueId(issueId);
266
+ if (!norm) return undefined;
267
+ for (const key of Object.keys(issues)) {
268
+ if (normIssueId(key) === norm) return key;
269
+ }
270
+ return undefined;
271
+ }
272
+
254
273
  /**
255
274
  * ES3 硬校验(5.3-P1 红线):(1) deferred 只允许 minor/trivial;(2) must-fix 必须全进
256
275
  * fixes[]——mustFixIds 中未修复且未显式处理的 ID 判 violation(漏修)。mustFixIds
257
276
  * 为 null/undefined 时仅做 (1)(无 aggregator 数据的降级路径,wave 2 限制)。
277
+ * trackedIssues(state.issues)可选:deferred 的 severity 与追踪表交叉核对(MF-4)——
278
+ * 追踪条目以追踪 severity 为准(must-fix 追踪皆 critical/major,defer 即违规),
279
+ * 仅追踪无此 ID(S-x minor)时采信 fix agent 自报。
258
280
  */
259
- function validateFixResult(result, mustFixIds) {
281
+ function validateFixResult(result, mustFixIds, trackedIssues) {
260
282
  const violations = [];
261
283
  for (const d of result.deferred || []) {
262
284
  if (!d) continue;
263
285
  const sev = typeof d.severity === "string" ? d.severity.toLowerCase() : "";
264
- if (sev && sev !== "minor" && sev !== "trivial") {
265
- violations.push({ issue_id: d.issue_id || "(unnamed)", severity: sev });
286
+ // m9: 自报 severity 可被单边绕过(fix agent 与审核方同一 LLM,有少干活动机,
287
+ // must-fix minor 塞进 deferred 即过旧校验)——与追踪表交叉核对:
288
+ // trackedIssues 中能找到的 ID 以其追踪 severity 为准;追踪表无此 ID 采信自报。
289
+ let effectiveSev = sev;
290
+ if (trackedIssues && typeof d.issue_id === "string" && d.issue_id) {
291
+ const trackedKey = findIssueKey(trackedIssues, d.issue_id);
292
+ const trackedSev = trackedKey ? trackedIssues[trackedKey].severity : undefined;
293
+ const ts = typeof trackedSev === "string" ? trackedSev.toLowerCase() : "";
294
+ // 仅认真实 severity 等级(critical/major/minor/trivial);"unknown"(reconcile 新
295
+ // ID 默认)等非等级值不覆盖自报,避免误伤合法 minor deferral
296
+ if (ts === "critical" || ts === "major" || ts === "minor" || ts === "trivial") {
297
+ effectiveSev = ts;
298
+ }
299
+ }
300
+ if (effectiveSev && effectiveSev !== "minor" && effectiveSev !== "trivial") {
301
+ violations.push({ issue_id: d.issue_id || "(unnamed)", severity: effectiveSev });
266
302
  }
267
303
  }
268
304
  if (Array.isArray(mustFixIds) && mustFixIds.length > 0) {
269
305
  // m3: ID 归一化比较——大小写 + 尾部括号尾注(如 "(fixed)")漂移不误杀:
270
306
  // 严格 trim 比较会把 "mf-1"/"MF-1 (fixed)" 判漏修,整轮 fix-failure 误杀
271
- const normId = (s) => String(s).toLowerCase().replace(/\s*\([^)]*\)\s*$/, "").trim();
272
307
  const fixedIds = new Set((result.fixes || [])
273
- .map((f) => (f && typeof f.issue_id === "string" ? normId(f.issue_id) : ""))
308
+ .map((f) => (f && typeof f.issue_id === "string" ? normIssueId(f.issue_id) : ""))
274
309
  .filter(Boolean));
275
310
  for (const id of mustFixIds) {
276
- const norm = typeof id === "string" ? normId(id) : (id && typeof id.id === "string" ? normId(id.id) : "");
311
+ const norm = typeof id === "string" ? normIssueId(id) : (id && typeof id.id === "string" ? normIssueId(id.id) : "");
277
312
  if (norm && !fixedIds.has(norm)) {
278
313
  violations.push({ issue_id: norm, severity: "must-fix-not-fixed" });
279
314
  }
@@ -820,6 +855,8 @@ module.exports = {
820
855
  resolveReviewReportPath,
821
856
  normalizeFixResult,
822
857
  validateFixResult,
858
+ normIssueId,
859
+ findIssueKey,
823
860
  reconcileIssues,
824
861
  normalizeReviewResult,
825
862
  computeKnownRemaining,
@@ -22,7 +22,7 @@
22
22
 
23
23
  const meta = {
24
24
  name: "review-fix-loop",
25
- description: "审查-修复循环:多批串行(批内并行 review → aggregate → fix → 重审直到 clean)。必填 targetType(git-diff/file/dir/text)+ target。批次由 batch1..batchN 控制(如 batch1=fallow-scan batch2=reviewer),用于前置检查先行的场景。注意:唯一带写操作/commit 副作用的内置 workflow,autoCommit 默认 false;skipCleanAgents 默认 true + recheckAfterFix 默认 false(clean agent 下轮跳过,与字面语义一致);传 recheckAfterFix=true 启用可选强回归模式(fix 后重派全批,clean agent 走限定 prompt 只审改动文件)。可选 fixAgent/maxFixAttempts/convergeNewIssues/convergeRounds 控制修复 agent 与收敛终止(详见 workflows/README.md)。",
25
+ description: "审查-修复循环:多批串行(批内并行 review → aggregate → fix → 重审直到 clean)。必填 targetType(git-diff/file/dir/text)+ target。批次由必填参数 batch1..batchN 控制(无默认,至少传一个;agents 为单批简写;如 batch1=fallow-scan batch2=reviewer),用于前置检查先行的场景。注意:唯一带写操作/commit 副作用的内置 workflow,autoCommit 默认 false;skipCleanAgents 默认 true + recheckAfterFix 默认 false(clean agent 下轮跳过,与字面语义一致);传 recheckAfterFix=true 启用可选强回归模式(fix 后重派全批,clean agent 走限定 prompt 只审改动文件)。可选 fixAgent/maxFixAttempts/convergeNewIssues/convergeRounds 控制修复 agent 与收敛终止(详见 workflows/README.md)。",
26
26
  phases: ["Review", "Fix"],
27
27
  };
28
28
 
@@ -56,6 +56,7 @@ const {
56
56
  resolveReviewReportPath,
57
57
  normalizeFixResult,
58
58
  validateFixResult,
59
+ findIssueKey,
59
60
  reconcileIssues,
60
61
  normalizeReviewResult,
61
62
  computeKnownRemaining,
@@ -823,8 +824,10 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
823
824
 
824
825
  // ES3 硬校验(5.3 红线,恢复 mustFixIds 交叉校验——wave 3 后 agg.must_fix_ids
825
826
  // 已是标准字段):(1) deferred 只允许 minor;(2) must-fix 必须全进 fixes[](漏修
826
- // 判 violation)。任一违规 → fix-failure(结构化终止)
827
- const es3Violations = validateFixResult(fixResult, agg.must_fix_ids);
827
+ // 判 violation)。任一违规 → fix-failure(结构化终止)。trackedIssues 传入
828
+ // state.issues——deferred severity 与追踪表交叉核对(MF-4):must-fix 被标 minor
829
+ // 塞进 deferred 的逃逸路径在追踪表面前失效(追踪 severity 为准)。
830
+ const es3Violations = validateFixResult(fixResult, agg.must_fix_ids, state.issues);
828
831
  if (es3Violations.length > 0) {
829
832
  // m7: violation 分两类——deferred 非 minor / must-fix 漏修(must-fix-not-fixed),
830
833
  // finalMessage 文案区分:统一文案会把漏修误报成 defer 违规,误导修复方向
@@ -867,22 +870,30 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
867
870
  // 写入与 knownRemaining 同步链路生效(否则 knownRemaining 恒空,deferred 跨轮继承整链失效)
868
871
  if (!state.issues) state.issues = {};
869
872
  // 5.1:fix 结果标记 fix-attempted(ID 对账驱动)+ fixResults 落库(R2+ prompt 输入)
873
+ // 归一化查表(findIssueKey,与 ES3 同键空间):fix agent ID 漂移("mf-1"/
874
+ // "MF-1 (fixed)")不再丢匹配——精确键查表时 issue 停留 open,reconcile 无
875
+ // fix-attempted 可转 fixed/regressed,needs-redesign 出口对该类 ID 静默失效。
870
876
  for (const f of fixResult.fixes) {
871
- if (f && typeof f.issue_id === "string" && state.issues[f.issue_id]) {
872
- state.issues[f.issue_id].status = "fix-attempted";
873
- state.issues[f.issue_id].history.push({ round, status: "fix-attempted" });
877
+ if (f && typeof f.issue_id === "string") {
878
+ const trackedKey = findIssueKey(state.issues, f.issue_id);
879
+ if (trackedKey) {
880
+ state.issues[trackedKey].status = "fix-attempted";
881
+ state.issues[trackedKey].history.push({ round, status: "fix-attempted" });
882
+ }
874
883
  }
875
884
  }
876
885
  // 5.3-4 deferred 写入 state.issues(known-remaining 跨轮继承链路):deferred 条目
877
886
  // 以 status=deferred 入 issues,据此生成 knownRemaining 传给 R2+ prompt。
878
- // ID 已存在(曾被修复/降级)→ 更新状态 + reason;不存在(S-x minor)→ 新建。
887
+ // ID 已存在(曾被修复/降级,含大小写/尾注漂移)→ 更新状态 + reason
888
+ // 不存在(S-x minor)→ 新建。漂移 ID 归一化匹配防止幽灵条目(原条目仍 open 阻塞收敛)。
879
889
  for (const d of fixResult.deferred) {
880
890
  if (!d || typeof d.issue_id !== "string" || !d.issue_id) continue;
881
891
  const reason = typeof d.reason === "string" ? d.reason : "";
882
- if (state.issues[d.issue_id]) {
883
- state.issues[d.issue_id].status = "deferred";
884
- state.issues[d.issue_id].deferredReason = reason;
885
- state.issues[d.issue_id].history.push({ round, status: "deferred" });
892
+ const trackedKey = findIssueKey(state.issues, d.issue_id);
893
+ if (trackedKey) {
894
+ state.issues[trackedKey].status = "deferred";
895
+ state.issues[trackedKey].deferredReason = reason;
896
+ state.issues[trackedKey].history.push({ round, status: "deferred" });
886
897
  } else {
887
898
  state.issues[d.issue_id] = {
888
899
  firstSeen: round, severity: "minor", status: "deferred",