lee-spec-kit 0.9.5 → 0.9.7

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
@@ -167,6 +167,7 @@ var koCli = {
167
167
  "config.fullstackRepoRequired": "Multi \uD504\uB85C\uC81D\uD2B8\uB294 --component\uB85C \uB300\uC0C1 \uCEF4\uD3EC\uB10C\uD2B8\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.",
168
168
  "config.projectRootSet": "\u2705 {repo} projectRoot \uC124\uC815 \uC644\uB8CC: {path}",
169
169
  "config.projectRootSetSingle": "\u2705 projectRoot \uC124\uC815 \uC644\uB8CC: {path}",
170
+ "config.workflowUpdated": "\u2705 \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC124\uC815\uC744 \uC5C5\uB370\uC774\uD2B8\uD588\uC2B5\uB2C8\uB2E4.",
170
171
  "update.start": "\u{1F4E6} \uD15C\uD50C\uB9BF \uC5C5\uB370\uC774\uD2B8\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4...",
171
172
  "update.langLabel": "\uC5B8\uC5B4",
172
173
  "update.typeLabel": "\uD0C0\uC785",
@@ -179,6 +180,7 @@ var koCli = {
179
180
  "update.fileUpdated": "{file} \uC5C5\uB370\uC774\uD2B8",
180
181
  "update.gitStatusUnavailable": "git \uC0C1\uD0DC\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (git repo\uAC00 \uC544\uB2C8\uAC70\uB098 git \uC2E4\uD589 \uBD88\uAC00) --force\uB85C \uAC15\uC81C \uB36E\uC5B4\uC4F0\uAE30\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.",
181
182
  "update.docsWorktreeDirty": "docs \uC791\uC5C5\uD2B8\uB9AC\uC5D0 \uBCC0\uACBD\uC0AC\uD56D\uC774 \uC788\uC5B4 update\uB97C \uC9C4\uD589\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBCC0\uACBD\uC0AC\uD56D\uC744 \uCEE4\uBC0B/\uC2A4\uD0DC\uC2DC \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uAC70\uB098 --force\uB85C \uB36E\uC5B4\uC4F0\uC138\uC694.",
183
+ "update.legacyAutomationSafe": "\uAE30\uC874 \uD504\uB85C\uC81D\uD2B8\uB294 \uBA85\uC2DC\uC801\uC73C\uB85C \uC0AC\uC6A9\uD55C \uACBD\uC6B0\uAC00 \uC544\uB2C8\uBA74 \uC0C8 Task \uC704\uC784\uACFC Plan/Task \uAC80\uC218\uB97C \uAEBC\uC9C4 \uC0C1\uD0DC\uB85C \uC720\uC9C0\uD569\uB2C8\uB2E4. `npx lee-spec-kit config --interactive` \uB610\uB294 config \uD50C\uB798\uADF8\uB85C \uC124\uC815\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
182
184
  "init.selectLangPrompt": "\uBB38\uC11C \uC5B8\uC5B4\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
183
185
  "init.currentDirectoryLabel": "\u{1F4CD} \uD604\uC7AC \uC704\uCE58",
184
186
  "init.gitDetected": "\u2705 Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uAC10\uC9C0\uB428",
@@ -213,7 +215,7 @@ var koCli = {
213
215
  "init.choice.taskAgent.on": "\uC0AC\uC6A9 - \uAC01 Task \uAD6C\uD604\uC744 \uC704\uC784",
214
216
  "init.choice.taskAgent.off": "\uC0AC\uC6A9 \uC548 \uD568 - \uBA54\uC778 \uC5D0\uC774\uC804\uD2B8\uAC00 \uC9C1\uC811 \uAD6C\uD604",
215
217
  "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:",
218
+ "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:",
217
219
  "init.choice.review.plan": "Plan",
218
220
  "init.choice.review.task": "Task",
219
221
  "init.choice.review.feature": "Feature",
@@ -481,6 +483,7 @@ var enCli = {
481
483
  "config.fullstackRepoRequired": "For multi projects, specify a target component via `--component`.",
482
484
  "config.projectRootSet": "\u2705 {repo} projectRoot set: {path}",
483
485
  "config.projectRootSetSingle": "\u2705 projectRoot set: {path}",
486
+ "config.workflowUpdated": "\u2705 Workflow configuration updated.",
484
487
  "update.start": "\u{1F4E6} Starting template update...",
485
488
  "update.langLabel": "Lang",
486
489
  "update.typeLabel": "Type",
@@ -493,6 +496,7 @@ var enCli = {
493
496
  "update.fileUpdated": "{file} updated",
494
497
  "update.gitStatusUnavailable": "Cannot determine git status (not a git repo or git unavailable). Use --force to overwrite.",
495
498
  "update.docsWorktreeDirty": "Docs working tree has changes. Commit/stash your changes, or run with --force to overwrite.",
499
+ "update.legacyAutomationSafe": "Existing projects keep newly introduced task delegation and Plan/Task review disabled unless they had explicitly opted in. Configure them with `npx lee-spec-kit config --interactive` or the config flags.",
496
500
  "init.selectLangPrompt": "Select docs language:",
497
501
  "init.currentDirectoryLabel": "\u{1F4CD} Current directory",
498
502
  "init.gitDetected": "\u2705 Git repository detected",
@@ -527,7 +531,7 @@ var enCli = {
527
531
  "init.choice.taskAgent.on": "On - delegate each task implementation",
528
532
  "init.choice.taskAgent.off": "Off - implement tasks in the main agent",
529
533
  "init.prompt.reviews": "Select independent review gates:",
530
- "init.prompt.maxReviewRounds": "Maximum automatic review finding remediation rounds before asking the user:",
534
+ "init.prompt.maxReviewRounds": "Maximum automatic finding remediation rounds before continuing with residual risks:",
531
535
  "init.choice.review.plan": "Plan",
532
536
  "init.choice.review.task": "Task",
533
537
  "init.choice.review.feature": "Feature",
@@ -1374,9 +1378,12 @@ Before taking the next workflow step:
1374
1378
  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
1379
  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
1376
1380
  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
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
1381
+ 19. After delegating to a subagent, wait until it returns a terminal outcome: completed, explicit failure, cancellation, or an approval/user-input request that requires action
1382
+ 20. While the subagent remains running, use repeated bounded waits, preferably longer waits. A bounded wait that returns no update, a lack of status messages, or a lack of file changes means only that the subagent is still pending; none is evidence of failure or stalled work. Read-only review subagents are expected not to modify files
1383
+ 21. Do not interrupt, replace, or abandon a running subagent solely because it has been quiet or has not changed files. Stop it only after an explicit user request, a terminal failure/cancellation, or an unrecoverable runtime status
1384
+ 22. 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
1385
+ 23. 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
1386
+ 24. 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
1380
1387
 
1381
1388
  Approval and remote actions:
1382
1389
 
@@ -1696,6 +1703,7 @@ function toProjectConfig(docsDir, configFile, projectType, components) {
1696
1703
  return {
1697
1704
  schemaId: "lee-spec",
1698
1705
  docsDir,
1706
+ createdAt: configFile.createdAt,
1699
1707
  projectName: configFile.projectName,
1700
1708
  projectType,
1701
1709
  components: projectType === "multi" ? components : void 0,
@@ -2131,7 +2139,7 @@ function initCommand(program2) {
2131
2139
  "Review gates: comma-separated plan,task,feature | none"
2132
2140
  ).option(
2133
2141
  "--max-review-rounds <count>",
2134
- "Automatic review finding remediation rounds before user escalation (default: 1)"
2142
+ "Automatic review finding remediation rounds before continuing with residual risks (default: 1)"
2135
2143
  ).option(
2136
2144
  "--completion-strategy <strategy>",
2137
2145
  "Local completion: local-ff | local-squash | none"
@@ -2865,6 +2873,7 @@ async function runInit(options) {
2865
2873
  featureChecks: [],
2866
2874
  postMergeChecks: []
2867
2875
  } : {},
2876
+ agentAutomationConfigured: true,
2868
2877
  agentExecution: {
2869
2878
  task: {
2870
2879
  ...createDefaultAgentExecutionTaskConfig(),
@@ -3816,6 +3825,50 @@ function migrateLegacyApprovalSettings(approval) {
3816
3825
  return { approval, changedPaths };
3817
3826
  }
3818
3827
 
3828
+ // src/config/agent-automation.ts
3829
+ var AGENT_AUTOMATION_OPT_IN_INIT_DATE = "2026-08-27";
3830
+ function isPlainRecord2(value) {
3831
+ return !!value && typeof value === "object" && !Array.isArray(value);
3832
+ }
3833
+ function matchesGeneratedAgentRuntime(value, options = {}) {
3834
+ if (!isPlainRecord2(value)) return false;
3835
+ const expectedKeys = [
3836
+ ...typeof options.enabled === "boolean" ? ["enabled"] : [],
3837
+ "model",
3838
+ "onUnavailable",
3839
+ "reasoningEffort",
3840
+ "type"
3841
+ ].sort();
3842
+ if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(expectedKeys)) {
3843
+ return false;
3844
+ }
3845
+ return (typeof options.enabled !== "boolean" || value.enabled === options.enabled) && value.type === "subagent" && value.model === "inherit" && value.reasoningEffort === "high" && value.onUnavailable === "inherit";
3846
+ }
3847
+ function matchesGeneratedReviewPhase(value, enabled) {
3848
+ if (!isPlainRecord2(value)) return false;
3849
+ if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(["enabled", "evidenceMode", "reviewer"])) {
3850
+ return false;
3851
+ }
3852
+ return value.enabled === enabled && value.evidenceMode === "path_required" && matchesGeneratedAgentRuntime(value.reviewer);
3853
+ }
3854
+ function resolveLegacyBackfilledAgentAutomation(config) {
3855
+ const none = { taskExecution: false, planReview: false };
3856
+ if (!isPlainRecord2(config.workflow)) return none;
3857
+ const workflow = config.workflow;
3858
+ if (workflow.agentAutomationConfigured === true) return none;
3859
+ const createdAt = typeof config.createdAt === "string" ? config.createdAt.trim() : "";
3860
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(createdAt)) return none;
3861
+ if (createdAt >= AGENT_AUTOMATION_OPT_IN_INIT_DATE) return none;
3862
+ const taskExecution = isPlainRecord2(workflow.agentExecution) ? workflow.agentExecution.task : null;
3863
+ const planReview = isPlainRecord2(workflow.agentReview) ? workflow.agentReview.plan : null;
3864
+ return {
3865
+ taskExecution: matchesGeneratedAgentRuntime(taskExecution, {
3866
+ enabled: true
3867
+ }),
3868
+ planReview: matchesGeneratedReviewPhase(planReview, true)
3869
+ };
3870
+ }
3871
+
3819
3872
  // src/commands/update.ts
3820
3873
  function hasOwnKey2(value, key) {
3821
3874
  return Object.prototype.hasOwnProperty.call(value, key);
@@ -3903,7 +3956,9 @@ async function runUpdate(options) {
3903
3956
  const updateAgents = options.agents || !hasExplicitSelection;
3904
3957
  const updateAgentsMd = options.agentsMd || !hasExplicitSelection;
3905
3958
  console.log(chalk.blue(tr(lang, "cli", "update.start")));
3906
- console.log(chalk.gray(` - ${tr(lang, "cli", "update.langLabel")}: ${lang}`));
3959
+ console.log(
3960
+ chalk.gray(` - ${tr(lang, "cli", "update.langLabel")}: ${lang}`)
3961
+ );
3907
3962
  console.log(
3908
3963
  chalk.gray(` - ${tr(lang, "cli", "update.typeLabel")}: ${projectType}`)
3909
3964
  );
@@ -3911,7 +3966,12 @@ async function runUpdate(options) {
3911
3966
  let updatedCount = 0;
3912
3967
  if (updateAgents) {
3913
3968
  console.log(chalk.blue(tr(lang, "cli", "update.updatingAgents")));
3914
- const commonAgentsBase = path8.join(templatesDir, lang, "common", "agents");
3969
+ const commonAgentsBase = path8.join(
3970
+ templatesDir,
3971
+ lang,
3972
+ "common",
3973
+ "agents"
3974
+ );
3915
3975
  const targetAgentsBase = path8.join(docsDir, "agents");
3916
3976
  const commonAgents = commonAgentsBase;
3917
3977
  const targetAgents = targetAgentsBase;
@@ -3940,13 +4000,14 @@ async function runUpdate(options) {
3940
4000
  updatedCount += count;
3941
4001
  }
3942
4002
  console.log(
3943
- chalk.green(
3944
- ` \u2705 ${tr(lang, "cli", "update.agentsUpdated")}`
3945
- )
4003
+ chalk.green(` \u2705 ${tr(lang, "cli", "update.agentsUpdated")}`)
3946
4004
  );
3947
4005
  }
3948
4006
  if (updateAgentsMd) {
3949
- const agentsMdTargets = await collectAgentsMdTargets(cwd, currentConfig);
4007
+ const agentsMdTargets = await collectAgentsMdTargets(
4008
+ cwd,
4009
+ currentConfig
4010
+ );
3950
4011
  for (const target of agentsMdTargets) {
3951
4012
  const result = await upsertLeeSpecKitAgentsMd(target, {
3952
4013
  lang,
@@ -3975,13 +4036,24 @@ async function runUpdate(options) {
3975
4036
  )
3976
4037
  );
3977
4038
  console.log(
3978
- chalk.gray(
3979
- ` (${configBackfill.changedPaths.join(", ")})`
3980
- )
4039
+ chalk.gray(` (${configBackfill.changedPaths.join(", ")})`)
3981
4040
  );
4041
+ if (configBackfill.changedPaths.includes(
4042
+ "workflow.agentExecution.task.enabled"
4043
+ ) || configBackfill.changedPaths.includes(
4044
+ "workflow.agentReview.plan.enabled"
4045
+ )) {
4046
+ console.log(
4047
+ chalk.yellow(
4048
+ ` - ${tr(lang, "cli", "update.legacyAutomationSafe")}`
4049
+ )
4050
+ );
4051
+ }
3982
4052
  }
3983
4053
  console.log(
3984
- chalk.green(`\u2705 ${tr(lang, "cli", "update.updatedTotal", { count: updatedCount })}`)
4054
+ chalk.green(
4055
+ `\u2705 ${tr(lang, "cli", "update.updatedTotal", { count: updatedCount })}`
4056
+ )
3985
4057
  );
3986
4058
  },
3987
4059
  { owner: "update" }
@@ -4050,6 +4122,7 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
4050
4122
  }
4051
4123
  const workflow = raw.workflow;
4052
4124
  changedPaths.push(...migrateLegacyWorkflowSettings(workflow));
4125
+ const restoreLegacyAgentAutomationDefaults = resolveLegacyBackfilledAgentAutomation(raw);
4053
4126
  setIfMissing(
4054
4127
  workflow,
4055
4128
  "requireWorktree",
@@ -4154,11 +4227,15 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
4154
4227
  setIfMissing(
4155
4228
  taskExecution,
4156
4229
  "enabled",
4157
- defaultTaskExecution.enabled,
4230
+ false,
4158
4231
  "workflow.agentExecution.task.enabled"
4159
4232
  );
4160
4233
  if (typeof taskExecution.enabled !== "boolean") {
4161
- taskExecution.enabled = defaultTaskExecution.enabled;
4234
+ taskExecution.enabled = false;
4235
+ changedPaths.push("workflow.agentExecution.task.enabled");
4236
+ }
4237
+ if (restoreLegacyAgentAutomationDefaults.taskExecution && taskExecution.enabled !== false) {
4238
+ taskExecution.enabled = false;
4162
4239
  changedPaths.push("workflow.agentExecution.task.enabled");
4163
4240
  }
4164
4241
  if (taskExecution.type !== "subagent") {
@@ -4248,14 +4325,17 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
4248
4325
  changedPaths.push(`${phasePath}.reviewer.onUnavailable`);
4249
4326
  }
4250
4327
  };
4251
- normalizeAgentReviewPhase("plan", true);
4328
+ normalizeAgentReviewPhase("plan", false);
4329
+ if (restoreLegacyAgentAutomationDefaults.planReview) {
4330
+ const planReview = agentReview.plan;
4331
+ if (planReview.enabled !== false) {
4332
+ planReview.enabled = false;
4333
+ changedPaths.push("workflow.agentReview.plan.enabled");
4334
+ }
4335
+ }
4252
4336
  normalizeAgentReviewPhase("task", false);
4253
4337
  const legacyFeatureEnabled = typeof legacyPrePrReview?.enabled === "boolean" ? legacyPrePrReview.enabled : workflow.mode !== "local";
4254
- normalizeAgentReviewPhase(
4255
- "feature",
4256
- legacyFeatureEnabled,
4257
- legacyPrePrReview
4258
- );
4338
+ normalizeAgentReviewPhase("feature", legacyFeatureEnabled, legacyPrePrReview);
4259
4339
  if (hasOwnKey2(workflow, "prePrReview")) {
4260
4340
  delete workflow.prePrReview;
4261
4341
  changedPaths.push("workflow.prePrReview");
@@ -4383,11 +4463,15 @@ function getDocsPorcelainStatus(docsDir, ignoredAbsPaths = []) {
4383
4463
  if (!top) return null;
4384
4464
  const rel = path8.relative(top, docsDir) || ".";
4385
4465
  try {
4386
- const output = execFileSync("git", ["status", "--porcelain=v1", "--", rel], {
4387
- cwd: top,
4388
- encoding: "utf-8",
4389
- stdio: ["ignore", "pipe", "ignore"]
4390
- });
4466
+ const output = execFileSync(
4467
+ "git",
4468
+ ["status", "--porcelain=v1", "--", rel],
4469
+ {
4470
+ cwd: top,
4471
+ encoding: "utf-8",
4472
+ stdio: ["ignore", "pipe", "ignore"]
4473
+ }
4474
+ );
4391
4475
  if (ignoredAbsPaths.length === 0) {
4392
4476
  return output;
4393
4477
  }
@@ -4423,8 +4507,62 @@ async function isDocsWorktreeCleanOrThrow(docsDir, lang, ignoredAbsPaths = []) {
4423
4507
  }
4424
4508
  return true;
4425
4509
  }
4510
+ var CONFIG_REVIEW_PHASES = ["plan", "task", "feature"];
4511
+ function isPlainObject2(value) {
4512
+ return !!value && typeof value === "object" && !Array.isArray(value);
4513
+ }
4514
+ function parseReviews(value) {
4515
+ if (typeof value === "undefined") return void 0;
4516
+ const normalized = value.split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean);
4517
+ if (normalized.length === 1 && normalized[0] === "none") return [];
4518
+ if (normalized.length === 0 || normalized.includes("none") || normalized.some(
4519
+ (entry) => !CONFIG_REVIEW_PHASES.includes(entry)
4520
+ )) {
4521
+ throw createCliError(
4522
+ "INVALID_ARGUMENT",
4523
+ "`--reviews` must be a comma-separated subset of `plan,task,feature`, or `none`."
4524
+ );
4525
+ }
4526
+ return CONFIG_REVIEW_PHASES.filter((phase) => normalized.includes(phase));
4527
+ }
4528
+ function parseMaxReviewRounds(value) {
4529
+ if (typeof value === "undefined") return void 0;
4530
+ const parsed = Number(value);
4531
+ if (!Number.isInteger(parsed) || parsed < 1) {
4532
+ throw createCliError(
4533
+ "INVALID_ARGUMENT",
4534
+ "`--max-review-rounds` must be a positive integer."
4535
+ );
4536
+ }
4537
+ return parsed;
4538
+ }
4539
+ function validateWorkflowOptions(options) {
4540
+ if (options.taskAgent && !["on", "off"].includes(options.taskAgent)) {
4541
+ throw createCliError(
4542
+ "INVALID_ARGUMENT",
4543
+ "`--task-agent` must be `on` or `off`."
4544
+ );
4545
+ }
4546
+ if (options.completionStrategy && !["local-ff", "local-squash", "none"].includes(options.completionStrategy)) {
4547
+ throw createCliError(
4548
+ "INVALID_ARGUMENT",
4549
+ "`--completion-strategy` must be `local-ff`, `local-squash`, or `none`."
4550
+ );
4551
+ }
4552
+ parseReviews(options.reviews);
4553
+ parseMaxReviewRounds(options.maxReviewRounds);
4554
+ }
4426
4555
  function configCommand(program2) {
4427
- program2.command("config").description("View or modify project configuration").option("--dir <dir>", "Docs directory or project path to target").option("--project-root <path>", "Set project root path").option("--component <component>", "Component name for multi projects").option("--non-interactive", "Fail instead of prompting for input").action(async (options) => {
4556
+ program2.command("config").description("View or modify project configuration").option("--dir <dir>", "Docs directory or project path to target").option("--project-root <path>", "Set project root path").option("--component <component>", "Component name for multi projects").option("--task-agent <mode>", "Task implementation agent: on | off").option(
4557
+ "--reviews <list>",
4558
+ "Review gates: comma-separated plan,task,feature | none"
4559
+ ).option(
4560
+ "--max-review-rounds <count>",
4561
+ "Automatic review finding remediation rounds before continuing with residual risks"
4562
+ ).option(
4563
+ "--completion-strategy <strategy>",
4564
+ "Local completion: local-ff | local-squash | none"
4565
+ ).option("--interactive", "Configure workflow options interactively").option("--non-interactive", "Fail instead of prompting for input").action(async (options) => {
4428
4566
  try {
4429
4567
  await runConfig(options);
4430
4568
  } catch (error) {
@@ -4450,6 +4588,7 @@ ${tr(lang2, "cli", "common.canceled")}`));
4450
4588
  });
4451
4589
  }
4452
4590
  async function runConfig(options) {
4591
+ validateWorkflowOptions(options);
4453
4592
  const cwd = process.cwd();
4454
4593
  const targetCwd = options.dir ? path8.resolve(cwd, options.dir) : cwd;
4455
4594
  const config = await getConfig(targetCwd);
@@ -4460,7 +4599,8 @@ async function runConfig(options) {
4460
4599
  );
4461
4600
  }
4462
4601
  const configPath = path8.join(config.docsDir, ".lee-spec-kit.json");
4463
- if (!options.projectRoot) {
4602
+ const hasWorkflowOptions = typeof options.taskAgent !== "undefined" || typeof options.reviews !== "undefined" || typeof options.maxReviewRounds !== "undefined" || typeof options.completionStrategy !== "undefined" || !!options.interactive;
4603
+ if (!options.projectRoot && !hasWorkflowOptions) {
4464
4604
  console.log();
4465
4605
  console.log(chalk.blue(tr(config.lang, "cli", "config.currentTitle")));
4466
4606
  console.log();
@@ -4475,85 +4615,21 @@ async function runConfig(options) {
4475
4615
  console.log();
4476
4616
  return;
4477
4617
  }
4478
- const projectRoot = options.projectRoot;
4479
4618
  await withFileLock(
4480
4619
  getDocsLockPath(config.docsDir),
4481
4620
  async () => {
4482
4621
  const configFile = await fs.readJson(configPath);
4483
- if (configFile.docsRepo !== "standalone") {
4484
- console.log(
4485
- chalk.yellow(tr(config.lang, "cli", "config.projectRootStandaloneOnly"))
4622
+ if (!isPlainObject2(configFile)) {
4623
+ throw createCliError(
4624
+ "PRECONDITION_FAILED",
4625
+ "Invalid project configuration."
4486
4626
  );
4487
- return;
4488
4627
  }
4489
- const projectType = normalizeProjectType(String(configFile.projectType || "single"));
4490
- const targetFromOptions = options.component?.trim().toLowerCase();
4491
- if (projectType === "multi") {
4492
- const components = resolveProjectComponents(projectType, configFile.components);
4493
- let targetComponent = targetFromOptions;
4494
- if (!targetComponent) {
4495
- if (components.length === 1) {
4496
- targetComponent = components[0];
4497
- } else if (options.nonInteractive) {
4498
- throw createCliError(
4499
- "PROMPT_BLOCKED",
4500
- "`--component` is required for multi projectRoot update when using `--non-interactive`."
4501
- );
4502
- } else {
4503
- const response = await prompts(
4504
- [
4505
- {
4506
- type: "select",
4507
- name: "component",
4508
- message: tr(config.lang, "cli", "config.selectRepoToUpdate"),
4509
- choices: components.map((value) => ({
4510
- title: value.toUpperCase(),
4511
- value
4512
- }))
4513
- }
4514
- ],
4515
- {
4516
- onCancel: () => {
4517
- throw new Error("canceled");
4518
- }
4519
- }
4520
- );
4521
- targetComponent = response.component;
4522
- }
4523
- }
4524
- if (!targetComponent) {
4525
- throw createCliError(
4526
- "INVALID_ARGUMENT",
4527
- "Component selection is required."
4528
- );
4529
- }
4530
- assertAllowedComponent(targetComponent, components);
4531
- const currentRoot = typeof configFile.projectRoot === "object" && configFile.projectRoot ? configFile.projectRoot : {};
4532
- currentRoot[targetComponent] = projectRoot;
4533
- configFile.projectRoot = currentRoot;
4534
- console.log(
4535
- chalk.green(
4536
- tr(config.lang, "cli", "config.projectRootSet", {
4537
- repo: targetComponent.toUpperCase(),
4538
- path: projectRoot
4539
- })
4540
- )
4541
- );
4542
- } else {
4543
- if (targetFromOptions) {
4544
- throw createCliError(
4545
- "INVALID_ARGUMENT",
4546
- "`--component` is only valid for multi projectRoot updates."
4547
- );
4548
- }
4549
- configFile.projectRoot = projectRoot;
4550
- console.log(
4551
- chalk.green(
4552
- tr(config.lang, "cli", "config.projectRootSetSingle", {
4553
- path: projectRoot
4554
- })
4555
- )
4556
- );
4628
+ if (options.projectRoot) {
4629
+ await updateProjectRoot(configFile, options, config.lang);
4630
+ }
4631
+ if (hasWorkflowOptions) {
4632
+ await updateWorkflowConfig(configFile, options, config.lang);
4557
4633
  }
4558
4634
  await fs.writeJson(configPath, configFile, { spaces: 2 });
4559
4635
  console.log();
@@ -4561,6 +4637,255 @@ async function runConfig(options) {
4561
4637
  { owner: "config" }
4562
4638
  );
4563
4639
  }
4640
+ async function updateProjectRoot(configFile, options, lang) {
4641
+ if (configFile.docsRepo !== "standalone") {
4642
+ console.log(
4643
+ chalk.yellow(tr(lang, "cli", "config.projectRootStandaloneOnly"))
4644
+ );
4645
+ return;
4646
+ }
4647
+ const projectRoot = options.projectRoot;
4648
+ if (!projectRoot) return;
4649
+ const projectType = normalizeProjectType(
4650
+ String(configFile.projectType || "single")
4651
+ );
4652
+ const targetFromOptions = options.component?.trim().toLowerCase();
4653
+ if (projectType === "multi") {
4654
+ const components = resolveProjectComponents(
4655
+ projectType,
4656
+ configFile.components
4657
+ );
4658
+ let targetComponent = targetFromOptions;
4659
+ if (!targetComponent) {
4660
+ if (components.length === 1) {
4661
+ targetComponent = components[0];
4662
+ } else if (options.nonInteractive) {
4663
+ throw createCliError(
4664
+ "PROMPT_BLOCKED",
4665
+ "`--component` is required for multi projectRoot update when using `--non-interactive`."
4666
+ );
4667
+ } else {
4668
+ const response = await prompts(
4669
+ [
4670
+ {
4671
+ type: "select",
4672
+ name: "component",
4673
+ message: tr(lang, "cli", "config.selectRepoToUpdate"),
4674
+ choices: components.map((value) => ({
4675
+ title: value.toUpperCase(),
4676
+ value
4677
+ }))
4678
+ }
4679
+ ],
4680
+ {
4681
+ onCancel: () => {
4682
+ throw new Error("canceled");
4683
+ }
4684
+ }
4685
+ );
4686
+ targetComponent = response.component;
4687
+ }
4688
+ }
4689
+ if (!targetComponent) {
4690
+ throw createCliError(
4691
+ "INVALID_ARGUMENT",
4692
+ "Component selection is required."
4693
+ );
4694
+ }
4695
+ assertAllowedComponent(targetComponent, components);
4696
+ const currentRoot = isPlainObject2(
4697
+ configFile.projectRoot
4698
+ ) ? Object.fromEntries(
4699
+ Object.entries(configFile.projectRoot).filter(
4700
+ (entry) => typeof entry[1] === "string"
4701
+ )
4702
+ ) : {};
4703
+ currentRoot[targetComponent] = projectRoot;
4704
+ configFile.projectRoot = currentRoot;
4705
+ console.log(
4706
+ chalk.green(
4707
+ tr(lang, "cli", "config.projectRootSet", {
4708
+ repo: targetComponent.toUpperCase(),
4709
+ path: projectRoot
4710
+ })
4711
+ )
4712
+ );
4713
+ } else {
4714
+ if (targetFromOptions) {
4715
+ throw createCliError(
4716
+ "INVALID_ARGUMENT",
4717
+ "`--component` is only valid for multi projectRoot updates."
4718
+ );
4719
+ }
4720
+ configFile.projectRoot = projectRoot;
4721
+ console.log(
4722
+ chalk.green(
4723
+ tr(lang, "cli", "config.projectRootSetSingle", {
4724
+ path: projectRoot
4725
+ })
4726
+ )
4727
+ );
4728
+ }
4729
+ }
4730
+ function ensureWorkflow(configFile) {
4731
+ if (!isPlainObject2(configFile.workflow)) configFile.workflow = {};
4732
+ return configFile.workflow;
4733
+ }
4734
+ function ensureTaskExecution(workflow) {
4735
+ if (!isPlainObject2(workflow.agentExecution)) workflow.agentExecution = {};
4736
+ const agentExecution = workflow.agentExecution;
4737
+ if (!isPlainObject2(agentExecution.task)) {
4738
+ agentExecution.task = createDefaultAgentExecutionTaskConfig();
4739
+ }
4740
+ return agentExecution.task;
4741
+ }
4742
+ function ensureAgentReview(workflow) {
4743
+ if (!isPlainObject2(workflow.agentReview)) workflow.agentReview = {};
4744
+ return workflow.agentReview;
4745
+ }
4746
+ function ensureReviewPhase(agentReview, phaseName) {
4747
+ if (!isPlainObject2(agentReview[phaseName])) {
4748
+ agentReview[phaseName] = {
4749
+ enabled: false,
4750
+ evidenceMode: "path_required",
4751
+ reviewer: createDefaultAgentReviewerConfig()
4752
+ };
4753
+ }
4754
+ const phase = agentReview[phaseName];
4755
+ if (!isPlainObject2(phase.reviewer)) {
4756
+ phase.reviewer = createDefaultAgentReviewerConfig();
4757
+ }
4758
+ if (phase.evidenceMode !== "any" && phase.evidenceMode !== "path_required") {
4759
+ phase.evidenceMode = "path_required";
4760
+ }
4761
+ return phase;
4762
+ }
4763
+ function currentReviewSelection(workflow) {
4764
+ const agentReview = isPlainObject2(workflow.agentReview) ? workflow.agentReview : {};
4765
+ const legacyFeature = isPlainObject2(workflow.prePrReview) ? workflow.prePrReview.enabled : void 0;
4766
+ const isLocal = workflow.mode === "local" || workflow.preset === "local";
4767
+ return CONFIG_REVIEW_PHASES.filter((phase) => {
4768
+ const phaseConfig = isPlainObject2(agentReview[phase]) ? agentReview[phase] : {};
4769
+ if (typeof phaseConfig.enabled === "boolean") return phaseConfig.enabled;
4770
+ if (phase === "feature") {
4771
+ return typeof legacyFeature === "boolean" ? legacyFeature : !isLocal;
4772
+ }
4773
+ return false;
4774
+ });
4775
+ }
4776
+ async function updateWorkflowConfig(configFile, options, lang) {
4777
+ const workflow = ensureWorkflow(configFile);
4778
+ const legacyBackfilledAgentAutomation = resolveLegacyBackfilledAgentAutomation(configFile);
4779
+ if (legacyBackfilledAgentAutomation.taskExecution) {
4780
+ ensureTaskExecution(workflow).enabled = false;
4781
+ }
4782
+ if (legacyBackfilledAgentAutomation.planReview) {
4783
+ ensureReviewPhase(ensureAgentReview(workflow), "plan").enabled = false;
4784
+ }
4785
+ const isLocal = workflow.mode === "local" || workflow.preset === "local";
4786
+ if (options.interactive && options.nonInteractive) {
4787
+ throw createCliError(
4788
+ "PROMPT_BLOCKED",
4789
+ "`--interactive` cannot be used with `--non-interactive`."
4790
+ );
4791
+ }
4792
+ let taskAgentEnabled = options.taskAgent === "on" ? true : options.taskAgent === "off" ? false : void 0;
4793
+ let reviews = parseReviews(options.reviews);
4794
+ let maxReviewRounds = parseMaxReviewRounds(options.maxReviewRounds);
4795
+ let completionStrategy = options.completionStrategy;
4796
+ if (options.interactive) {
4797
+ const selectedReviews = currentReviewSelection(workflow);
4798
+ const currentTaskExecution = isPlainObject2(workflow.agentExecution) && isPlainObject2(workflow.agentExecution.task) ? workflow.agentExecution.task.enabled === true : false;
4799
+ const currentAgentReview = isPlainObject2(workflow.agentReview) ? workflow.agentReview : {};
4800
+ const currentMaxRounds = typeof currentAgentReview.maxRounds === "number" && Number.isInteger(currentAgentReview.maxRounds) && currentAgentReview.maxRounds > 0 ? currentAgentReview.maxRounds : 1;
4801
+ const response = await prompts(
4802
+ [
4803
+ {
4804
+ type: typeof options.taskAgent === "undefined" ? "select" : null,
4805
+ name: "taskAgent",
4806
+ message: tr(lang, "cli", "init.prompt.taskAgent"),
4807
+ choices: [
4808
+ { title: tr(lang, "cli", "init.choice.taskAgent.on"), value: true },
4809
+ {
4810
+ title: tr(lang, "cli", "init.choice.taskAgent.off"),
4811
+ value: false
4812
+ }
4813
+ ],
4814
+ initial: currentTaskExecution ? 0 : 1
4815
+ },
4816
+ {
4817
+ type: typeof options.reviews === "undefined" ? "multiselect" : null,
4818
+ name: "reviews",
4819
+ message: tr(lang, "cli", "init.prompt.reviews"),
4820
+ choices: CONFIG_REVIEW_PHASES.map((phase) => ({
4821
+ title: tr(lang, "cli", `init.choice.review.${phase}`),
4822
+ value: phase,
4823
+ selected: selectedReviews.includes(phase)
4824
+ }))
4825
+ },
4826
+ {
4827
+ type: typeof options.maxReviewRounds === "undefined" ? "number" : null,
4828
+ name: "maxReviewRounds",
4829
+ message: tr(lang, "cli", "init.prompt.maxReviewRounds"),
4830
+ initial: currentMaxRounds,
4831
+ min: 1
4832
+ },
4833
+ {
4834
+ type: isLocal && typeof options.completionStrategy === "undefined" ? "select" : null,
4835
+ name: "completionStrategy",
4836
+ message: tr(lang, "cli", "init.prompt.completionStrategy"),
4837
+ choices: [
4838
+ { title: "local-ff", value: "local-ff" },
4839
+ { title: "local-squash", value: "local-squash" },
4840
+ { title: "none", value: "none" }
4841
+ ],
4842
+ initial: ["local-ff", "local-squash", "none"].indexOf(
4843
+ String(workflow.completionStrategy || "none")
4844
+ )
4845
+ }
4846
+ ],
4847
+ {
4848
+ onCancel: () => {
4849
+ throw new Error("canceled");
4850
+ }
4851
+ }
4852
+ );
4853
+ if (typeof response.taskAgent === "boolean")
4854
+ taskAgentEnabled = response.taskAgent;
4855
+ if (Array.isArray(response.reviews)) {
4856
+ reviews = CONFIG_REVIEW_PHASES.filter(
4857
+ (phase) => response.reviews.includes(phase)
4858
+ );
4859
+ }
4860
+ if (Number.isInteger(response.maxReviewRounds) && response.maxReviewRounds > 0) {
4861
+ maxReviewRounds = response.maxReviewRounds;
4862
+ }
4863
+ completionStrategy = response.completionStrategy || completionStrategy;
4864
+ }
4865
+ if (completionStrategy && !isLocal) {
4866
+ throw createCliError(
4867
+ "INVALID_ARGUMENT",
4868
+ "`--completion-strategy` can only be used with a local workflow."
4869
+ );
4870
+ }
4871
+ if (typeof taskAgentEnabled === "boolean") {
4872
+ ensureTaskExecution(workflow).enabled = taskAgentEnabled;
4873
+ }
4874
+ if (reviews) {
4875
+ const agentReview = ensureAgentReview(workflow);
4876
+ for (const phase of CONFIG_REVIEW_PHASES) {
4877
+ ensureReviewPhase(agentReview, phase).enabled = reviews.includes(phase);
4878
+ }
4879
+ }
4880
+ if (typeof maxReviewRounds === "number") {
4881
+ ensureAgentReview(workflow).maxRounds = maxReviewRounds;
4882
+ }
4883
+ if (completionStrategy) workflow.completionStrategy = completionStrategy;
4884
+ if (typeof taskAgentEnabled === "boolean" || reviews) {
4885
+ workflow.agentAutomationConfigured = true;
4886
+ }
4887
+ console.log(chalk.green(tr(lang, "cli", "config.workflowUpdated")));
4888
+ }
4564
4889
  function runProcess(bin, args, cwd) {
4565
4890
  const result = spawnSync(bin, args, {
4566
4891
  cwd,
@@ -8441,6 +8766,7 @@ var COMPLETION_MARKERS = {
8441
8766
  };
8442
8767
  function resolveWorkflowRequirements(config) {
8443
8768
  const workflow = config.workflow || {};
8769
+ const legacyBackfilledAgentAutomation = resolveLegacyBackfilledAgentAutomation(config);
8444
8770
  const hasCanonicalMode = workflow.mode === "github" || workflow.mode === "local";
8445
8771
  const workflowMode = hasCanonicalMode ? workflow.mode : workflow.preset === "local" ? "local" : "github";
8446
8772
  const isLocalWorkflow = workflowMode === "local";
@@ -8452,10 +8778,13 @@ function resolveWorkflowRequirements(config) {
8452
8778
  requirePr: workflow.requirePr ?? !isLocalWorkflow,
8453
8779
  requireReview: workflow.requireReview ?? !isLocalWorkflow,
8454
8780
  requireMerge: workflow.requireMerge ?? !isLocalWorkflow,
8455
- planReviewEnabled: workflow.agentReview?.plan?.enabled ?? true,
8781
+ // Missing agent automation keys identify projects created before these
8782
+ // policies existed. Keep their historical main-agent/no-Plan-review
8783
+ // behavior; new projects opt in explicitly through init.
8784
+ planReviewEnabled: legacyBackfilledAgentAutomation.planReview ? false : workflow.agentReview?.plan?.enabled ?? false,
8456
8785
  taskReviewEnabled: workflow.agentReview?.task?.enabled ?? false,
8457
8786
  featureReviewEnabled: workflow.agentReview?.feature?.enabled ?? workflow.prePrReview?.enabled ?? !isLocalWorkflow,
8458
- taskExecutionEnabled: workflow.agentExecution?.task?.enabled ?? true
8787
+ taskExecutionEnabled: legacyBackfilledAgentAutomation.taskExecution ? false : workflow.agentExecution?.task?.enabled ?? false
8459
8788
  };
8460
8789
  }
8461
8790
  function parseApprovalStatus(raw) {
@@ -8518,6 +8847,9 @@ function resolveMaxReviewRounds(config) {
8518
8847
  const configured = config.workflow?.agentReview?.maxRounds;
8519
8848
  return typeof configured === "number" && Number.isInteger(configured) && configured > 0 ? configured : 1;
8520
8849
  }
8850
+ function reviewRemediationLimitReached(config, reviewRound, decisionOutcome) {
8851
+ return decisionOutcome === "changes_requested" && (reviewRound || 1) > resolveMaxReviewRounds(config);
8852
+ }
8521
8853
  function parseWorkflowTaskLine(line, index = -1) {
8522
8854
  const canonical = parseTaskLine(line, index);
8523
8855
  if (canonical) {
@@ -9241,16 +9573,32 @@ function reviewEvidenceSatisfied(config, feature, scope, evidence) {
9241
9573
  });
9242
9574
  }
9243
9575
  function planReviewSatisfied(config, feature, review, target) {
9244
- return review.status === "done" && reviewEvidenceSatisfied(config, feature, "plan", review.evidence) && review.decisionOutcome === "approve" && review.reviewedSpecHash === target.specHash && review.reviewedPlanHash === target.planHash;
9576
+ return review.status === "done" && reviewEvidenceSatisfied(config, feature, "plan", review.evidence) && (review.decisionOutcome === "approve" || reviewRemediationLimitReached(
9577
+ config,
9578
+ review.reviewRound,
9579
+ review.decisionOutcome
9580
+ )) && review.reviewedSpecHash === target.specHash && review.reviewedPlanHash === target.planHash;
9245
9581
  }
9246
9582
  function featureReviewSatisfied(config, feature, tasks, target) {
9247
- return !!target && tasks.prePrReviewStatus === "done" && reviewEvidenceSatisfied(config, feature, "feature", tasks.prePrEvidence) && !!tasks.prePrDecision && tasks.prePrDecisionOutcome === "approve" && tasks.prePrReviewedHead === target.targetSha && tasks.prePrReviewedTree === target.targetTree;
9583
+ return !!target && tasks.prePrReviewStatus === "done" && reviewEvidenceSatisfied(config, feature, "feature", tasks.prePrEvidence) && !!tasks.prePrDecision && (tasks.prePrDecisionOutcome === "approve" || reviewRemediationLimitReached(
9584
+ config,
9585
+ tasks.prePrReviewRound,
9586
+ tasks.prePrDecisionOutcome
9587
+ )) && tasks.prePrReviewedHead === target.targetSha && tasks.prePrReviewedTree === target.targetTree;
9248
9588
  }
9249
9589
  function taskReviewEvidenceSatisfied(config, feature, task, target) {
9250
- return !!target && reviewEvidenceSatisfied(config, feature, "task", task.reviewEvidence) && task.reviewDecisionOutcome === "approve" && task.reviewedHead === target.targetSha && task.reviewedTree === target.targetTree;
9590
+ return !!target && reviewEvidenceSatisfied(config, feature, "task", task.reviewEvidence) && (task.reviewDecisionOutcome === "approve" || reviewRemediationLimitReached(
9591
+ config,
9592
+ task.reviewRound,
9593
+ task.reviewDecisionOutcome
9594
+ )) && task.reviewedHead === target.targetSha && task.reviewedTree === target.targetTree;
9251
9595
  }
9252
9596
  function recordedTaskReviewSatisfied(config, feature, projectGitCwd, task, currentTargetSha) {
9253
- if (!currentTargetSha || !task.reviewedHead || !task.reviewedTree || task.reviewDecisionOutcome !== "approve" || !reviewEvidenceSatisfied(config, feature, "task", task.reviewEvidence)) {
9597
+ if (!currentTargetSha || !task.reviewedHead || !task.reviewedTree || task.reviewDecisionOutcome !== "approve" && !reviewRemediationLimitReached(
9598
+ config,
9599
+ task.reviewRound,
9600
+ task.reviewDecisionOutcome
9601
+ ) || !reviewEvidenceSatisfied(config, feature, "task", task.reviewEvidence)) {
9254
9602
  return false;
9255
9603
  }
9256
9604
  const actualTree = runGitCapture(
@@ -9671,36 +10019,6 @@ function resolvePlanReviewPayload(config, feature, review, target) {
9671
10019
  docsDirectory: config.docsDir
9672
10020
  };
9673
10021
  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
10022
  if (targetMatches) {
9705
10023
  return {
9706
10024
  status: "ok",
@@ -9801,6 +10119,11 @@ async function collectWorkflowStage(cwd, selector, component) {
9801
10119
  specContent || "",
9802
10120
  planContent || ""
9803
10121
  );
10122
+ const planReviewAutoCompleted = planReview.status === "done" && reviewEvidenceSatisfied(config, feature, "plan", planReview.evidence) && reviewRemediationLimitReached(
10123
+ config,
10124
+ planReview.reviewRound,
10125
+ planReview.decisionOutcome
10126
+ ) && planReview.reviewedSpecHash === planReviewTarget.specHash && planReview.reviewedPlanHash === planReviewTarget.planHash;
9804
10127
  const enforcePlanReview = requirements.planReviewEnabled && (tasks.docStatus !== "approved" || planReview.hasMetadata);
9805
10128
  const issueDraft = parseWorkflowDraftMetadataExtended(issueContent || "");
9806
10129
  const prDraft = parseWorkflowDraftMetadataExtended(prContent || "");
@@ -9845,7 +10168,7 @@ async function collectWorkflowStage(cwd, selector, component) {
9845
10168
  planReviewTarget
9846
10169
  );
9847
10170
  }
9848
- const approvalRequired = isReviewStage ? resolveActionApprovalRequired(config, "plan_approve", true) : false;
10171
+ const approvalRequired = isReviewStage && !planReviewAutoCompleted ? resolveActionApprovalRequired(config, "plan_approve", true) : false;
9849
10172
  const actionOptions = approvalRequired ? buildApprovalActionOptions({
9850
10173
  approveSummary: "Approve plan.md and continue to the tasks stage.",
9851
10174
  holdSummary: "Request plan changes before continuing."
@@ -9858,7 +10181,7 @@ async function collectWorkflowStage(cwd, selector, component) {
9858
10181
  stage: "plan",
9859
10182
  nextAction: buildAction(
9860
10183
  isReviewStage ? "plan_approve" : "plan_write",
9861
- 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.",
10184
+ 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.",
9862
10185
  approvalRequired
9863
10186
  ),
9864
10187
  approvalRequired,
@@ -10045,32 +10368,7 @@ async function collectWorkflowStage(cwd, selector, component) {
10045
10368
  };
10046
10369
  const evidenceMatchesTarget = reviewTask.reviewedHead === reviewTarget.targetSha && reviewTask.reviewedTree === reviewTarget.targetTree;
10047
10370
  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) {
10371
+ if (evidenceMatchesTarget && reviewContext.reviewRound <= reviewContext.maxReviewRounds) {
10074
10372
  return {
10075
10373
  status: "ok",
10076
10374
  reasonCode: "WORKFLOW_STAGE_RESOLVED",
@@ -10112,7 +10410,7 @@ async function collectWorkflowStage(cwd, selector, component) {
10112
10410
  };
10113
10411
  }
10114
10412
  if (taskReviewEvidenceSatisfied(config, feature, reviewTask, reviewTarget)) {
10115
- 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.`;
10413
+ 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.`;
10116
10414
  return {
10117
10415
  status: "ok",
10118
10416
  reasonCode: "WORKFLOW_STAGE_RESOLVED",
@@ -10255,32 +10553,7 @@ async function collectWorkflowStage(cwd, selector, component) {
10255
10553
  } : null;
10256
10554
  const evidenceMatchesTarget = !!reviewTarget && tasks.prePrReviewedHead === reviewTarget.targetSha && tasks.prePrReviewedTree === reviewTarget.targetTree;
10257
10555
  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) {
10556
+ if (evidenceMatchesTarget && reviewContext.reviewRound <= reviewContext.maxReviewRounds) {
10284
10557
  return {
10285
10558
  status: "ok",
10286
10559
  reasonCode: "WORKFLOW_STAGE_RESOLVED",