@ifrankwang/openspec-agents 0.130.0 → 0.132.0

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.
@@ -29686,6 +29686,7 @@ var EMPTY_COMPLETION_RESULT = {
29686
29686
 
29687
29687
  // src/core/tools/lifecycle.ts
29688
29688
  import path6 from "path";
29689
+ import { rmdir } from "node:fs/promises";
29689
29690
 
29690
29691
  // src/core/types.ts
29691
29692
  var CODE_DIMENSIONS = ["style", "architecture", "performance", "security", "maintainability"];
@@ -29693,6 +29694,7 @@ var REVIEW_DIMENSIONS = [...CODE_DIMENSIONS];
29693
29694
  var BUILD_PHASE_TARGETS = ["task_analysis", "dev_impl", "review"];
29694
29695
  var REVIEW_LAYERS = ["tool", "task", "quality"];
29695
29696
  var REVIEW_VERIFY_STEPS = ["verify_tool", "verify_task", "verify_quality"];
29697
+ var SIMPLE_REVIEW_STEPS = ["quality_review"];
29696
29698
 
29697
29699
  // src/core/constants.ts
29698
29700
  var STATE_DIR_NAME = "openspec";
@@ -29742,7 +29744,15 @@ function reviewLayerFromMetadata(child) {
29742
29744
  // src/core/git.ts
29743
29745
  import path from "path";
29744
29746
  import { execFile } from "node:child_process";
29745
- import { readFile, writeFile, stat } from "node:fs/promises";
29747
+ import { readFile, writeFile, stat, rm } from "node:fs/promises";
29748
+ async function pathExists(p) {
29749
+ try {
29750
+ await stat(p);
29751
+ return true;
29752
+ } catch {
29753
+ return false;
29754
+ }
29755
+ }
29746
29756
  function execGit(args) {
29747
29757
  return new Promise((resolve) => {
29748
29758
  const child = execFile("git", args, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
@@ -29775,6 +29785,55 @@ async function runGit(worktree, args) {
29775
29785
  async function runGitChecked(worktree, args) {
29776
29786
  return gitRunner.runChecked(worktree, args);
29777
29787
  }
29788
+ async function removeTaskGroupWorktree(repoRoot, wtPath, opts = {}) {
29789
+ const result = { dirResolved: false, dirResolvedByFallback: false, pruned: false, branchDeleted: null, errors: [] };
29790
+ if (!await pathExists(wtPath)) {
29791
+ const pruneRes = await runGitChecked(repoRoot, ["worktree", "prune"]);
29792
+ if (pruneRes.success)
29793
+ result.pruned = true;
29794
+ else
29795
+ result.errors.push(`worktree prune 失败:${pruneRes.stderr}`);
29796
+ result.dirResolved = true;
29797
+ } else {
29798
+ const rmRes = await runGitChecked(repoRoot, ["worktree", "remove", wtPath, "--force"]);
29799
+ if (!rmRes.success)
29800
+ result.errors.push(`git worktree remove 失败:${rmRes.stderr}`);
29801
+ if (await pathExists(wtPath)) {
29802
+ try {
29803
+ await rm(wtPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
29804
+ } catch (e) {
29805
+ result.errors.push(`文件系统兜底删除 "${wtPath}" 失败:${e instanceof Error ? e.message : String(e)}`);
29806
+ }
29807
+ const pruneRes = await runGitChecked(repoRoot, ["worktree", "prune"]);
29808
+ if (pruneRes.success)
29809
+ result.pruned = true;
29810
+ else
29811
+ result.errors.push(`worktree prune 失败:${pruneRes.stderr}`);
29812
+ if (!await pathExists(wtPath)) {
29813
+ result.dirResolved = true;
29814
+ result.dirResolvedByFallback = true;
29815
+ }
29816
+ } else {
29817
+ result.dirResolved = true;
29818
+ }
29819
+ }
29820
+ if (!result.dirResolved) {
29821
+ if (opts.branchName) {
29822
+ result.errors.push(`目录 "${wtPath}" 未能删除,跳过分支 "${opts.branchName}" 的删除以保留排查线索`);
29823
+ }
29824
+ return result;
29825
+ }
29826
+ if (opts.branchName) {
29827
+ const branchRes = await runGitChecked(repoRoot, ["branch", "-D", opts.branchName]);
29828
+ if (branchRes.success)
29829
+ result.branchDeleted = true;
29830
+ else {
29831
+ result.branchDeleted = false;
29832
+ result.errors.push(`删除分支 "${opts.branchName}" 失败:${branchRes.stderr}`);
29833
+ }
29834
+ }
29835
+ return result;
29836
+ }
29778
29837
  async function getCurrentHead(worktree) {
29779
29838
  return runGit(worktree, ["rev-parse", "HEAD"]);
29780
29839
  }
@@ -30061,6 +30120,48 @@ function stepAgentIds(step) {
30061
30120
  return step.agents.map((a) => a.id);
30062
30121
  }
30063
30122
 
30123
+ // src/core/task-children.ts
30124
+ function taskChildrenOf(item) {
30125
+ return (item.children ?? []).filter((c) => c.type === "task");
30126
+ }
30127
+ function issueChildrenOf(item) {
30128
+ return (item.children ?? []).filter((c) => c.type === "issue");
30129
+ }
30130
+ function taskChildById(item, id) {
30131
+ return item.children.find((c) => c.type === "task" && (c.id === id || c.externalId === id)) ?? null;
30132
+ }
30133
+ function normalizeTaskChildIds(rawIds, item) {
30134
+ const byNumber = new Map;
30135
+ for (const c of taskChildrenOf(item)) {
30136
+ if (c.externalId !== undefined)
30137
+ byNumber.set(c.externalId, c.id);
30138
+ }
30139
+ return rawIds.map((id) => byNumber.get(id) ?? id);
30140
+ }
30141
+ function phaseToTaskStatus(phase, hasRejectReason) {
30142
+ switch (phase) {
30143
+ case "review":
30144
+ return "submitted";
30145
+ case "done":
30146
+ case "cancelled":
30147
+ return "verified";
30148
+ case "todo":
30149
+ return hasRejectReason ? "rejected" : "open";
30150
+ default:
30151
+ return "open";
30152
+ }
30153
+ }
30154
+ function taskListOf(item) {
30155
+ return taskChildrenOf(item).map((c) => ({
30156
+ id: c.id,
30157
+ specTrace: typeof c.metadata["specTrace"] === "string" ? c.metadata["specTrace"] : "",
30158
+ title: c.title,
30159
+ status: phaseToTaskStatus(c.phase, c.metadata["reject_reason"] !== undefined),
30160
+ taskNumber: typeof c.externalId === "string" ? c.externalId : "",
30161
+ rejectReason: typeof c.metadata["reject_reason"] === "string" ? c.metadata["reject_reason"] : null
30162
+ }));
30163
+ }
30164
+
30064
30165
  // src/core/workflow/engine.ts
30065
30166
  var REVIEW_STEP_TO_LAYER = {
30066
30167
  verify_tool: "tool",
@@ -30443,6 +30544,14 @@ function recommendForItem(item, workflow) {
30443
30544
  }
30444
30545
  const gateBlock = forwardAdvanceBlockReason(item, workflow, current.step);
30445
30546
  if (gateBlock) {
30547
+ if (current.phaseName === "review" && taskChildrenOf(item).some((c) => c.phase === "review")) {
30548
+ return {
30549
+ status: "blocked",
30550
+ stepId: current.step.id,
30551
+ agents: stepAgentIds(current.step),
30552
+ blockedReason: `${gateBlock}需该 step 审查者经 opx_agent_submit 补交 verified_tasks(覆盖全部待验证任务)后推进。`
30553
+ };
30554
+ }
30446
30555
  return {
30447
30556
  status: "blocked",
30448
30557
  stepId: current.step.id,
@@ -30465,48 +30574,6 @@ function recommendForItem(item, workflow) {
30465
30574
  };
30466
30575
  }
30467
30576
 
30468
- // src/core/task-children.ts
30469
- function taskChildrenOf(item) {
30470
- return (item.children ?? []).filter((c) => c.type === "task");
30471
- }
30472
- function issueChildrenOf(item) {
30473
- return (item.children ?? []).filter((c) => c.type === "issue");
30474
- }
30475
- function taskChildById(item, id) {
30476
- return item.children.find((c) => c.type === "task" && (c.id === id || c.externalId === id)) ?? null;
30477
- }
30478
- function normalizeTaskChildIds(rawIds, item) {
30479
- const byNumber = new Map;
30480
- for (const c of taskChildrenOf(item)) {
30481
- if (c.externalId !== undefined)
30482
- byNumber.set(c.externalId, c.id);
30483
- }
30484
- return rawIds.map((id) => byNumber.get(id) ?? id);
30485
- }
30486
- function phaseToTaskStatus(phase, hasRejectReason) {
30487
- switch (phase) {
30488
- case "review":
30489
- return "submitted";
30490
- case "done":
30491
- case "cancelled":
30492
- return "verified";
30493
- case "todo":
30494
- return hasRejectReason ? "rejected" : "open";
30495
- default:
30496
- return "open";
30497
- }
30498
- }
30499
- function taskListOf(item) {
30500
- return taskChildrenOf(item).map((c) => ({
30501
- id: c.id,
30502
- specTrace: typeof c.metadata["specTrace"] === "string" ? c.metadata["specTrace"] : "",
30503
- title: c.title,
30504
- status: phaseToTaskStatus(c.phase, c.metadata["reject_reason"] !== undefined),
30505
- taskNumber: typeof c.externalId === "string" ? c.externalId : "",
30506
- rejectReason: typeof c.metadata["reject_reason"] === "string" ? c.metadata["reject_reason"] : null
30507
- }));
30508
- }
30509
-
30510
30577
  // src/core/workflow/submit.ts
30511
30578
  function resolveReviewStepForSource(workflow, source) {
30512
30579
  for (const phase of workflow.phases) {
@@ -32695,16 +32762,21 @@ function requireLoader() {
32695
32762
  state.result += _result;
32696
32763
  }
32697
32764
  }
32765
+ function chargeMergeWork(state) {
32766
+ state.totalMergeKeys++;
32767
+ if (state.maxTotalMergeKeys !== -1 && state.totalMergeKeys > state.maxTotalMergeKeys) {
32768
+ throwError(state, "merge keys exceeded maxTotalMergeKeys (" + state.maxTotalMergeKeys + ")");
32769
+ }
32770
+ }
32698
32771
  function mergeMappings(state, destination, source, overridableKeys) {
32699
32772
  if (!common2.isObject(source)) {
32700
32773
  throwError(state, "cannot merge mappings; the provided source object is unacceptable");
32701
32774
  }
32775
+ chargeMergeWork(state);
32702
32776
  const sourceKeys = Object.keys(source);
32703
32777
  for (let index = 0, quantity = sourceKeys.length;index < quantity; index += 1) {
32704
32778
  const key = sourceKeys[index];
32705
- if (state.maxTotalMergeKeys !== -1 && ++state.totalMergeKeys > state.maxTotalMergeKeys) {
32706
- throwError(state, "merge keys exceeded maxTotalMergeKeys (" + state.maxTotalMergeKeys + ")");
32707
- }
32779
+ chargeMergeWork(state);
32708
32780
  if (!_hasOwnProperty.call(destination, key)) {
32709
32781
  setProperty(destination, key, source[key]);
32710
32782
  overridableKeys[key] = true;
@@ -32732,6 +32804,9 @@ function requireLoader() {
32732
32804
  }
32733
32805
  if (keyTag === "tag:yaml.org,2002:merge") {
32734
32806
  if (Array.isArray(valueNode)) {
32807
+ if (valueNode.length > 100) {
32808
+ throwError(state, "abnormal merge sequence size");
32809
+ }
32735
32810
  for (let index = 0, quantity = valueNode.length;index < quantity; index += 1) {
32736
32811
  mergeMappings(state, _result, valueNode[index], overridableKeys);
32737
32812
  }
@@ -35638,6 +35713,7 @@ function renderAnalyzeBlockers(item) {
35638
35713
  }
35639
35714
  function renderDeveloperChildren(item, ctxAgent, exemptionCtx) {
35640
35715
  const lines = [];
35716
+ lines.push(...renderBlockerTrail(item, BLOCKER_TRAIL_TITLE_DEV));
35641
35717
  const toFix = issueChildrenOf(item).filter((c) => isAgentOwnedIssue(c, ctxAgent));
35642
35718
  if (toFix.length === 0)
35643
35719
  return lines;
@@ -35778,6 +35854,21 @@ function renderToolAdjudicateOnly(item, rec, ctxAgent, exemptedHits, exemptionCt
35778
35854
  return lines.join(`
35779
35855
  `);
35780
35856
  }
35857
+ var BLOCKER_TRAIL_TITLE_REVIEW = "## Blocker 留痕(人工/不可验证任务的申报依据)";
35858
+ var BLOCKER_TRAIL_TITLE_DEV = "## Blocker 留痕(已处理 blocker 的用户确认,凭记录申报完成)";
35859
+ function renderBlockerTrail(item, title) {
35860
+ const resolved = readBlockers(item).filter((b) => b.status === "resolved" && b.userResponse);
35861
+ if (resolved.length === 0)
35862
+ return [];
35863
+ const lines = [title, ""];
35864
+ for (const b of resolved) {
35865
+ lines.push(`- Blocker #${b.id}${b.taskId ? ` | Task #${b.taskId}` : ""} | ${b.category}`);
35866
+ lines.push(` - 描述:${b.description}`);
35867
+ lines.push(` - 用户确认:${b.userResponse}`);
35868
+ }
35869
+ lines.push("");
35870
+ return lines;
35871
+ }
35781
35872
  function renderTaskChildren(item, ctxAgent, exemptionCtx) {
35782
35873
  const lines = [];
35783
35874
  const pendingTasks = readTasks(item).filter((t) => t.status === "submitted");
@@ -35787,6 +35878,7 @@ function renderTaskChildren(item, ctxAgent, exemptionCtx) {
35787
35878
  lines.push(renderTaskItem(t));
35788
35879
  lines.push("");
35789
35880
  }
35881
+ lines.push(...renderBlockerTrail(item, BLOCKER_TRAIL_TITLE_REVIEW));
35790
35882
  const issues = issueChildrenOf(item);
35791
35883
  const own = issues.filter((c) => isAgentOwnedIssue(c, ctxAgent));
35792
35884
  lines.push(...renderChildrenSection("Issue (待复核)", own, exemptionCtx));
@@ -35815,6 +35907,7 @@ function renderMergedChildren(item, ctxAgent, exemptionCtx) {
35815
35907
  lines.push(renderTaskItem(t));
35816
35908
  lines.push("");
35817
35909
  }
35910
+ lines.push(...renderBlockerTrail(item, BLOCKER_TRAIL_TITLE_REVIEW));
35818
35911
  const issues = issueChildrenOf(item);
35819
35912
  const own = issues.filter((c) => isAgentOwnedIssue(c, ctxAgent));
35820
35913
  lines.push(...renderChildrenSection("Issue (待复核)", own, exemptionCtx));
@@ -36011,6 +36104,9 @@ function applyRecoveryState(item, recovery, parsedTasks, mode) {
36011
36104
  delete item.tags[key];
36012
36105
  }
36013
36106
  }
36107
+ if ((recovery?.reset_steps ?? []).includes("quality_review")) {
36108
+ clearStepTags(item, "quality_review");
36109
+ }
36014
36110
  item.currentStep = "quality_review";
36015
36111
  syncTaskChildren(item, parsedTasks, { defaultStatus: "done" });
36016
36112
  return;
@@ -36080,12 +36176,17 @@ function assertValidRecovery(recovery) {
36080
36176
  throw new Error("reset_steps 与 review_layer 互斥,不可同时使用。");
36081
36177
  }
36082
36178
  if (recovery.reset_steps.length === 0) {
36083
- throw new Error("reset_steps 不能为空数组,请至少指定一个 verify step。");
36179
+ throw new Error("reset_steps 不能为空数组,请至少指定一个审查 step。");
36084
36180
  }
36085
- for (const stepId of recovery.reset_steps) {
36086
- if (!REVIEW_VERIFY_STEPS.includes(stepId)) {
36087
- throw new Error(`reset_steps 中的 step "${stepId}" 不合法,合法值:${REVIEW_VERIFY_STEPS.join("、")}。传入值:"${stepId}"。`);
36088
- }
36181
+ }
36182
+ }
36183
+ function assertValidResetStepValues(recovery, mode) {
36184
+ if (!recovery?.reset_steps?.length)
36185
+ return;
36186
+ const valid = mode === "simple" ? SIMPLE_REVIEW_STEPS : REVIEW_VERIFY_STEPS;
36187
+ for (const stepId of recovery.reset_steps) {
36188
+ if (!valid.includes(stepId)) {
36189
+ throw new Error(`reset_steps 中的 step "${stepId}" 不属于当前模式(${mode})的审查 step,` + `合法值:${valid.join("、")}。传入值:"${stepId}"。`);
36089
36190
  }
36090
36191
  }
36091
36192
  }
@@ -36168,6 +36269,7 @@ async function initExecute(params, ctx) {
36168
36269
  state.baseBranch = state.baseBranch || baseBranch;
36169
36270
  state.isolationNamespace = state.isolationNamespace || generateIsolationNamespace(state.changeId);
36170
36271
  }
36272
+ assertValidResetStepValues(args.recovery, state.mode ?? "full");
36171
36273
  for (const group of parsedGroups) {
36172
36274
  const isCurrent = group.id === args.task_group_id;
36173
36275
  const groupTasks = tasksByGroup.get(group.id) ?? [];
@@ -36258,6 +36360,11 @@ async function initExecute(params, ctx) {
36258
36360
  parts.push(`已恢复到 ${args.recovery.phase} 阶段。`);
36259
36361
  if (modeSwitchNote)
36260
36362
  parts.push(modeSwitchNote);
36363
+ if (args.recovery?.review_layer && (state.mode ?? "full") === "simple") {
36364
+ parts.push(`
36365
+
36366
+ ⚠️ simple 模式无 review 子层(仅 quality_review 单层审查),recovery.review_layer 参数未生效。`);
36367
+ }
36261
36368
  return parts.join("");
36262
36369
  }
36263
36370
  async function bindWorktreeRefs(item, worktreePath, branch, baseBranch, opts = {}) {
@@ -36329,13 +36436,15 @@ async function setWorktreeExecute(params, ctx) {
36329
36436
  await bindWorktreeRefs(item, existingPath, branch, state.baseBranch);
36330
36437
  reused = true;
36331
36438
  } else {
36332
- const rmResult = await runGitChecked(repoRoot, ["worktree", "remove", existingPath, "--force"]);
36333
- if (!rmResult.success) {
36334
- throw new Error(`无法清理已有 worktree "${existingPath}":${rmResult.stderr}`);
36335
- }
36336
- const branchRmResult = await runGitChecked(repoRoot, ["branch", "-D", branch]);
36337
- if (!branchRmResult.success) {
36338
- throw new Error(`无法清理已有分支 "${branch}":${branchRmResult.stderr}`);
36439
+ const cleanup = await removeTaskGroupWorktree(repoRoot, existingPath, { branchName: branch });
36440
+ if (!cleanup.dirResolved || cleanup.branchDeleted === false) {
36441
+ const problems = [];
36442
+ if (!cleanup.dirResolved)
36443
+ problems.push(`无法清理已有 worktree "${existingPath}"`);
36444
+ if (cleanup.branchDeleted === false)
36445
+ problems.push(`无法清理已有分支 "${branch}"`);
36446
+ throw new Error(`${problems.join(";")}:${cleanup.errors.join(";") || "原因未知"}
36447
+ 请手动处理后重试。`);
36339
36448
  }
36340
36449
  }
36341
36450
  }
@@ -36399,6 +36508,19 @@ async function statusExecute(params, ctx) {
36399
36508
  const exemptionItems = (await readExemptions(ctx.worktree)).items;
36400
36509
  return renderWorkflowStatusView(item, workflow, rec, { agent, orchestrator: ctx.orchestrator, identityDeclared: ctx.identityDeclared }, { state, tg, mainPollution, toolChanges, exemptedHits, exemptionItems });
36401
36510
  }
36511
+ async function sweepEmptyWorktreeParents(repoRoot, removedDir, changeId) {
36512
+ const repoRootAbs = path6.resolve(repoRoot);
36513
+ const groupLevelDir = path6.join(repoRootAbs, ".worktree", changeId);
36514
+ const worktreeLevelDir = path6.join(repoRootAbs, ".worktree");
36515
+ if (path6.dirname(path6.resolve(removedDir)) === groupLevelDir) {
36516
+ try {
36517
+ await rmdir(groupLevelDir);
36518
+ } catch {}
36519
+ }
36520
+ try {
36521
+ await rmdir(worktreeLevelDir);
36522
+ } catch {}
36523
+ }
36402
36524
  async function completeTaskGroupExecute(params, ctx) {
36403
36525
  assertOrchestrator(ctx, "opx_orch_complete_task_group");
36404
36526
  const state = await readStateByWorktree(ctx.worktree, params.change_id);
@@ -36453,17 +36575,42 @@ async function completeTaskGroupExecute(params, ctx) {
36453
36575
  `);
36454
36576
  }
36455
36577
  }
36456
- if (worktreePath && branchName) {
36457
- try {
36458
- await runGit(ctx.worktree, ["worktree", "remove", worktreePath, "--force"]);
36459
- await runGit(ctx.worktree, ["branch", "-D", branchName]);
36460
- } catch {}
36578
+ let cleanupNote = "";
36579
+ let cleanupResidualWarning = "";
36580
+ if (worktreePath) {
36581
+ const cleanup = await removeTaskGroupWorktree(ctx.worktree, worktreePath, { branchName });
36582
+ if (cleanup.dirResolved) {
36583
+ await sweepEmptyWorktreeParents(ctx.worktree, worktreePath, params.change_id);
36584
+ const successNotes = [];
36585
+ if (cleanup.dirResolvedByFallback) {
36586
+ successNotes.push("- **cleanup**: git worktree remove 未直接移除目录,已按文件系统兜底删除并 prune 管理记录。");
36587
+ }
36588
+ if (cleanup.errors.length > 0) {
36589
+ successNotes.push(`- **cleanup 部分告警**: ${cleanup.errors.join(";")}`);
36590
+ }
36591
+ cleanupNote = successNotes.join(`
36592
+ `);
36593
+ } else {
36594
+ item.metadata["cleanup_residual"] = { worktree_path: worktreePath, errors: cleanup.errors };
36595
+ cleanupResidualWarning = [
36596
+ "",
36597
+ "## ⚠️ worktree 清理残留(不影响收尾)",
36598
+ `- **残留路径**: \`${worktreePath}\``,
36599
+ "- **原因**:",
36600
+ ...cleanup.errors.map((e) => ` - ${e}`),
36601
+ `- **处理**: 请人工执行 \`rm -rf '${worktreePath}' && git worktree prune\` 清理残留目录与 git 管理记录。`,
36602
+ branchName ? `- **分支**: 分支 "${branchName}" 未删除。` : ""
36603
+ ].filter(Boolean).join(`
36604
+ `);
36605
+ }
36461
36606
  }
36462
36607
  item.metadata["completed_at"] = new Date().toISOString();
36463
36608
  await writeState(ctx.worktree, state);
36464
36609
  const doneMessage = `任务组已完成并合并到 "${mergeTarget}"。`;
36465
- return checkboxWarning ? `${doneMessage}
36466
- ${checkboxWarning}` : doneMessage;
36610
+ const notes = [cleanupNote, checkboxWarning, cleanupResidualWarning].filter(Boolean);
36611
+ return notes.length > 0 ? `${doneMessage}
36612
+ ${notes.join(`
36613
+ `)}` : doneMessage;
36467
36614
  }
36468
36615
  async function setUnattendedExecute(params, ctx) {
36469
36616
  assertOrchestrator(ctx, "opx_orch_set_unattended");
@@ -36487,6 +36634,14 @@ function isSupplementOnly(params) {
36487
36634
  return false;
36488
36635
  return (params.fixed_issue_ids?.length ?? 0) === 0 && (params.exempt_issue_ids?.length ?? 0) === 0 && (params.new_children?.length ?? 0) === 0 && (params.verified_tasks?.length ?? 0) === 0 && (params.failed_tasks?.length ?? 0) === 0 && !params.boundary_expansion && (params.validation_steps?.length ?? 0) === 0 && params.test_results === undefined;
36489
36636
  }
36637
+ function isTaskVerificationSupplement(params) {
36638
+ if ((params.verified_tasks?.length ?? 0) === 0)
36639
+ return false;
36640
+ return (params.failed_tasks?.length ?? 0) === 0 && (params.new_children?.length ?? 0) === 0 && (params.fixed_issue_ids?.length ?? 0) === 0 && (params.exempt_issue_ids?.length ?? 0) === 0 && (params.exempt_adjudications?.length ?? 0) === 0 && (params.recheck_adjudications?.length ?? 0) === 0 && !params.boundary_expansion && (params.validation_steps?.length ?? 0) === 0 && params.test_results === undefined;
36641
+ }
36642
+ function isTaskVerifySupplementAllowed(item, params, stepId, agent) {
36643
+ return isTaskVerificationSupplement(params) && getStepVerdict(item, stepId, agent) === "passed" && taskChildrenOf(item).some((c) => c.phase === "review");
36644
+ }
36490
36645
  function assertRejectedAdjudicationNotPassed(params) {
36491
36646
  if (params.verdict !== "passed")
36492
36647
  return;
@@ -36602,6 +36757,8 @@ function clearReviewVerificationTags(item) {
36602
36757
  }
36603
36758
  function resetTasksForBlocker(item) {
36604
36759
  for (const child of taskChildrenOf(item)) {
36760
+ if (child.phase === "done")
36761
+ continue;
36605
36762
  child.phase = "todo";
36606
36763
  delete child.metadata["reject_reason"];
36607
36764
  }
@@ -36633,6 +36790,10 @@ function handleAnalyzeParams(item, params) {
36633
36790
  }
36634
36791
  function handleImplementParams(item, params) {
36635
36792
  if (params.blocker) {
36793
+ if (params.blocker_updates?.length) {
36794
+ throw new Error(`不可在同一轮提交中同时上报新 blocker(blocker 参数)与更新旧 blocker(blocker_updates)。
36795
+ ` + "请分两轮处理:先以 verdict=failed + blocker 上报新阻塞;用户确认后,下一轮再携带 blocker_updates 记录答复并重新提交。");
36796
+ }
36636
36797
  if (params.verdict !== "failed") {
36637
36798
  throw new Error("blocker 参数仅支持 verdict=failed 提交(on_fail 回退 analyze)。");
36638
36799
  }
@@ -36640,6 +36801,20 @@ function handleImplementParams(item, params) {
36640
36801
  resetTasksForBlocker(item);
36641
36802
  return;
36642
36803
  }
36804
+ if (params.blocker_updates?.length) {
36805
+ for (const u of params.blocker_updates) {
36806
+ const blocker = itemBlockers(item).find((b) => b.id === u.blocker_id);
36807
+ if (!blocker)
36808
+ throw new Error(`blocker "${u.blocker_id}" 不存在于 metadata.blockers 中。`);
36809
+ if (blocker.status !== "awaiting_user")
36810
+ throw new Error(`blocker "${u.blocker_id}" 状态不是 awaiting_user,无法更新。`);
36811
+ blocker.userResponse = u.user_response;
36812
+ blocker.status = "resolved";
36813
+ }
36814
+ }
36815
+ if (params.verdict === "passed" && itemBlockers(item).some((b) => b.status !== "resolved")) {
36816
+ throw new Error("存在未解决的 blocker,无法以 passed 提交 implement step。请先通过 blocker_updates 处理全部 blocker。");
36817
+ }
36643
36818
  const tasks = taskListOf(item);
36644
36819
  const completed = normalizeTaskChildIds(params.completed_task_ids ?? [], item);
36645
36820
  const validIds = new Set(tasks.map((t) => t.id));
@@ -36661,7 +36836,7 @@ function handleImplementParams(item, params) {
36661
36836
  throw new Error(`以下 task 处于 open/rejected 状态且未在 completed_task_ids 中:
36662
36837
  ` + remaining.map((t) => `- #${t.id}(${t.status}) ${t.title}`).join(`
36663
36838
  `) + `
36664
- 请将未完成的 task 列在 completed_task_ids 中,或改用 blocker 上报阻塞。`);
36839
+ completed_task_ids 仅申报已完成可供验证的任务;无法完成或人工执行的任务不得虚报,` + `应以 verdict=failed + blocker 上报(task_id 指向该任务),经用户确认后凭 blocker 留痕重新申报。`);
36665
36840
  }
36666
36841
  if (params.self_check_results) {
36667
36842
  item.metadata["self_check_results"] = params.self_check_results;
@@ -36699,7 +36874,7 @@ function assertFailedHasReason(item, params, newChildren, stepId, agent) {
36699
36874
  hasReason = hasNewBlocking || existingBlocking.length > 0;
36700
36875
  } else if (stepId === "quality_review") {
36701
36876
  layerName = "AI 审查层";
36702
- hasReason = hasNewBlocking || existingBlocking.some((c) => resolveChildIssueFields(c).sourcePhase === "quality");
36877
+ hasReason = (params.failed_tasks?.length ?? 0) > 0 || hasNewBlocking || existingBlocking.some((c) => resolveChildIssueFields(c).sourcePhase === "quality");
36703
36878
  } else {
36704
36879
  const dimension = agentToReviewDimension(agent);
36705
36880
  layerName = dimension ? `AI 审查层(${dimension})` : "AI 审查层";
@@ -36733,7 +36908,8 @@ function handleReviewParams(item, params, newChildren, stepId, opts) {
36733
36908
  ` + `请按已加载质量门类 skill 的必做清单逐项执行并逐项申报结果(completed=true);` + `确实无法执行的必做项须以结构化 skip_reason 申报降级理由` + `(格式:${SKIP_REASON_FORMAT})。` + `低成本必做项必须实跑后申报;核验申报仅限 workflow 指令白名单限定的高成本必做项。`);
36734
36909
  }
36735
36910
  }
36736
- if (params.verified_tasks?.length || params.failed_tasks?.length) {
36911
+ const isTaskVerifyStep = stepId === "verify_task" || stepId === "quality_review";
36912
+ if (params.verified_tasks?.length || params.failed_tasks?.length || params.verdict === "passed" && isTaskVerifyStep) {
36737
36913
  const tasks = taskListOf(item);
36738
36914
  const validIds = new Set(tasks.map((t) => t.id));
36739
36915
  const verified = normalizeTaskChildIds(params.verified_tasks ?? [], item);
@@ -36756,7 +36932,8 @@ function handleReviewParams(item, params, newChildren, stepId, opts) {
36756
36932
  if (uncovered.length > 0) {
36757
36933
  throw new Error(`以下 submitted task 未被 verified_tasks 或 failed_tasks 覆盖:
36758
36934
  ` + uncovered.map((t) => `- #${t.id} ${t.title}`).join(`
36759
- `));
36935
+ `) + `
36936
+ verified_tasks 用于逐项确认已完成的任务;验证未通过的任务须以 failed_tasks 逐项驳回并设 verdict=failed 回退;` + `人工执行或环境不可验证的任务不得虚报确认,应以 verdict=failed + failed_tasks 驳回,` + `由开发者经 verdict=failed + blocker(task_id 指向该任务)上报、用户确认留痕后凭记录重新申报完成。`);
36760
36937
  }
36761
36938
  for (const id of verified) {
36762
36939
  const child = taskChildById(item, id);
@@ -36833,7 +37010,7 @@ async function agentSubmitExecute(params, ctx) {
36833
37010
  }
36834
37011
  }
36835
37012
  const guardStepPhase = workflow.stepMap.get(params.step_id)?.phase.name;
36836
- if (guardStepPhase === "review" && getStepVerdict(item, params.step_id, ctx.agent) === "passed" && !isSupplementOnly(params)) {
37013
+ if (guardStepPhase === "review" && getStepVerdict(item, params.step_id, ctx.agent) === "passed" && !isSupplementOnly(params) && !isTaskVerifySupplementAllowed(item, params, params.step_id, ctx.agent)) {
36837
37014
  throw new Error(`重复提交守卫:agent "${ctx.agent}" 已在 step "${params.step_id}" 以 passed 通过,不允许重复提交。`);
36838
37015
  }
36839
37016
  assertRejectedAdjudicationNotPassed(params);
@@ -36903,7 +37080,7 @@ async function agentSubmitExecute(params, ctx) {
36903
37080
  const agentCfg = stepCfg.agents.find((a) => a.id === ctx.agent);
36904
37081
  handleReviewParams(item, params, newChildren, params.step_id, {
36905
37082
  capabilityTags: agentCfg?.capability_tags ?? [],
36906
- skipMustDoGate: isSupplementOnly(params)
37083
+ skipMustDoGate: isSupplementOnly(params) || isTaskVerifySupplementAllowed(item, params, params.step_id, ctx.agent)
36907
37084
  });
36908
37085
  } else if (stepPhase === "todo") {
36909
37086
  handleAnalyzeParams(item, params);
@@ -37228,8 +37405,8 @@ var recoverySchema = {
37228
37405
  },
37229
37406
  reset_steps: {
37230
37407
  type: "array",
37231
- description: "重置指定 verify step 的通过标记为 pending(仅 phase=review 时有效,与 review_layer 互斥)。用于已 passed 但被本层遗漏复核/裁定阻塞的 review step 强制重新审查;恢复后 currentStep 落在第一个未全部通过的 verify step,可能早于被重置的 step。",
37232
- items: { type: "string", enum: ["verify_tool", "verify_task", "verify_quality"] }
37408
+ description: "重置指定审查 step 的通过标记为 pending(仅 phase=review 时有效,与 review_layer 互斥)。用于已 passed 但被本层遗漏复核/裁定/任务验证阻塞的 review step 强制重新审查;恢复后 currentStep 落在第一个未全部通过的审查 step,可能早于被重置的 step。值按模式生效:full 模式合法值为 verify_tool/verify_task/verify_quality,simple 模式合法值为 quality_review;传入不属于当前模式的值会在运行时报错。",
37409
+ items: { type: "string", enum: ["verify_tool", "verify_task", "verify_quality", "quality_review"] }
37233
37410
  }
37234
37411
  },
37235
37412
  required: ["phase"],
@@ -37341,7 +37518,7 @@ var agentSubmitSchema = {
37341
37518
  blockers: { type: "array", description: "analyze step:新增 blocker 列表", items: blockerItem },
37342
37519
  blocker_updates: {
37343
37520
  type: "array",
37344
- description: "analyze step:按 blocker_id 置 resolved 并记录用户答复",
37521
+ description: "analyze / implement step:按 blocker_id 置 resolved 并记录用户答复(implement 下人工执行任务凭留痕重新申报的依据)",
37345
37522
  items: {
37346
37523
  type: "object",
37347
37524
  properties: {
@@ -37377,12 +37554,12 @@ var agentSubmitSchema = {
37377
37554
  },
37378
37555
  verified_tasks: {
37379
37556
  type: "array",
37380
- description: "verify_task step:验证通过的 task id",
37557
+ description: "verify_task / quality_review step(任务验证归属层):逐项确认验证通过的 task id;passed 提交时 submitted task 必须被 verified_tasks/failed_tasks 全覆盖(工具强制门禁),漏带会被拒绝",
37381
37558
  items: { type: "string" }
37382
37559
  },
37383
37560
  failed_tasks: {
37384
37561
  type: "array",
37385
- description: "verify_task step:验证失败的 task 列表(含原因)",
37562
+ description: "verify_task / quality_review step:验证失败的 task 列表(含原因)",
37386
37563
  items: taskVerifyItem
37387
37564
  }
37388
37565
  },
@@ -37507,7 +37684,7 @@ async function ensureDefaultUnattended(args, ctx) {
37507
37684
  }
37508
37685
  } catch {}
37509
37686
  }
37510
- var PKG_VERSION = "0.130.0";
37687
+ var PKG_VERSION = "0.132.0";
37511
37688
  function buildMcpServer(worktree, opts = {}) {
37512
37689
  const mcp = new McpServer({ name: "openspec-agents", version: PKG_VERSION });
37513
37690
  for (const [name, spec] of Object.entries(TOOL_SPECS)) {
package/README.md CHANGED
@@ -12,9 +12,9 @@
12
12
  - **多工具支持**:Claude Code、Codex、ZCode、DeepSeek Harness(DSH)、OpenCode 均可原生接入。
13
13
  - **规范驱动**:直接消费 OpenSpec 变更规范(proposal / design / tasks / specs),流程与规范一一对应。
14
14
  - **智能体团队**:不再是单个 AI 从头写到尾,而是「实现者 + 审查者 + 质量把关者」分工协作,结果更稳。
15
- - **质量门禁**:实现之后经过工具检查、任务验证与多维度质量审查;发现的问题自动回退修复,也可按规则豁免并留痕。simple 模式下验证按性质分流——开发者执行确定性检查并申报,审查者分级复验(低成本项实跑、高成本项核验申报加抽样重放)并专注对抗性判断。
15
+ - **质量门禁**:实现之后经过工具检查、任务验证与多维度质量审查;发现的问题自动回退修复,也可按规则豁免并留痕。任务验证层(任务审查 / 合并审查)以通过(passed)提交时必须逐项确认全部已申报任务(verified_tasks 全覆盖为工具强制门禁);已通过但漏带确认导致阻塞时,审查者可补交确认结果解除阻塞。simple 模式下验证按性质分流——开发者执行确定性检查并申报,审查者分级复验(低成本项实跑、高成本项核验申报加抽样重放)并专注对抗性判断。
16
16
  - **隔离执行**:每个变更在独立的环境中实施,互不干扰;变更完成后合并并清理。
17
- - **过程可审计**:进度、结论、豁免与恢复点全程记录,随时可以接着上次的进度继续。
17
+ - **过程可审计**:进度、结论、豁免与恢复点全程记录,随时可以接着上次的进度继续。人工执行或环境不可验证的任务经 blocker 上报、用户确认留痕(blocker_updates)后凭记录申报完成;恢复参数 reset_steps 按流程模式(full / simple)校验各自的审查步骤值域后生效。
18
18
 
19
19
  ## 快速开始
20
20
 
@@ -46,7 +46,8 @@ phases:
46
46
  - "修复完成的 issue 经 fixed_issue_ids 上报后进入待复核(review)状态,终态由报源 reviewer 复核裁定;不可修 issue 经 exempt_issue_ids 申请豁免(申请后进入待裁定(review)状态,由报源 reviewer 在后续审查轮次裁定,被驳回的 issue 回待修复);Info 级 issue 不阻塞提交,无需申请豁免;全部 blocking issue 均已上报修复或申请豁免后才可 passed 提交推进"
47
47
  - "确定性质量工具检查出的问题不得以压制手段(压制注解/注释抑制/规则排除配置/平台状态标注/构建跳过开关等)静默规避:确需压制时,必须在压制处代码注释说明清楚理由(commit message 可补充),并经 exempt_issue_ids 申请豁免由报源 reviewer 裁定;不得以 fixed_issue_ids 冒充修复;被驳回后须移除压制并真实修复"
48
48
  - "遇外部依赖、凭证、真实输入或必须 stub/降级/跳过验收才能继续时,以 verdict=failed 提交 blocker(含 source_role、task_id、category、description、evidence、attempted_actions、options)"
49
- - "提交 opx_agent_submit:passed 时存在未完成或被驳回(open/rejected)的子任务必须全部列入 completed_task_ids(数字 id 如 1、2,或任务编号如 1.1),全部子任务已验证时可省略;不可修 issue 申请豁免(exempt_issue_ids)"
49
+ - "blocker 上报后经 question 向用户确认处理方式,确认结果经 blocker_updates(blocker_id + user_response)上报置 resolved;人工执行任务凭用户确认留痕列入 completed_task_ids 重新提交(申报依据为 blocker 记录,不得凭空申报)"
50
+ - "提交 opx_agent_submit:passed 提交前须将全部已完成(含被驳回后修复的)子任务列入 completed_task_ids(数字 id 如 1、2,或任务编号如 1.1)申报,全部子任务已验证时可省略;无法完成或人工执行的任务不得虚报,应经 verdict=failed + blocker 上报后凭留痕申报;不可修 issue 申请豁免(exempt_issue_ids)"
50
51
  constraints:
51
52
  - "禁用 edit/write 修改 `{{worktree_path}}/openspec/changes/` 下任何文档(spec/design/tasks/clarify);唯一例外:所提交 fixed_issue_ids 中某 issue 的 file 字段指向该目录下文档时,仅允许按该 issue 的要求修改对应文档,不得扩大范围;本约束为提示词级约束,工具层不校验实际改动的文件"
52
53
  - "执行边界默认整个 worktree(无 analyze 环节,不生成边界白名单)"
@@ -83,6 +84,7 @@ phases:
83
84
  - "对视图「待裁定是否可豁免」区块中的豁免申请经 exempt_adjudications 裁定(dismissed/rejected)"
84
85
  - "审查变更 diff 与 git log,识别变更中新增的压制手段(压制注解/注释抑制/规则排除配置/平台状态标注/构建跳过开关等):视为豁免申请,核对 dev 是否已在压制处代码注释或 commit message 说明理由并申报豁免;未申报或理由不成立 → 报 issue(file=压制位置、line=压制行、rule=被压制规则名,severity 至少 Low,description/suggestion 写明判定门槛与理由),要求移除压制真实修复或补申报;理由成立 → 经 exempt_adjudications 裁定 dismissed"
85
86
  - "有 task 未通过时必须 verdict=failed 并经 failed_tasks 上报(task_id + reason);passed 时不允许提供 failed_tasks"
87
+ - "对标注人工执行/环境不可验证的任务,核验视图 Blocker 留痕区块(resolved 且 user_response 记录用户确认)后经 verified_tasks 确认并在 validation_steps 留痕;无留痕且无法自行验证 → verdict=failed 经 failed_tasks 驳回并说明理由"
86
88
  - "报静态分析类 issue 时,rule 字段必填官方规则全名(命名格式遵循已加载的质量门类 skill 的规则导航索引),漏填将导致项目级豁免清单无法生效"
87
89
  - "报静态分析类 issue 时,description 必须含:①复现命令(非静默形态,含必要开关);②实测数据(命中类清单/计数);③规则默认阈值与当前项目配置对比(引用可改的配置文件路径)"
88
90
  - "按 opx_status 操作指引完成全部验证项,提交前将每项验证结果与证据填入 validation_steps 参数(未执行的项须填 skip_reason 注明原因),不得省略任何一项"
@@ -77,7 +77,8 @@ phases:
77
77
  - "修复完成的 issue 经 fixed_issue_ids 上报后进入待复核(review)状态,终态由报源 reviewer 复核裁定;不可修 issue 经 exempt_issue_ids 申请豁免(申请后进入待裁定(review)状态,由报源 reviewer 在后续审查轮次裁定,被驳回的 issue 回待修复);Info 级 issue 不阻塞提交,无需申请豁免;全部 blocking issue 均已上报修复或申请豁免后才可 passed 提交推进"
78
78
  - "确定性质量工具检查出的问题不得以压制手段(压制注解/注释抑制/规则排除配置/平台状态标注/构建跳过开关等)静默规避:确需压制时,必须在压制处代码注释说明清楚理由(commit message 可补充),并经 exempt_issue_ids 申请豁免由报源 reviewer 裁定;不得以 fixed_issue_ids 冒充修复;被驳回后须移除压制并真实修复"
79
79
  - "遇外部依赖、凭证、真实输入或必须 stub/降级/跳过验收才能继续时,以 verdict=failed 提交 blocker(含 source_role、task_id、category、description、evidence、attempted_actions、options)"
80
- - "提交 opx_agent_submit:passed 时存在未完成或被驳回(open/rejected)的子任务必须全部列入 completed_task_ids(数字 id 如 1、2,或任务编号如 1.1),全部子任务已验证时可省略;不可修 issue 申请豁免(exempt_issue_ids)"
80
+ - "blocker 上报后经 question 向用户确认处理方式,确认结果经 blocker_updates(blocker_id + user_response)上报置 resolved;人工执行任务凭用户确认留痕列入 completed_task_ids 重新提交(申报依据为 blocker 记录,不得凭空申报)"
81
+ - "提交 opx_agent_submit:passed 提交前须将全部已完成(含被驳回后修复的)子任务列入 completed_task_ids(数字 id 如 1、2,或任务编号如 1.1)申报,全部子任务已验证时可省略;无法完成或人工执行的任务不得虚报,应经 verdict=failed + blocker 上报后凭留痕申报;不可修 issue 申请豁免(exempt_issue_ids)"
81
82
  constraints:
82
83
  - "禁用 edit/write 修改 `{{worktree_path}}/openspec/changes/` 下任何文档(spec/design/tasks/clarify);唯一例外:所提交 fixed_issue_ids 中某 issue 的 file 字段指向该目录下文档时,仅允许按该 issue 的要求修改对应文档,不得扩大范围;本约束为提示词级约束,工具层不校验实际改动的文件"
83
84
  - "允许变更目录范围:{{allowed_directories}}"
@@ -130,6 +131,7 @@ phases:
130
131
  - "本次变更仅含测试代码(判定:视图「变更范围」文件清单全部命中已加载技术栈 skill 定义的测试代码路径,且无生产代码、接口契约、构建与测试配置等非测试文件)时,豁免②启动服务并检查健康与③独立执行全量 API 测试,仅验证①④;豁免时 validation_steps 中②③置 completed=false,并以结构化 skip_reason 申报(格式:{\"item\":\"<对应验证项>\",\"category\":\"<降级类别>\",\"adjudication\":\"user_response|unattended_auto|env_unavailable\",\"note\":\"<说明>\"}),note 注明「本次变更仅含测试代码,豁免启动服务与 API 测试」及判定依据;⑤⑥的实现核验在纯测试代码变更场景无对象,其断言证据核验并入④测试代码质量"
131
132
  - "读取 design.md 的 API 定义(请求/响应结构、数据模型)与 specs/ 下相关 spec.md(需求细节、验收标准与数据产出要求),用于准备测试数据与对照 API 合约"
132
133
  - "有 task 未通过时必须 verdict=failed 并经 failed_tasks 上报(task_id + reason);passed 时不允许提供 failed_tasks"
134
+ - "对标注人工执行/环境不可验证的任务,核验视图 Blocker 留痕区块(resolved 且 user_response 记录用户确认)后经 verified_tasks 确认并在 validation_steps 留痕;无留痕且无法自行验证 → verdict=failed 经 failed_tasks 驳回并说明理由"
133
135
  - "当发现修复需要调整 openspec/changes/ 下文档时,所报 issue 的 file 字段必须指向目标文档"
134
136
  - "报 issue 时必须显式声明归因维度 dimension(style/architecture/performance/security/maintainability):本 reviewer 不归属任一质量维度,dimension 即该 issue 归属的目标质量维度"
135
137
  - "复核本层已修复待复核(review 态)issue:通过置 done,不通过驳回(附驳回原因,issue 回待修复并累计修复未过次数)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ifrankwang/openspec-agents",
3
- "version": "0.130.0",
3
+ "version": "0.132.0",
4
4
  "description": "OpenSpec change orchestration plugin for AI coding agents (Claude Code, Codex, ZCode, DeepSeek Harness, OpenCode)",
5
5
  "author": "ifrankwang",
6
6
  "license": "MIT",