lee-spec-kit 0.9.4 → 0.9.6

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": "\uC794\uC5EC \uC704\uD5D8\uC744 \uAE30\uB85D\uD558\uACE0 \uACC4\uC18D\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 finding remediation rounds before continuing with residual risks:",
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. After those passes are exhausted, keep the latest changes_requested findings as residual risks and automatically complete that Plan/task/Feature review gate without asking the user for a review-approval token; blocked decisions never auto-complete
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 continuing with residual risks (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,17 @@ 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
+ }
8521
+ function reviewRemediationLimitReached(config, reviewRound, decisionOutcome) {
8522
+ return decisionOutcome === "changes_requested" && (reviewRound || 1) > resolveMaxReviewRounds(config);
8523
+ }
8463
8524
  function parseWorkflowTaskLine(line, index = -1) {
8464
8525
  const canonical = parseTaskLine(line, index);
8465
8526
  if (canonical) {
@@ -8530,6 +8591,13 @@ function parseTasksDoc(content, feature) {
8530
8591
  index,
8531
8592
  ["Review Decision", "Task Review Decision", "\uD0DC\uC2A4\uD06C \uB9AC\uBDF0 Decision"]
8532
8593
  );
8594
+ const reviewRound = parseReviewRound(
8595
+ extractTaskReviewValue(nonCodeLines, index, [
8596
+ "Review Round",
8597
+ "Task Review Round",
8598
+ "\uD0DC\uC2A4\uD06C \uB9AC\uBDF0 Round"
8599
+ ])
8600
+ );
8533
8601
  const legacyTitleKey = normalizeCommitTopicText(parsed.title).toLowerCase();
8534
8602
  const sameTitleOccurrence = (legacyTitleOccurrences.get(legacyTitleKey) || 0) + 1;
8535
8603
  legacyTitleOccurrences.set(legacyTitleKey, sameTitleOccurrence);
@@ -8547,6 +8615,7 @@ function parseTasksDoc(content, feature) {
8547
8615
  ),
8548
8616
  reviewDecision,
8549
8617
  reviewDecisionOutcome: parseReviewDecisionOutcome(reviewDecision),
8618
+ reviewRound,
8550
8619
  reviewedHead: extractTaskReviewValue(
8551
8620
  nonCodeLines,
8552
8621
  index,
@@ -8602,6 +8671,9 @@ function parseTasksDoc(content, feature) {
8602
8671
  ),
8603
8672
  prePrDecision: sanitizeMetadataValue2(prePrDecision),
8604
8673
  prePrDecisionOutcome,
8674
+ prePrReviewRound: parseReviewRound(
8675
+ extractFieldValue2(content, PRE_PR_REVIEW_ROUND_LABELS)
8676
+ ),
8605
8677
  prePrReviewedHead: sanitizeMetadataValue2(
8606
8678
  extractFieldValue2(content, PRE_PR_REVIEWED_HEAD_LABELS)
8607
8679
  ),
@@ -8633,6 +8705,9 @@ function parsePlanReview(content) {
8633
8705
  const decision = sanitizeMetadataValue2(
8634
8706
  extractFieldValue2(content, PLAN_REVIEW_DECISION_LABELS)
8635
8707
  );
8708
+ const reviewRound = parseReviewRound(
8709
+ extractFieldValue2(content, PLAN_REVIEW_ROUND_LABELS)
8710
+ );
8636
8711
  const reviewedSpecHash = sanitizeMetadataValue2(
8637
8712
  extractFieldValue2(content, PLAN_REVIEWED_SPEC_HASH_LABELS)
8638
8713
  );
@@ -8644,6 +8719,7 @@ function parsePlanReview(content) {
8644
8719
  evidence,
8645
8720
  decision,
8646
8721
  decisionOutcome: parseReviewDecisionOutcome(decision),
8722
+ reviewRound,
8647
8723
  reviewedSpecHash,
8648
8724
  reviewedPlanHash,
8649
8725
  hasMetadata: PLAN_REVIEW_STATUS_LABELS.some((label) => {
@@ -8669,6 +8745,7 @@ function buildPlanReviewTarget(specContent, planContent) {
8669
8745
  ...PLAN_REVIEW_STATUS_LABELS,
8670
8746
  ...PLAN_REVIEW_EVIDENCE_LABELS,
8671
8747
  ...PLAN_REVIEW_DECISION_LABELS,
8748
+ ...PLAN_REVIEW_ROUND_LABELS,
8672
8749
  ...PLAN_REVIEWED_SPEC_HASH_LABELS,
8673
8750
  ...PLAN_REVIEWED_PLAN_HASH_LABELS
8674
8751
  ])
@@ -9167,16 +9244,32 @@ function reviewEvidenceSatisfied(config, feature, scope, evidence) {
9167
9244
  });
9168
9245
  }
9169
9246
  function planReviewSatisfied(config, feature, review, target) {
9170
- return review.status === "done" && reviewEvidenceSatisfied(config, feature, "plan", review.evidence) && review.decisionOutcome === "approve" && review.reviewedSpecHash === target.specHash && review.reviewedPlanHash === target.planHash;
9247
+ return review.status === "done" && reviewEvidenceSatisfied(config, feature, "plan", review.evidence) && (review.decisionOutcome === "approve" || reviewRemediationLimitReached(
9248
+ config,
9249
+ review.reviewRound,
9250
+ review.decisionOutcome
9251
+ )) && review.reviewedSpecHash === target.specHash && review.reviewedPlanHash === target.planHash;
9171
9252
  }
9172
9253
  function featureReviewSatisfied(config, feature, tasks, target) {
9173
- return !!target && tasks.prePrReviewStatus === "done" && reviewEvidenceSatisfied(config, feature, "feature", tasks.prePrEvidence) && !!tasks.prePrDecision && tasks.prePrDecisionOutcome === "approve" && tasks.prePrReviewedHead === target.targetSha && tasks.prePrReviewedTree === target.targetTree;
9254
+ return !!target && tasks.prePrReviewStatus === "done" && reviewEvidenceSatisfied(config, feature, "feature", tasks.prePrEvidence) && !!tasks.prePrDecision && (tasks.prePrDecisionOutcome === "approve" || reviewRemediationLimitReached(
9255
+ config,
9256
+ tasks.prePrReviewRound,
9257
+ tasks.prePrDecisionOutcome
9258
+ )) && tasks.prePrReviewedHead === target.targetSha && tasks.prePrReviewedTree === target.targetTree;
9174
9259
  }
9175
9260
  function taskReviewEvidenceSatisfied(config, feature, task, target) {
9176
- return !!target && reviewEvidenceSatisfied(config, feature, "task", task.reviewEvidence) && task.reviewDecisionOutcome === "approve" && task.reviewedHead === target.targetSha && task.reviewedTree === target.targetTree;
9261
+ return !!target && reviewEvidenceSatisfied(config, feature, "task", task.reviewEvidence) && (task.reviewDecisionOutcome === "approve" || reviewRemediationLimitReached(
9262
+ config,
9263
+ task.reviewRound,
9264
+ task.reviewDecisionOutcome
9265
+ )) && task.reviewedHead === target.targetSha && task.reviewedTree === target.targetTree;
9177
9266
  }
9178
9267
  function recordedTaskReviewSatisfied(config, feature, projectGitCwd, task, currentTargetSha) {
9179
- if (!currentTargetSha || !task.reviewedHead || !task.reviewedTree || task.reviewDecisionOutcome !== "approve" || !reviewEvidenceSatisfied(config, feature, "task", task.reviewEvidence)) {
9268
+ if (!currentTargetSha || !task.reviewedHead || !task.reviewedTree || task.reviewDecisionOutcome !== "approve" && !reviewRemediationLimitReached(
9269
+ config,
9270
+ task.reviewRound,
9271
+ task.reviewDecisionOutcome
9272
+ ) || !reviewEvidenceSatisfied(config, feature, "task", task.reviewEvidence)) {
9180
9273
  return false;
9181
9274
  }
9182
9275
  const actualTree = runGitCapture(
@@ -9585,31 +9678,38 @@ function resolveFeatureSelectionError(selection) {
9585
9678
  }
9586
9679
  function resolvePlanReviewPayload(config, feature, review, target) {
9587
9680
  const targetMatches = review.reviewedSpecHash === target.specHash && review.reviewedPlanHash === target.planHash;
9681
+ const maxReviewRounds = resolveMaxReviewRounds(config);
9682
+ const reviewRound = review.reviewRound || 1;
9683
+ const nextReviewRound = !targetMatches && review.decisionOutcome ? reviewRound + 1 : reviewRound;
9588
9684
  const actionContext = {
9589
9685
  reviewScope: "plan",
9686
+ reviewRound: nextReviewRound,
9687
+ maxReviewRounds,
9590
9688
  specHash: target.specHash,
9591
9689
  planHash: target.planHash,
9592
9690
  docsDirectory: config.docsDir
9593
9691
  };
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
- };
9692
+ if (review.decisionOutcome === "changes_requested") {
9693
+ if (targetMatches) {
9694
+ return {
9695
+ status: "ok",
9696
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
9697
+ docsDir: config.docsDir,
9698
+ featureRef: buildFeatureRef(feature),
9699
+ stage: "plan_review_fix",
9700
+ nextAction: buildAction(
9701
+ "plan_review_fix",
9702
+ `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.`,
9703
+ false,
9704
+ null,
9705
+ void 0,
9706
+ { ...actionContext, reviewRound }
9707
+ ),
9708
+ approvalRequired: false,
9709
+ implementationAllowed: false,
9710
+ blockedReasonCode: "PLAN_REVIEW_NOT_APPROVED"
9711
+ };
9712
+ }
9613
9713
  }
9614
9714
  if (targetMatches && review.decisionOutcome === "blocked") {
9615
9715
  return {
@@ -9624,7 +9724,7 @@ function resolvePlanReviewPayload(config, feature, review, target) {
9624
9724
  false,
9625
9725
  null,
9626
9726
  void 0,
9627
- actionContext
9727
+ { ...actionContext, reviewRound }
9628
9728
  ),
9629
9729
  approvalRequired: false,
9630
9730
  implementationAllowed: false,
@@ -9639,7 +9739,7 @@ function resolvePlanReviewPayload(config, feature, review, target) {
9639
9739
  stage: "plan_review",
9640
9740
  nextAction: buildAction(
9641
9741
  "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.",
9742
+ `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
9743
  false,
9644
9744
  null,
9645
9745
  resolveAgentReviewer(config, "plan"),
@@ -9690,6 +9790,11 @@ async function collectWorkflowStage(cwd, selector, component) {
9690
9790
  specContent || "",
9691
9791
  planContent || ""
9692
9792
  );
9793
+ const planReviewAutoCompleted = planReview.status === "done" && reviewEvidenceSatisfied(config, feature, "plan", planReview.evidence) && reviewRemediationLimitReached(
9794
+ config,
9795
+ planReview.reviewRound,
9796
+ planReview.decisionOutcome
9797
+ ) && planReview.reviewedSpecHash === planReviewTarget.specHash && planReview.reviewedPlanHash === planReviewTarget.planHash;
9693
9798
  const enforcePlanReview = requirements.planReviewEnabled && (tasks.docStatus !== "approved" || planReview.hasMetadata);
9694
9799
  const issueDraft = parseWorkflowDraftMetadataExtended(issueContent || "");
9695
9800
  const prDraft = parseWorkflowDraftMetadataExtended(prContent || "");
@@ -9734,7 +9839,7 @@ async function collectWorkflowStage(cwd, selector, component) {
9734
9839
  planReviewTarget
9735
9840
  );
9736
9841
  }
9737
- const approvalRequired = isReviewStage ? resolveActionApprovalRequired(config, "plan_approve", true) : false;
9842
+ const approvalRequired = isReviewStage && !planReviewAutoCompleted ? resolveActionApprovalRequired(config, "plan_approve", true) : false;
9738
9843
  const actionOptions = approvalRequired ? buildApprovalActionOptions({
9739
9844
  approveSummary: "Approve plan.md and continue to the tasks stage.",
9740
9845
  holdSummary: "Request plan changes before continuing."
@@ -9747,7 +9852,7 @@ async function collectWorkflowStage(cwd, selector, component) {
9747
9852
  stage: "plan",
9748
9853
  nextAction: buildAction(
9749
9854
  isReviewStage ? "plan_approve" : "plan_write",
9750
- isReviewStage ? approvalRequired ? "Get user approval and update plan.md status to Approved." : "Promote plan.md from Review to Approved and continue automatically." : "Write or refine plan.md until it is ready for approval.",
9855
+ isReviewStage ? planReviewAutoCompleted ? "The Plan review remediation limit was reached. Preserve the remaining findings as residual risks, promote plan.md from Review to Approved, and continue automatically without asking the user for review approval." : approvalRequired ? "Get user approval and update plan.md status to Approved." : "Promote plan.md from Review to Approved and continue automatically." : "Write or refine plan.md until it is ready for approval.",
9751
9856
  approvalRequired
9752
9857
  ),
9753
9858
  approvalRequired,
@@ -9927,29 +10032,33 @@ async function collectWorkflowStage(cwd, selector, component) {
9927
10032
  }
9928
10033
  const reviewContext = {
9929
10034
  reviewScope: "task",
10035
+ reviewRound: reviewTask.reviewRound || 1,
10036
+ maxReviewRounds: resolveMaxReviewRounds(config),
9930
10037
  ...reviewTask.taskId ? { taskId: reviewTask.taskId } : {},
9931
10038
  ...reviewTarget
9932
10039
  };
9933
10040
  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
- };
10041
+ if (reviewTask.reviewDecisionOutcome === "changes_requested") {
10042
+ if (evidenceMatchesTarget && reviewContext.reviewRound <= reviewContext.maxReviewRounds) {
10043
+ return {
10044
+ status: "ok",
10045
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
10046
+ docsDir: config.docsDir,
10047
+ featureRef: buildFeatureRef(feature),
10048
+ stage: "task_review_fix",
10049
+ nextAction: buildAction(
10050
+ "task_review_fix",
10051
+ `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}.`,
10052
+ false,
10053
+ null,
10054
+ void 0,
10055
+ reviewContext
10056
+ ),
10057
+ approvalRequired: false,
10058
+ implementationAllowed: true,
10059
+ blockedReasonCode: "TASK_REVIEW_NOT_APPROVED"
10060
+ };
10061
+ }
9953
10062
  }
9954
10063
  if (evidenceMatchesTarget && reviewTask.reviewDecisionOutcome === "blocked") {
9955
10064
  return {
@@ -9972,7 +10081,7 @@ async function collectWorkflowStage(cwd, selector, component) {
9972
10081
  };
9973
10082
  }
9974
10083
  if (taskReviewEvidenceSatisfied(config, feature, reviewTask, reviewTarget)) {
9975
- const completionSummary = reviewTask.status === "DONE" ? `The independent review approved ${reviewTask.taskId || reviewTask.title} at the current code tree. Commit the review evidence before continuing.` : `The independent review approved ${reviewTask.taskId || reviewTask.title} at the current code tree. Mark the task DONE and commit the review evidence before starting another task.`;
10084
+ const completionSummary = reviewTask.reviewDecisionOutcome === "approve" ? reviewTask.status === "DONE" ? `The independent review approved ${reviewTask.taskId || reviewTask.title} at the current code tree. Commit the review evidence before continuing.` : `The independent review approved ${reviewTask.taskId || reviewTask.title} at the current code tree. Mark the task DONE and commit the review evidence before starting another task.` : reviewTask.status === "DONE" ? `The review remediation limit was reached for ${reviewTask.taskId || reviewTask.title}. Keep the remaining findings as residual risks and commit the review evidence before continuing automatically.` : `The review remediation limit was reached for ${reviewTask.taskId || reviewTask.title}. Keep the remaining findings as residual risks, mark the task DONE, and commit the review evidence before continuing automatically.`;
9976
10085
  return {
9977
10086
  status: "ok",
9978
10087
  reasonCode: "WORKFLOW_STAGE_RESOLVED",
@@ -10000,11 +10109,14 @@ async function collectWorkflowStage(cwd, selector, component) {
10000
10109
  stage: "task_review",
10001
10110
  nextAction: buildAction(
10002
10111
  "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.`,
10112
+ `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
10113
  false,
10005
10114
  null,
10006
10115
  resolveAgentReviewer(config, "task"),
10007
- reviewContext
10116
+ {
10117
+ ...reviewContext,
10118
+ reviewRound: !evidenceMatchesTarget && reviewTask.reviewDecisionOutcome ? reviewContext.reviewRound + 1 : reviewContext.reviewRound
10119
+ }
10008
10120
  ),
10009
10121
  approvalRequired: false,
10010
10122
  implementationAllowed: false,
@@ -10104,27 +10216,34 @@ async function collectWorkflowStage(cwd, selector, component) {
10104
10216
  effectiveProjectGitCwd,
10105
10217
  "feature"
10106
10218
  );
10107
- const reviewContext = reviewTarget ? { reviewScope: "feature", ...reviewTarget } : null;
10219
+ const reviewContext = reviewTarget ? {
10220
+ reviewScope: "feature",
10221
+ reviewRound: tasks.prePrReviewRound || 1,
10222
+ maxReviewRounds: resolveMaxReviewRounds(config),
10223
+ ...reviewTarget
10224
+ } : null;
10108
10225
  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
- };
10226
+ if (reviewContext && tasks.prePrDecisionOutcome === "changes_requested") {
10227
+ if (evidenceMatchesTarget && reviewContext.reviewRound <= reviewContext.maxReviewRounds) {
10228
+ return {
10229
+ status: "ok",
10230
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
10231
+ docsDir: config.docsDir,
10232
+ featureRef: buildFeatureRef(feature),
10233
+ stage: "feature_review_fix",
10234
+ nextAction: buildAction(
10235
+ "feature_review_fix",
10236
+ `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.`,
10237
+ false,
10238
+ null,
10239
+ void 0,
10240
+ reviewContext
10241
+ ),
10242
+ approvalRequired: false,
10243
+ implementationAllowed: true,
10244
+ blockedReasonCode: "PRE_PR_REVIEW_NOT_APPROVED"
10245
+ };
10246
+ }
10128
10247
  }
10129
10248
  if (reviewContext && evidenceMatchesTarget && tasks.prePrDecisionOutcome === "blocked") {
10130
10249
  return {
@@ -10147,6 +10266,7 @@ async function collectWorkflowStage(cwd, selector, component) {
10147
10266
  };
10148
10267
  }
10149
10268
  if (!featureReviewSatisfied(config, feature, tasks, reviewTarget)) {
10269
+ const requestedRound = reviewContext && !evidenceMatchesTarget && tasks.prePrDecisionOutcome ? reviewContext.reviewRound + 1 : reviewContext?.reviewRound;
10150
10270
  return {
10151
10271
  status: "ok",
10152
10272
  reasonCode: "WORKFLOW_STAGE_RESOLVED",
@@ -10155,11 +10275,11 @@ async function collectWorkflowStage(cwd, selector, component) {
10155
10275
  stage: "pre_pr_review",
10156
10276
  nextAction: buildAction(
10157
10277
  "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.",
10278
+ 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
10279
  false,
10160
10280
  null,
10161
10281
  reviewTarget ? resolveAgentReviewer(config, "feature") : void 0,
10162
- reviewContext || void 0
10282
+ reviewContext ? { ...reviewContext, reviewRound: requestedRound } : void 0
10163
10283
  ),
10164
10284
  approvalRequired: false,
10165
10285
  implementationAllowed: false,