lee-spec-kit 0.9.4 → 0.9.5

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 CHANGED
@@ -213,6 +213,7 @@ var koCli = {
213
213
  "init.choice.taskAgent.on": "\uC0AC\uC6A9 - \uAC01 Task \uAD6C\uD604\uC744 \uC704\uC784",
214
214
  "init.choice.taskAgent.off": "\uC0AC\uC6A9 \uC548 \uD568 - \uBA54\uC778 \uC5D0\uC774\uC804\uD2B8\uAC00 \uC9C1\uC811 \uAD6C\uD604",
215
215
  "init.prompt.reviews": "\uB3C5\uB9BD \uAC80\uC218 \uB2E8\uACC4\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
216
+ "init.prompt.maxReviewRounds": "\uC0AC\uC6A9\uC790\uC5D0\uAC8C \uD310\uB2E8\uC744 \uC694\uCCAD\uD558\uAE30 \uC804 \uB9AC\uBDF0 \uC9C0\uC801 \uC790\uB3D9 \uBC18\uC601 \uCD5C\uB300 \uD69F\uC218:",
216
217
  "init.choice.review.plan": "Plan",
217
218
  "init.choice.review.task": "Task",
218
219
  "init.choice.review.feature": "Feature",
@@ -238,6 +239,7 @@ var koCli = {
238
239
  "init.log.workflowLabel": "\uC6CC\uD06C\uD50C\uB85C\uC6B0",
239
240
  "init.log.taskAgentLabel": "Task \uAD6C\uD604",
240
241
  "init.log.reviewsLabel": "\uAC80\uC218",
242
+ "init.log.maxReviewRoundsLabel": "\uB9AC\uBDF0 \uC9C0\uC801 \uC790\uB3D9 \uBC18\uC601 \uD69F\uC218",
241
243
  "init.log.completionStrategyLabel": "\uB85C\uCEEC \uD1B5\uD569",
242
244
  "init.summary.taskAgent.on": "\uC11C\uBE0C\uC5D0\uC774\uC804\uD2B8",
243
245
  "init.summary.taskAgent.off": "\uBA54\uC778 \uC5D0\uC774\uC804\uD2B8",
@@ -525,6 +527,7 @@ var enCli = {
525
527
  "init.choice.taskAgent.on": "On - delegate each task implementation",
526
528
  "init.choice.taskAgent.off": "Off - implement tasks in the main agent",
527
529
  "init.prompt.reviews": "Select independent review gates:",
530
+ "init.prompt.maxReviewRounds": "Maximum automatic review finding remediation rounds before asking the user:",
528
531
  "init.choice.review.plan": "Plan",
529
532
  "init.choice.review.task": "Task",
530
533
  "init.choice.review.feature": "Feature",
@@ -550,6 +553,7 @@ var enCli = {
550
553
  "init.log.workflowLabel": "Workflow",
551
554
  "init.log.taskAgentLabel": "Task implementation",
552
555
  "init.log.reviewsLabel": "Reviews",
556
+ "init.log.maxReviewRoundsLabel": "Review remediation rounds",
553
557
  "init.log.completionStrategyLabel": "Local integration",
554
558
  "init.summary.taskAgent.on": "subagent",
555
559
  "init.summary.taskAgent.off": "main agent",
@@ -1364,14 +1368,15 @@ Before taking the next workflow step:
1364
1368
  10. In standalone mode, do not hand-write \`git worktree add\`; run the exact \`nextAction.command\` from \`workflow-stage\` so the managed workspace path, stale directory cleanup, and \`.env\` / \`.env.*\` copy step stay consistent
1365
1369
  11. Keep docs and code synchronized; if code changes materially, update the active feature docs in the same turn before stopping
1366
1370
  12. When docs are synced to code, keep exactly one explicit marker like \`<!-- lee-spec-kit:workflow-sync 2026-04-16T12:34:56.789Z -->\` in a single active feature doc (prefer \`tasks.md\` or \`decisions.md\`): replace an existing marker timestamp or remove duplicates instead of appending another marker, so \`workflow-audit\` can prove the sync happened after the latest code change
1367
- 13. When \`workflow-stage --json\` returns \`nextAction.category === "plan_review"\` with \`executor === "subagent"\`, delegate a fresh read-only review of \`spec.md\` and \`plan.md\` using the returned model settings and exact \`specHash\` / \`planHash\`; the main agent records evidence, decision, reviewer metadata, and both hashes, and any later spec/plan content change requires a fresh review
1371
+ 13. When \`workflow-stage --json\` returns \`nextAction.category === "plan_review"\` with \`executor === "subagent"\`, delegate a fresh read-only review of \`spec.md\` and \`plan.md\` using the returned model settings and exact \`specHash\` / \`planHash\`; the main agent records the returned reviewRound, evidence, decision, reviewer metadata, and both hashes, and any later spec/plan content change requires a fresh review
1368
1372
  14. When \`workflow-stage --json\` returns \`nextAction.category === "task_execute"\` with \`executor === "subagent"\`, mark exactly the returned \`taskId\` as active and delegate its implementation plus task-scoped verification to a fresh subagent in the returned \`workingDirectory\`, using the returned \`model\`, \`reasoningEffort\`, \`onUnavailable\`, and exact \`workerContract\`; no named execution skill is required
1369
1373
  15. The task implementation worker executes directly: it must follow the approved Verification Contract, must not add unplanned durable tests, and must not run \`workflow-stage\`, spawn another subagent, edit lee-spec-kit docs, change task state, commit, request approvals, or perform remote/destructive actions. It may modify project code and run task-scoped checks only. The main agent inspects the result, synchronizes docs and task state, and owns every commit and workflow transition; official hooks block commits while \`task_execute\` is still active
1370
- 16. When \`workflow-stage --json\` returns \`nextAction.category === "task_review"\` with \`executor === "subagent"\`, delegate a fresh read-only review for the returned \`taskId\`, \`baseSha\`, \`targetSha\`, and \`targetTree\`; the main agent records the evidence and moves the task from \`REVIEW\` to \`DONE\` only after an approve decision
1371
- 17. When \`workflow-stage --json\` returns \`nextAction.category === "pre_pr_review"\` and \`nextAction.executor === "subagent"\`, delegate a fresh read-only Feature review using the returned \`model\`, \`reasoningEffort\`, \`onUnavailable\`, and review target metadata; do not select or require a named review skill
1374
+ 16. When \`workflow-stage --json\` returns \`nextAction.category === "task_review"\` with \`executor === "subagent"\`, delegate a fresh read-only review for the returned \`taskId\`, \`baseSha\`, \`targetSha\`, and \`targetTree\`; the main agent records the returned reviewRound and evidence and moves the task from \`REVIEW\` to \`DONE\` only after an approve decision
1375
+ 17. When \`workflow-stage --json\` returns \`nextAction.category === "pre_pr_review"\` and \`nextAction.executor === "subagent"\`, delegate a fresh read-only Feature review using the returned \`model\`, \`reasoningEffort\`, \`onUnavailable\`, reviewRound, and review target metadata; do not select or require a named review skill
1372
1376
  18. Review subagents return findings without modifying code; the main agent remediates findings and records the actual reviewer metadata, reviewed scope, evidence, decision, and exact hash/SHA/tree target metadata
1373
- 19. For a local workflow, do not report completion directly after implementation approval; follow the exact returned \`local verify\` / \`local merge\` / \`local cleanup\` commands until \`workflow-stage\` proves verification, integration, and cleanup and returns \`done\`; review-fix and \`feature_remediation\` stages explicitly permit scoped fixes
1374
- 20. In a \`local-ff\` or \`local-squash\` workflow, keep implementation approval and local merge approval distinct when \`local_merge\` is required: the first accepts the implementation, and the second authorizes the configured integration strategy, post-merge checks, and local cleanup
1377
+ 19. workflow.agentReview.maxRounds counts automatic finding-remediation passes, not the initial review. When workflow-stage returns nextAction.category review_escalation, those passes have been exhausted; do not remediate or delegate another review, keep implementation blocked, and present the exact returned action options to the user. Another remediation pass requires increasing maxRounds first
1378
+ 20. For a local workflow, do not report completion directly after implementation approval; follow the exact returned \`local verify\` / \`local merge\` / \`local cleanup\` commands until \`workflow-stage\` proves verification, integration, and cleanup and returns \`done\`; review-fix and \`feature_remediation\` stages explicitly permit scoped fixes
1379
+ 21. In a \`local-ff\` or \`local-squash\` workflow, keep implementation approval and local merge approval distinct when \`local_merge\` is required: the first accepts the implementation, and the second authorizes the configured integration strategy, post-merge checks, and local cleanup
1375
1380
 
1376
1381
  Approval and remote actions:
1377
1382
 
@@ -2104,6 +2109,15 @@ function assertValidInitWorkflowOptions(options) {
2104
2109
  "`--completion-strategy` must be `local-ff`, `local-squash`, or `none`."
2105
2110
  );
2106
2111
  }
2112
+ if (options.maxReviewRounds !== void 0) {
2113
+ const parsed = Number(options.maxReviewRounds);
2114
+ if (!Number.isInteger(parsed) || parsed < 1) {
2115
+ throw createCliError(
2116
+ "INVALID_ARGUMENT",
2117
+ "`--max-review-rounds` must be a positive integer."
2118
+ );
2119
+ }
2120
+ }
2107
2121
  }
2108
2122
  function initCommand(program2) {
2109
2123
  program2.command("init").description("Initialize project documentation structure").option("-n, --name <name>", "Project name (default: current folder name)").option(
@@ -2115,6 +2129,9 @@ function initCommand(program2) {
2115
2129
  ).option("-l, --lang <lang>", "Language: ko | en (default: en)").option("--workflow <mode>", "Workflow mode: github | local").option("--task-agent <mode>", "Task implementation agent: on | off").option(
2116
2130
  "--reviews <list>",
2117
2131
  "Review gates: comma-separated plan,task,feature | none"
2132
+ ).option(
2133
+ "--max-review-rounds <count>",
2134
+ "Automatic review finding remediation rounds before user escalation (default: 1)"
2118
2135
  ).option(
2119
2136
  "--completion-strategy <strategy>",
2120
2137
  "Local completion: local-ff | local-squash | none"
@@ -2158,6 +2175,7 @@ async function runInit(options) {
2158
2175
  let workflowMode = options.workflow || "github";
2159
2176
  let taskAgentEnabled = options.taskAgent !== "off";
2160
2177
  let enabledReviews = parseInitReviews(options.reviews) ?? ["plan", "feature"];
2178
+ let maxReviewRounds = Number(options.maxReviewRounds || 1);
2161
2179
  let completionStrategy = options.completionStrategy || "local-ff";
2162
2180
  let docsRepo = options.docsRepo || "embedded";
2163
2181
  let pushDocs = typeof options.pushDocs === "boolean" ? options.pushDocs : void 0;
@@ -2419,7 +2437,7 @@ async function runInit(options) {
2419
2437
  docsRemote = remoteResponse.docsRemote || docsRemote;
2420
2438
  }
2421
2439
  }
2422
- const hasExplicitWorkflowSetup = typeof options.taskAgent !== "undefined" || typeof options.reviews !== "undefined" || typeof options.completionStrategy !== "undefined";
2440
+ const hasExplicitWorkflowSetup = typeof options.taskAgent !== "undefined" || typeof options.reviews !== "undefined" || typeof options.maxReviewRounds !== "undefined" || typeof options.completionStrategy !== "undefined";
2423
2441
  let customizeWorkflow = hasExplicitWorkflowSetup;
2424
2442
  if (!hasExplicitWorkflowSetup) {
2425
2443
  const setupResponse = await prompts(
@@ -2496,6 +2514,13 @@ async function runInit(options) {
2496
2514
  selected: phase !== "task"
2497
2515
  }))
2498
2516
  },
2517
+ {
2518
+ type: typeof options.maxReviewRounds === "undefined" ? "number" : null,
2519
+ name: "maxReviewRounds",
2520
+ message: tr(lang, "cli", "init.prompt.maxReviewRounds"),
2521
+ initial: 1,
2522
+ min: 1
2523
+ },
2499
2524
  {
2500
2525
  type: workflowMode === "local" && typeof options.completionStrategy === "undefined" ? "select" : null,
2501
2526
  name: "completionStrategy",
@@ -2539,6 +2564,9 @@ async function runInit(options) {
2539
2564
  (phase) => workflowResponse.reviews.includes(phase)
2540
2565
  );
2541
2566
  }
2567
+ if (Number.isInteger(workflowResponse.maxReviewRounds) && workflowResponse.maxReviewRounds > 0) {
2568
+ maxReviewRounds = workflowResponse.maxReviewRounds;
2569
+ }
2542
2570
  completionStrategy = workflowResponse.completionStrategy || completionStrategy;
2543
2571
  }
2544
2572
  }
@@ -2770,6 +2798,11 @@ async function runInit(options) {
2770
2798
  ` ${tr(lang, "cli", "init.log.reviewsLabel")}: ${reviewSummary}`
2771
2799
  )
2772
2800
  );
2801
+ console.log(
2802
+ chalk.gray(
2803
+ ` ${tr(lang, "cli", "init.log.maxReviewRoundsLabel")}: ${maxReviewRounds}`
2804
+ )
2805
+ );
2773
2806
  if (workflowMode === "local") {
2774
2807
  console.log(
2775
2808
  chalk.gray(
@@ -2839,6 +2872,7 @@ async function runInit(options) {
2839
2872
  }
2840
2873
  },
2841
2874
  agentReview: {
2875
+ maxRounds: maxReviewRounds,
2842
2876
  plan: {
2843
2877
  enabled: enabledReviews.includes("plan"),
2844
2878
  evidenceMode: "path_required",
@@ -3099,7 +3133,7 @@ function sanitizeTasksForLocal(content, lang) {
3099
3133
  filtered.push(line);
3100
3134
  }
3101
3135
  next = filtered.join("\n");
3102
- next = lang === "ko" ? next.replaceAll("**PR \uC804 \uB9AC\uBDF0**", "**Feature \uB9AC\uBDF0**").replaceAll("**PR \uC804 \uB9AC\uBDF0 Evidence**", "**Feature \uB9AC\uBDF0 Evidence**").replaceAll("**PR \uC804 \uB9AC\uBDF0 Decision**", "**Feature \uB9AC\uBDF0 Decision**").replaceAll("**PR \uC804 \uB9AC\uBDF0 Head**", "**Feature \uB9AC\uBDF0 Head**").replaceAll("**PR \uC804 \uB9AC\uBDF0 Tree**", "**Feature \uB9AC\uBDF0 Tree**").replaceAll("pre-PR \uB9AC\uBDF0 handoff", "Feature \uB9AC\uBDF0 handoff") : next.replaceAll("**Pre-PR Review**", "**Feature Review**").replaceAll("**Pre-PR Evidence**", "**Feature Review Evidence**").replaceAll("**Pre-PR Decision**", "**Feature Review Decision**").replaceAll("**Pre-PR Reviewed Head**", "**Feature Reviewed Head**").replaceAll("**Pre-PR Reviewed Tree**", "**Feature Reviewed Tree**").replaceAll("pre-PR review handoff", "Feature review handoff");
3136
+ next = lang === "ko" ? next.replaceAll("**PR \uC804 \uB9AC\uBDF0**", "**Feature \uB9AC\uBDF0**").replaceAll("**PR \uC804 \uB9AC\uBDF0 Evidence**", "**Feature \uB9AC\uBDF0 Evidence**").replaceAll("**PR \uC804 \uB9AC\uBDF0 Decision**", "**Feature \uB9AC\uBDF0 Decision**").replaceAll("**PR \uC804 \uB9AC\uBDF0 Round**", "**Feature \uB9AC\uBDF0 Round**").replaceAll("**PR \uC804 \uB9AC\uBDF0 Head**", "**Feature \uB9AC\uBDF0 Head**").replaceAll("**PR \uC804 \uB9AC\uBDF0 Tree**", "**Feature \uB9AC\uBDF0 Tree**").replaceAll("pre-PR \uB9AC\uBDF0 handoff", "Feature \uB9AC\uBDF0 handoff") : next.replaceAll("**Pre-PR Review**", "**Feature Review**").replaceAll("**Pre-PR Evidence**", "**Feature Review Evidence**").replaceAll("**Pre-PR Decision**", "**Feature Review Decision**").replaceAll("**Pre-PR Review Round**", "**Feature Review Round**").replaceAll("**Pre-PR Reviewed Head**", "**Feature Reviewed Head**").replaceAll("**Pre-PR Reviewed Tree**", "**Feature Reviewed Tree**").replaceAll("pre-PR review handoff", "Feature review handoff");
3103
3137
  next = next.replace(/feat\/\{issue-number\}-/g, "feat/").replace(/feat\/\{이슈번호\}-/g, "feat/").replace(/feat\/-/g, "feat/");
3104
3138
  return normalizeTrailingBlankLines(next);
3105
3139
  }
@@ -4154,6 +4188,11 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
4154
4188
  changedPaths.push("workflow.agentReview");
4155
4189
  }
4156
4190
  const agentReview = workflow.agentReview;
4191
+ setIfMissing(agentReview, "maxRounds", 1, "workflow.agentReview.maxRounds");
4192
+ if (typeof agentReview.maxRounds !== "number" || !Number.isInteger(agentReview.maxRounds) || agentReview.maxRounds < 1) {
4193
+ agentReview.maxRounds = 1;
4194
+ changedPaths.push("workflow.agentReview.maxRounds");
4195
+ }
4157
4196
  const normalizeAgentReviewPhase = (key, enabledDefault, legacySeed = null) => {
4158
4197
  const phasePath = `workflow.agentReview.${key}`;
4159
4198
  if (!isPlainObject(agentReview[key])) {
@@ -8351,6 +8390,12 @@ var PRE_PR_DECISION_LABELS = [
8351
8390
  "Feature Review Decision",
8352
8391
  "Feature \uB9AC\uBDF0 Decision"
8353
8392
  ];
8393
+ var PRE_PR_REVIEW_ROUND_LABELS = [
8394
+ "Pre-PR Review Round",
8395
+ "PR \uC804 \uB9AC\uBDF0 Round",
8396
+ "Feature Review Round",
8397
+ "Feature \uB9AC\uBDF0 Round"
8398
+ ];
8354
8399
  var PRE_PR_REVIEWED_HEAD_LABELS = [
8355
8400
  "Pre-PR Reviewed Head",
8356
8401
  "PR \uC804 \uB9AC\uBDF0 Head",
@@ -8374,6 +8419,11 @@ var PLAN_REVIEW_DECISION_LABELS = [
8374
8419
  "Plan \uAC80\uC218 Decision",
8375
8420
  "Plan \uB9AC\uBDF0 Decision"
8376
8421
  ];
8422
+ var PLAN_REVIEW_ROUND_LABELS = [
8423
+ "Plan Review Round",
8424
+ "Plan \uAC80\uC218 Round",
8425
+ "Plan \uB9AC\uBDF0 Round"
8426
+ ];
8377
8427
  var PLAN_REVIEWED_SPEC_HASH_LABELS = [
8378
8428
  "Plan Reviewed Spec Hash",
8379
8429
  "Plan \uAC80\uC218 Spec Hash",
@@ -8460,6 +8510,14 @@ function parseReviewDecisionOutcome(value) {
8460
8510
  const match = (value || "").trim().toLowerCase().match(/\b(approve|changes_requested|blocked)\b/);
8461
8511
  return match?.[1] || null;
8462
8512
  }
8513
+ function parseReviewRound(value) {
8514
+ const parsed = Number((value || "").trim());
8515
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
8516
+ }
8517
+ function resolveMaxReviewRounds(config) {
8518
+ const configured = config.workflow?.agentReview?.maxRounds;
8519
+ return typeof configured === "number" && Number.isInteger(configured) && configured > 0 ? configured : 1;
8520
+ }
8463
8521
  function parseWorkflowTaskLine(line, index = -1) {
8464
8522
  const canonical = parseTaskLine(line, index);
8465
8523
  if (canonical) {
@@ -8530,6 +8588,13 @@ function parseTasksDoc(content, feature) {
8530
8588
  index,
8531
8589
  ["Review Decision", "Task Review Decision", "\uD0DC\uC2A4\uD06C \uB9AC\uBDF0 Decision"]
8532
8590
  );
8591
+ const reviewRound = parseReviewRound(
8592
+ extractTaskReviewValue(nonCodeLines, index, [
8593
+ "Review Round",
8594
+ "Task Review Round",
8595
+ "\uD0DC\uC2A4\uD06C \uB9AC\uBDF0 Round"
8596
+ ])
8597
+ );
8533
8598
  const legacyTitleKey = normalizeCommitTopicText(parsed.title).toLowerCase();
8534
8599
  const sameTitleOccurrence = (legacyTitleOccurrences.get(legacyTitleKey) || 0) + 1;
8535
8600
  legacyTitleOccurrences.set(legacyTitleKey, sameTitleOccurrence);
@@ -8547,6 +8612,7 @@ function parseTasksDoc(content, feature) {
8547
8612
  ),
8548
8613
  reviewDecision,
8549
8614
  reviewDecisionOutcome: parseReviewDecisionOutcome(reviewDecision),
8615
+ reviewRound,
8550
8616
  reviewedHead: extractTaskReviewValue(
8551
8617
  nonCodeLines,
8552
8618
  index,
@@ -8602,6 +8668,9 @@ function parseTasksDoc(content, feature) {
8602
8668
  ),
8603
8669
  prePrDecision: sanitizeMetadataValue2(prePrDecision),
8604
8670
  prePrDecisionOutcome,
8671
+ prePrReviewRound: parseReviewRound(
8672
+ extractFieldValue2(content, PRE_PR_REVIEW_ROUND_LABELS)
8673
+ ),
8605
8674
  prePrReviewedHead: sanitizeMetadataValue2(
8606
8675
  extractFieldValue2(content, PRE_PR_REVIEWED_HEAD_LABELS)
8607
8676
  ),
@@ -8633,6 +8702,9 @@ function parsePlanReview(content) {
8633
8702
  const decision = sanitizeMetadataValue2(
8634
8703
  extractFieldValue2(content, PLAN_REVIEW_DECISION_LABELS)
8635
8704
  );
8705
+ const reviewRound = parseReviewRound(
8706
+ extractFieldValue2(content, PLAN_REVIEW_ROUND_LABELS)
8707
+ );
8636
8708
  const reviewedSpecHash = sanitizeMetadataValue2(
8637
8709
  extractFieldValue2(content, PLAN_REVIEWED_SPEC_HASH_LABELS)
8638
8710
  );
@@ -8644,6 +8716,7 @@ function parsePlanReview(content) {
8644
8716
  evidence,
8645
8717
  decision,
8646
8718
  decisionOutcome: parseReviewDecisionOutcome(decision),
8719
+ reviewRound,
8647
8720
  reviewedSpecHash,
8648
8721
  reviewedPlanHash,
8649
8722
  hasMetadata: PLAN_REVIEW_STATUS_LABELS.some((label) => {
@@ -8669,6 +8742,7 @@ function buildPlanReviewTarget(specContent, planContent) {
8669
8742
  ...PLAN_REVIEW_STATUS_LABELS,
8670
8743
  ...PLAN_REVIEW_EVIDENCE_LABELS,
8671
8744
  ...PLAN_REVIEW_DECISION_LABELS,
8745
+ ...PLAN_REVIEW_ROUND_LABELS,
8672
8746
  ...PLAN_REVIEWED_SPEC_HASH_LABELS,
8673
8747
  ...PLAN_REVIEWED_PLAN_HASH_LABELS
8674
8748
  ])
@@ -9585,31 +9659,68 @@ function resolveFeatureSelectionError(selection) {
9585
9659
  }
9586
9660
  function resolvePlanReviewPayload(config, feature, review, target) {
9587
9661
  const targetMatches = review.reviewedSpecHash === target.specHash && review.reviewedPlanHash === target.planHash;
9662
+ const maxReviewRounds = resolveMaxReviewRounds(config);
9663
+ const reviewRound = review.reviewRound || 1;
9664
+ const nextReviewRound = !targetMatches && review.decisionOutcome ? reviewRound + 1 : reviewRound;
9588
9665
  const actionContext = {
9589
9666
  reviewScope: "plan",
9667
+ reviewRound: nextReviewRound,
9668
+ maxReviewRounds,
9590
9669
  specHash: target.specHash,
9591
9670
  planHash: target.planHash,
9592
9671
  docsDirectory: config.docsDir
9593
9672
  };
9594
- if (targetMatches && review.decisionOutcome === "changes_requested") {
9595
- return {
9596
- status: "ok",
9597
- reasonCode: "WORKFLOW_STAGE_RESOLVED",
9598
- docsDir: config.docsDir,
9599
- featureRef: buildFeatureRef(feature),
9600
- stage: "plan_review_fix",
9601
- nextAction: buildAction(
9602
- "plan_review_fix",
9603
- "Address the independent Plan review findings in spec.md or plan.md, keep implementation blocked, and request a fresh review for the resulting document hashes.",
9604
- false,
9605
- null,
9606
- void 0,
9607
- actionContext
9608
- ),
9609
- approvalRequired: false,
9610
- implementationAllowed: false,
9611
- blockedReasonCode: "PLAN_REVIEW_NOT_APPROVED"
9612
- };
9673
+ if (review.decisionOutcome === "changes_requested") {
9674
+ if (reviewRound > maxReviewRounds) {
9675
+ return {
9676
+ status: "ok",
9677
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
9678
+ docsDir: config.docsDir,
9679
+ featureRef: buildFeatureRef(feature),
9680
+ stage: "review_escalation",
9681
+ nextAction: buildAction(
9682
+ "review_escalation",
9683
+ `Plan review requested more changes after ${maxReviewRounds} automatic remediation round(s) were used. Ask the user whether to revise again, request a different reviewer, accept the documented risk, change scope, or hold the Feature.`,
9684
+ true,
9685
+ null,
9686
+ void 0,
9687
+ { ...actionContext, reviewRound }
9688
+ ),
9689
+ approvalRequired: true,
9690
+ implementationAllowed: false,
9691
+ primaryActionLabel: "A",
9692
+ actionOptions: [
9693
+ buildStageOption(
9694
+ "A",
9695
+ "A",
9696
+ "review_retry",
9697
+ "Increase workflow.agentReview.maxRounds, then revise the Plan and run one more fresh review round."
9698
+ ),
9699
+ buildStageOption("B", "B", "hold", "Hold the Feature for user resolution.")
9700
+ ],
9701
+ blockedReasonCode: "PLAN_REVIEW_NOT_APPROVED"
9702
+ };
9703
+ }
9704
+ if (targetMatches) {
9705
+ return {
9706
+ status: "ok",
9707
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
9708
+ docsDir: config.docsDir,
9709
+ featureRef: buildFeatureRef(feature),
9710
+ stage: "plan_review_fix",
9711
+ nextAction: buildAction(
9712
+ "plan_review_fix",
9713
+ `Address the independent Plan review findings in spec.md or plan.md, record review round ${reviewRound + 1}, keep implementation blocked, and request a fresh review for the resulting document hashes.`,
9714
+ false,
9715
+ null,
9716
+ void 0,
9717
+ { ...actionContext, reviewRound }
9718
+ ),
9719
+ approvalRequired: false,
9720
+ implementationAllowed: false,
9721
+ blockedReasonCode: "PLAN_REVIEW_NOT_APPROVED"
9722
+ };
9723
+ }
9613
9724
  }
9614
9725
  if (targetMatches && review.decisionOutcome === "blocked") {
9615
9726
  return {
@@ -9624,7 +9735,7 @@ function resolvePlanReviewPayload(config, feature, review, target) {
9624
9735
  false,
9625
9736
  null,
9626
9737
  void 0,
9627
- actionContext
9738
+ { ...actionContext, reviewRound }
9628
9739
  ),
9629
9740
  approvalRequired: false,
9630
9741
  implementationAllowed: false,
@@ -9639,7 +9750,7 @@ function resolvePlanReviewPayload(config, feature, review, target) {
9639
9750
  stage: "plan_review",
9640
9751
  nextAction: buildAction(
9641
9752
  "plan_review",
9642
- "Delegate a fresh read-only review of spec.md and plan.md. Verify the Verification Contract, NONE/UPDATE/ADD decisions, requirement coverage, independent oracles, stable observation boundaries, realistic failure/rollback cases, exclusions, and focused/full verification scope. Record Plan Review status, evidence, decision, reviewer metadata, Reviewed Spec Hash, and Reviewed Plan Hash without modifying the documents.",
9753
+ `Delegate fresh read-only Plan review round ${nextReviewRound} of spec.md and plan.md. Verify the Verification Contract, NONE/UPDATE/ADD decisions, requirement coverage, independent oracles, stable observation boundaries, realistic failure/rollback cases, exclusions, and focused/full verification scope. Record Plan Review Round, status, evidence, decision, reviewer metadata, Reviewed Spec Hash, and Reviewed Plan Hash without modifying the documents.`,
9643
9754
  false,
9644
9755
  null,
9645
9756
  resolveAgentReviewer(config, "plan"),
@@ -9927,29 +10038,58 @@ async function collectWorkflowStage(cwd, selector, component) {
9927
10038
  }
9928
10039
  const reviewContext = {
9929
10040
  reviewScope: "task",
10041
+ reviewRound: reviewTask.reviewRound || 1,
10042
+ maxReviewRounds: resolveMaxReviewRounds(config),
9930
10043
  ...reviewTask.taskId ? { taskId: reviewTask.taskId } : {},
9931
10044
  ...reviewTarget
9932
10045
  };
9933
10046
  const evidenceMatchesTarget = reviewTask.reviewedHead === reviewTarget.targetSha && reviewTask.reviewedTree === reviewTarget.targetTree;
9934
- if (evidenceMatchesTarget && reviewTask.reviewDecisionOutcome === "changes_requested") {
9935
- return {
9936
- status: "ok",
9937
- reasonCode: "WORKFLOW_STAGE_RESOLVED",
9938
- docsDir: config.docsDir,
9939
- featureRef: buildFeatureRef(feature),
9940
- stage: "task_review_fix",
9941
- nextAction: buildAction(
9942
- "task_review_fix",
9943
- `Address the independent review findings for ${reviewTask.taskId || reviewTask.title}, return the task to REVIEW, commit the new code tip, and request a fresh review.`,
9944
- false,
9945
- null,
9946
- void 0,
9947
- reviewContext
9948
- ),
9949
- approvalRequired: false,
9950
- implementationAllowed: true,
9951
- blockedReasonCode: "TASK_REVIEW_NOT_APPROVED"
9952
- };
10047
+ if (reviewTask.reviewDecisionOutcome === "changes_requested") {
10048
+ if (reviewContext.reviewRound > reviewContext.maxReviewRounds) {
10049
+ return {
10050
+ status: "ok",
10051
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
10052
+ docsDir: config.docsDir,
10053
+ featureRef: buildFeatureRef(feature),
10054
+ stage: "review_escalation",
10055
+ nextAction: buildAction(
10056
+ "review_escalation",
10057
+ `Task review for ${reviewTask.taskId || reviewTask.title} requested more changes after ${reviewContext.maxReviewRounds} automatic remediation round(s) were used. Ask the user whether to authorize another remediation round or hold the Feature.`,
10058
+ true,
10059
+ null,
10060
+ void 0,
10061
+ reviewContext
10062
+ ),
10063
+ approvalRequired: true,
10064
+ implementationAllowed: false,
10065
+ primaryActionLabel: "A",
10066
+ actionOptions: [
10067
+ buildStageOption("A", "A", "review_retry", "Increase workflow.agentReview.maxRounds, then run one more task remediation and fresh review round."),
10068
+ buildStageOption("B", "B", "hold", "Hold the Feature for user resolution.")
10069
+ ],
10070
+ blockedReasonCode: "TASK_REVIEW_NOT_APPROVED"
10071
+ };
10072
+ }
10073
+ if (evidenceMatchesTarget) {
10074
+ return {
10075
+ status: "ok",
10076
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
10077
+ docsDir: config.docsDir,
10078
+ featureRef: buildFeatureRef(feature),
10079
+ stage: "task_review_fix",
10080
+ nextAction: buildAction(
10081
+ "task_review_fix",
10082
+ `Address the independent review findings for ${reviewTask.taskId || reviewTask.title}, return the task to REVIEW, commit the new code tip, and request fresh review round ${reviewContext.reviewRound + 1}.`,
10083
+ false,
10084
+ null,
10085
+ void 0,
10086
+ reviewContext
10087
+ ),
10088
+ approvalRequired: false,
10089
+ implementationAllowed: true,
10090
+ blockedReasonCode: "TASK_REVIEW_NOT_APPROVED"
10091
+ };
10092
+ }
9953
10093
  }
9954
10094
  if (evidenceMatchesTarget && reviewTask.reviewDecisionOutcome === "blocked") {
9955
10095
  return {
@@ -10000,11 +10140,14 @@ async function collectWorkflowStage(cwd, selector, component) {
10000
10140
  stage: "task_review",
10001
10141
  nextAction: buildAction(
10002
10142
  "task_review",
10003
- `Delegate a fresh read-only review of ${reviewTask.taskId || reviewTask.title} for ${reviewTarget.baseSha}..${reviewTarget.targetSha}. Record evidence, decision, reviewer metadata, Reviewed Head, and Reviewed Tree without modifying code.`,
10143
+ `Delegate fresh read-only review round ${evidenceMatchesTarget ? reviewContext.reviewRound : reviewTask.reviewDecisionOutcome ? reviewContext.reviewRound + 1 : reviewContext.reviewRound} of ${reviewTask.taskId || reviewTask.title} for ${reviewTarget.baseSha}..${reviewTarget.targetSha}. Record Review Round, evidence, decision, reviewer metadata, Reviewed Head, and Reviewed Tree without modifying code.`,
10004
10144
  false,
10005
10145
  null,
10006
10146
  resolveAgentReviewer(config, "task"),
10007
- reviewContext
10147
+ {
10148
+ ...reviewContext,
10149
+ reviewRound: !evidenceMatchesTarget && reviewTask.reviewDecisionOutcome ? reviewContext.reviewRound + 1 : reviewContext.reviewRound
10150
+ }
10008
10151
  ),
10009
10152
  approvalRequired: false,
10010
10153
  implementationAllowed: false,
@@ -10104,27 +10247,59 @@ async function collectWorkflowStage(cwd, selector, component) {
10104
10247
  effectiveProjectGitCwd,
10105
10248
  "feature"
10106
10249
  );
10107
- const reviewContext = reviewTarget ? { reviewScope: "feature", ...reviewTarget } : null;
10250
+ const reviewContext = reviewTarget ? {
10251
+ reviewScope: "feature",
10252
+ reviewRound: tasks.prePrReviewRound || 1,
10253
+ maxReviewRounds: resolveMaxReviewRounds(config),
10254
+ ...reviewTarget
10255
+ } : null;
10108
10256
  const evidenceMatchesTarget = !!reviewTarget && tasks.prePrReviewedHead === reviewTarget.targetSha && tasks.prePrReviewedTree === reviewTarget.targetTree;
10109
- if (reviewContext && evidenceMatchesTarget && tasks.prePrDecisionOutcome === "changes_requested") {
10110
- return {
10111
- status: "ok",
10112
- reasonCode: "WORKFLOW_STAGE_RESOLVED",
10113
- docsDir: config.docsDir,
10114
- featureRef: buildFeatureRef(feature),
10115
- stage: "feature_review_fix",
10116
- nextAction: buildAction(
10117
- "feature_review_fix",
10118
- "Address the Feature review findings, commit the remediation, and request a fresh review of the new code tree before implementation approval.",
10119
- false,
10120
- null,
10121
- void 0,
10122
- reviewContext
10123
- ),
10124
- approvalRequired: false,
10125
- implementationAllowed: true,
10126
- blockedReasonCode: "PRE_PR_REVIEW_NOT_APPROVED"
10127
- };
10257
+ if (reviewContext && tasks.prePrDecisionOutcome === "changes_requested") {
10258
+ if (reviewContext.reviewRound > reviewContext.maxReviewRounds) {
10259
+ return {
10260
+ status: "ok",
10261
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
10262
+ docsDir: config.docsDir,
10263
+ featureRef: buildFeatureRef(feature),
10264
+ stage: "review_escalation",
10265
+ nextAction: buildAction(
10266
+ "review_escalation",
10267
+ `Feature review requested more changes after ${reviewContext.maxReviewRounds} automatic remediation round(s) were used. Ask the user whether to authorize another remediation round, accept the documented risk, change scope, or hold the Feature.`,
10268
+ true,
10269
+ null,
10270
+ void 0,
10271
+ reviewContext
10272
+ ),
10273
+ approvalRequired: true,
10274
+ implementationAllowed: false,
10275
+ primaryActionLabel: "A",
10276
+ actionOptions: [
10277
+ buildStageOption("A", "A", "review_retry", "Increase workflow.agentReview.maxRounds, then run one more Feature remediation and fresh review round."),
10278
+ buildStageOption("B", "B", "hold", "Hold the Feature for user resolution.")
10279
+ ],
10280
+ blockedReasonCode: "PRE_PR_REVIEW_NOT_APPROVED"
10281
+ };
10282
+ }
10283
+ if (evidenceMatchesTarget) {
10284
+ return {
10285
+ status: "ok",
10286
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
10287
+ docsDir: config.docsDir,
10288
+ featureRef: buildFeatureRef(feature),
10289
+ stage: "feature_review_fix",
10290
+ nextAction: buildAction(
10291
+ "feature_review_fix",
10292
+ `Address the Feature review findings, commit the remediation, record review round ${reviewContext.reviewRound + 1}, and request a fresh review of the new code tree before implementation approval.`,
10293
+ false,
10294
+ null,
10295
+ void 0,
10296
+ reviewContext
10297
+ ),
10298
+ approvalRequired: false,
10299
+ implementationAllowed: true,
10300
+ blockedReasonCode: "PRE_PR_REVIEW_NOT_APPROVED"
10301
+ };
10302
+ }
10128
10303
  }
10129
10304
  if (reviewContext && evidenceMatchesTarget && tasks.prePrDecisionOutcome === "blocked") {
10130
10305
  return {
@@ -10147,6 +10322,7 @@ async function collectWorkflowStage(cwd, selector, component) {
10147
10322
  };
10148
10323
  }
10149
10324
  if (!featureReviewSatisfied(config, feature, tasks, reviewTarget)) {
10325
+ const requestedRound = reviewContext && !evidenceMatchesTarget && tasks.prePrDecisionOutcome ? reviewContext.reviewRound + 1 : reviewContext?.reviewRound;
10150
10326
  return {
10151
10327
  status: "ok",
10152
10328
  reasonCode: "WORKFLOW_STAGE_RESOLVED",
@@ -10155,11 +10331,11 @@ async function collectWorkflowStage(cwd, selector, component) {
10155
10331
  stage: "pre_pr_review",
10156
10332
  nextAction: buildAction(
10157
10333
  "pre_pr_review",
10158
- reviewTarget ? `Delegate an independent read-only Feature review to a fresh subagent for ${reviewTarget.baseSha}..${reviewTarget.targetSha}. Record findings, decision, reviewer metadata, Pre-PR Reviewed Head, and Pre-PR Reviewed Tree as evidence.` : "Create a project commit before requesting the independent Feature review.",
10334
+ reviewTarget ? `Delegate independent read-only Feature review round ${requestedRound} to a fresh subagent for ${reviewTarget.baseSha}..${reviewTarget.targetSha}. Record Review Round, findings, decision, reviewer metadata, Pre-PR Reviewed Head, and Pre-PR Reviewed Tree as evidence.` : "Create a project commit before requesting the independent Feature review.",
10159
10335
  false,
10160
10336
  null,
10161
10337
  reviewTarget ? resolveAgentReviewer(config, "feature") : void 0,
10162
- reviewContext || void 0
10338
+ reviewContext ? { ...reviewContext, reviewRound: requestedRound } : void 0
10163
10339
  ),
10164
10340
  approvalRequired: false,
10165
10341
  implementationAllowed: false,