lee-spec-kit 0.9.3 → 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
@@ -199,6 +199,28 @@ var koCli = {
199
199
  "init.choice.docsRepo.embedded.desc": "\uD504\uB85C\uC81D\uD2B8\uC640 \uD568\uAED8 push\uB429\uB2C8\uB2E4",
200
200
  "init.choice.docsRepo.standalone.title": "standalone - \uBCC4\uB3C4 \uB3C5\uB9BD \uB808\uD3EC",
201
201
  "init.choice.docsRepo.standalone.desc": "push \uC5EC\uBD80\uB97C \uBCC4\uB3C4\uB85C \uC124\uC815\uD569\uB2C8\uB2E4",
202
+ "init.prompt.workflowMode": "\uC6CC\uD06C\uD50C\uB85C\uC6B0 \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:",
203
+ "init.choice.workflow.github.title": "GitHub - Issue\uC640 PR \uAE30\uBC18 \uC6CC\uD06C\uD50C\uB85C\uC6B0",
204
+ "init.choice.workflow.github.desc": "\uC6D0\uACA9 Issue, \uBE0C\uB79C\uCE58, PR, \uB9AC\uBDF0, \uBCD1\uD569 \uB2E8\uACC4\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4",
205
+ "init.choice.workflow.local.title": "Local - \uB85C\uCEEC \uD1B5\uD569 \uC6CC\uD06C\uD50C\uB85C\uC6B0",
206
+ "init.choice.workflow.local.desc": "GitHub \uC5C6\uC774 \uAD00\uB9AC\uD615 worktree\uC640 \uB85C\uCEEC \uAC80\uC99D\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4",
207
+ "init.prompt.workflowSetup": "\uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC790\uB3D9\uD654 \uBC29\uC2DD\uC744 \uC124\uC815\uD558\uC138\uC694:",
208
+ "init.choice.workflowSetup.recommended.title": "\uAD8C\uC7A5 \uC124\uC815 - \uADE0\uD615 \uC7A1\uD78C \uAE30\uBCF8\uAC12 \uC0AC\uC6A9",
209
+ "init.choice.workflowSetup.recommended.desc": "Task \uAD6C\uD604\uC744 \uC704\uC784\uD558\uACE0 Plan\uACFC Feature\uB97C \uAC80\uC218\uD569\uB2C8\uB2E4",
210
+ "init.choice.workflowSetup.custom.title": "\uC9C1\uC811 \uC124\uC815 - \uC635\uC158 \uC120\uD0DD",
211
+ "init.choice.workflowSetup.custom.desc": "Task \uC704\uC784, \uAC80\uC218 \uB2E8\uACC4, \uB85C\uCEEC \uD1B5\uD569 \uBC29\uC2DD\uC744 \uC120\uD0DD\uD569\uB2C8\uB2E4",
212
+ "init.prompt.taskAgent": "Task \uAD6C\uD604\uC744 \uC11C\uBE0C\uC5D0\uC774\uC804\uD2B8\uC5D0\uAC8C \uC704\uC784\uD560\uAE4C\uC694?",
213
+ "init.choice.taskAgent.on": "\uC0AC\uC6A9 - \uAC01 Task \uAD6C\uD604\uC744 \uC704\uC784",
214
+ "init.choice.taskAgent.off": "\uC0AC\uC6A9 \uC548 \uD568 - \uBA54\uC778 \uC5D0\uC774\uC804\uD2B8\uAC00 \uC9C1\uC811 \uAD6C\uD604",
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:",
217
+ "init.choice.review.plan": "Plan",
218
+ "init.choice.review.task": "Task",
219
+ "init.choice.review.feature": "Feature",
220
+ "init.prompt.completionStrategy": "\uB85C\uCEEC \uD1B5\uD569 \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:",
221
+ "init.choice.completionStrategy.localFf": "local-ff - \uAC80\uC99D\uB41C Feature \uBE0C\uB79C\uCE58\uB97C fast-forward \uD1B5\uD569",
222
+ "init.choice.completionStrategy.localSquash": "local-squash - \uAC80\uC99D\uB41C \uD558\uB098\uC758 \uCEE4\uBC0B\uC73C\uB85C \uD1B5\uD569",
223
+ "init.choice.completionStrategy.none": "none - Feature \uBE0C\uB79C\uCE58\uB97C \uD1B5\uD569\uD558\uC9C0 \uC54A\uACE0 \uC885\uB8CC",
202
224
  "init.prompt.componentRepoPath": "{component} \uCEF4\uD3EC\uB10C\uD2B8 \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
203
225
  "init.prompt.projectRepoPath": "\uD504\uB85C\uC81D\uD2B8 \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
204
226
  "init.validation.enterPath": "\uACBD\uB85C\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694",
@@ -208,11 +230,20 @@ var koCli = {
208
230
  "init.prompt.remoteUrl": "\uC6D0\uACA9 \uB808\uD3EC URL\uC744 \uC785\uB825\uD558\uC138\uC694:",
209
231
  "init.validation.enterUrl": "URL\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694",
210
232
  "init.prompt.overwrite": "{dir} \uD3F4\uB354\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. \uB36E\uC5B4\uC4F0\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?",
233
+ "init.log.configSummaryTitle": "\u2699\uFE0F \uC124\uC815 \uC694\uC57D",
211
234
  "init.log.creatingDocs": "\u{1F4C1} docs \uAD6C\uC870 \uC0DD\uC131 \uC911...",
212
235
  "init.log.projectLabel": "\uD504\uB85C\uC81D\uD2B8",
213
236
  "init.log.typeLabel": "\uD0C0\uC785",
214
237
  "init.log.langLabel": "\uC5B8\uC5B4",
215
238
  "init.log.pathLabel": "\uACBD\uB85C",
239
+ "init.log.workflowLabel": "\uC6CC\uD06C\uD50C\uB85C\uC6B0",
240
+ "init.log.taskAgentLabel": "Task \uAD6C\uD604",
241
+ "init.log.reviewsLabel": "\uAC80\uC218",
242
+ "init.log.maxReviewRoundsLabel": "\uB9AC\uBDF0 \uC9C0\uC801 \uC790\uB3D9 \uBC18\uC601 \uD69F\uC218",
243
+ "init.log.completionStrategyLabel": "\uB85C\uCEEC \uD1B5\uD569",
244
+ "init.summary.taskAgent.on": "\uC11C\uBE0C\uC5D0\uC774\uC804\uD2B8",
245
+ "init.summary.taskAgent.off": "\uBA54\uC778 \uC5D0\uC774\uC804\uD2B8",
246
+ "init.summary.reviews.none": "\uC5C6\uC74C",
216
247
  "init.log.docsCreated": "\u2705 docs \uAD6C\uC870 \uC0DD\uC131 \uC644\uB8CC!",
217
248
  "init.log.nextStepsTitle": "\uB2E4\uC74C \uB2E8\uACC4:",
218
249
  "init.log.nextSteps1": " 1. {docsDir}/prd/README.md \uC791\uC131",
@@ -482,6 +513,28 @@ var enCli = {
482
513
  "init.choice.docsRepo.embedded.desc": "Pushed together with the project",
483
514
  "init.choice.docsRepo.standalone.title": "standalone - separate docs repo",
484
515
  "init.choice.docsRepo.standalone.desc": "Configure push settings separately",
516
+ "init.prompt.workflowMode": "Select workflow mode:",
517
+ "init.choice.workflow.github.title": "GitHub - Issue and pull request workflow",
518
+ "init.choice.workflow.github.desc": "Use remote Issue, branch, pull request, review, and merge gates",
519
+ "init.choice.workflow.local.title": "Local - local integration workflow",
520
+ "init.choice.workflow.local.desc": "Use managed worktrees and local verification without requiring GitHub",
521
+ "init.prompt.workflowSetup": "Configure workflow automation:",
522
+ "init.choice.workflowSetup.recommended.title": "Recommended - use balanced defaults",
523
+ "init.choice.workflowSetup.recommended.desc": "Delegate task implementation and review Plan plus Feature",
524
+ "init.choice.workflowSetup.custom.title": "Custom - choose workflow options",
525
+ "init.choice.workflowSetup.custom.desc": "Select task delegation, review gates, and local integration",
526
+ "init.prompt.taskAgent": "Delegate task implementation to a subagent?",
527
+ "init.choice.taskAgent.on": "On - delegate each task implementation",
528
+ "init.choice.taskAgent.off": "Off - implement tasks in the main agent",
529
+ "init.prompt.reviews": "Select independent review gates:",
530
+ "init.prompt.maxReviewRounds": "Maximum automatic review finding remediation rounds before asking the user:",
531
+ "init.choice.review.plan": "Plan",
532
+ "init.choice.review.task": "Task",
533
+ "init.choice.review.feature": "Feature",
534
+ "init.prompt.completionStrategy": "Select local integration strategy:",
535
+ "init.choice.completionStrategy.localFf": "local-ff - fast-forward the verified Feature branch",
536
+ "init.choice.completionStrategy.localSquash": "local-squash - integrate as one verified commit",
537
+ "init.choice.completionStrategy.none": "none - finish without integrating the Feature branch",
485
538
  "init.prompt.componentRepoPath": 'Enter repository path for component "{component}":',
486
539
  "init.prompt.projectRepoPath": "Enter project repository path:",
487
540
  "init.validation.enterPath": "Please enter a path",
@@ -491,11 +544,20 @@ var enCli = {
491
544
  "init.prompt.remoteUrl": "Enter remote repository URL:",
492
545
  "init.validation.enterUrl": "Please enter a URL",
493
546
  "init.prompt.overwrite": "{dir} already exists. Overwrite?",
547
+ "init.log.configSummaryTitle": "\u2699\uFE0F Configuration summary",
494
548
  "init.log.creatingDocs": "\u{1F4C1} Creating docs structure...",
495
549
  "init.log.projectLabel": "Project",
496
550
  "init.log.typeLabel": "Type",
497
551
  "init.log.langLabel": "Lang",
498
552
  "init.log.pathLabel": "Path",
553
+ "init.log.workflowLabel": "Workflow",
554
+ "init.log.taskAgentLabel": "Task implementation",
555
+ "init.log.reviewsLabel": "Reviews",
556
+ "init.log.maxReviewRoundsLabel": "Review remediation rounds",
557
+ "init.log.completionStrategyLabel": "Local integration",
558
+ "init.summary.taskAgent.on": "subagent",
559
+ "init.summary.taskAgent.off": "main agent",
560
+ "init.summary.reviews.none": "none",
499
561
  "init.log.docsCreated": "\u2705 Docs structure created!",
500
562
  "init.log.nextStepsTitle": "Next steps:",
501
563
  "init.log.nextSteps1": " 1. Write {docsDir}/prd/README.md",
@@ -1295,7 +1357,7 @@ Before taking the next workflow step:
1295
1357
  3. When relevant, also read \`issue.md\` and \`pr.md\`
1296
1358
  4. Run \`npx lee-spec-kit workflow-stage <feature-ref> --json\` and follow only the returned \`nextAction\`
1297
1359
  5. If \`workflow-stage --json\` returns \`primaryActionLabel\` and \`actionOptions\`, treat \`primaryActionLabel\` as the default option label and present the exact \`actionOptions[*].reply\` tokens to the user before continuing
1298
- 6. Do not start implementation unless \`stage === "implementation"\` and \`implementationAllowed === true\`
1360
+ 6. Do not modify implementation code unless \`implementationAllowed === true\`; normal task work uses \`stage === "implementation"\`, while \`task_review_fix\`, \`feature_review_fix\`, and \`feature_remediation\` are the only review/remediation exceptions
1299
1361
  7. Treat stages before implementation as hard gates:
1300
1362
  - spec approval plus plan / tasks readiness
1301
1363
  - issue preparation / issue creation
@@ -1306,10 +1368,15 @@ Before taking the next workflow step:
1306
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
1307
1369
  11. Keep docs and code synchronized; if code changes materially, update the active feature docs in the same turn before stopping
1308
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
1309
- 13. When \`workflow-stage --json\` returns \`nextAction.category === "pre_pr_review"\` and \`nextAction.executor === "subagent"\`, delegate a fresh read-only review using the returned \`model\`, \`reasoningEffort\`, and \`onUnavailable\` policy; do not select or require a named review skill
1310
- 14. The Pre-PR review subagent returns findings without modifying code; the main agent remediates findings and records the actual reviewer metadata, reviewed diff scope, evidence, and final decision
1311
- 15. 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\`; \`feature_remediation\` explicitly permits Feature worktree fixes
1312
- 16. 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
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
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
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
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
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
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
1313
1380
 
1314
1381
  Approval and remote actions:
1315
1382
 
@@ -1898,7 +1965,7 @@ var DEFAULT_APPROVAL_REQUIRE_CHECK_CATEGORIES = [
1898
1965
  "implementation_approve",
1899
1966
  "local_merge"
1900
1967
  ];
1901
- var PRE_PR_REVIEW_REASONING_EFFORTS = [
1968
+ var AGENT_REVIEW_REASONING_EFFORTS = [
1902
1969
  "low",
1903
1970
  "medium",
1904
1971
  "high",
@@ -1913,7 +1980,7 @@ function createDefaultApprovalConfig() {
1913
1980
  requireCheckCategories: [...DEFAULT_APPROVAL_REQUIRE_CHECK_CATEGORIES]
1914
1981
  };
1915
1982
  }
1916
- function createDefaultPrePrReviewerConfig() {
1983
+ function createDefaultAgentReviewerConfig() {
1917
1984
  return {
1918
1985
  type: "subagent",
1919
1986
  model: "inherit",
@@ -1921,6 +1988,15 @@ function createDefaultPrePrReviewerConfig() {
1921
1988
  onUnavailable: "inherit"
1922
1989
  };
1923
1990
  }
1991
+ function createDefaultAgentExecutionTaskConfig() {
1992
+ return {
1993
+ enabled: true,
1994
+ type: "subagent",
1995
+ model: "inherit",
1996
+ reasoningEffort: "high",
1997
+ onUnavailable: "inherit"
1998
+ };
1999
+ }
1924
2000
 
1925
2001
  // src/commands/init.ts
1926
2002
  function checkGitRepo(cwd) {
@@ -2005,6 +2081,44 @@ function hasStandaloneProjectRepoOutsideWorkspaceGitRoot(workspaceGitRoot, proje
2005
2081
  return !!projectGitRoot && path8.resolve(projectGitRoot) !== path8.resolve(workspaceGitRoot);
2006
2082
  });
2007
2083
  }
2084
+ var INIT_REVIEW_PHASES = ["plan", "task", "feature"];
2085
+ function parseInitReviews(value) {
2086
+ if (typeof value === "undefined") return void 0;
2087
+ const normalized = value.split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean);
2088
+ if (normalized.length === 1 && normalized[0] === "none") return [];
2089
+ if (normalized.length === 0 || normalized.includes("none") || normalized.some(
2090
+ (entry) => !INIT_REVIEW_PHASES.includes(entry)
2091
+ )) {
2092
+ throw createCliError(
2093
+ "INVALID_ARGUMENT",
2094
+ "`--reviews` must be a comma-separated subset of `plan,task,feature`, or `none`."
2095
+ );
2096
+ }
2097
+ return INIT_REVIEW_PHASES.filter((phase) => normalized.includes(phase));
2098
+ }
2099
+ function assertValidInitWorkflowOptions(options) {
2100
+ if (options.taskAgent && !["on", "off"].includes(options.taskAgent)) {
2101
+ throw createCliError(
2102
+ "INVALID_ARGUMENT",
2103
+ "`--task-agent` must be `on` or `off`."
2104
+ );
2105
+ }
2106
+ if (options.completionStrategy && !["local-ff", "local-squash", "none"].includes(options.completionStrategy)) {
2107
+ throw createCliError(
2108
+ "INVALID_ARGUMENT",
2109
+ "`--completion-strategy` must be `local-ff`, `local-squash`, or `none`."
2110
+ );
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
+ }
2121
+ }
2008
2122
  function initCommand(program2) {
2009
2123
  program2.command("init").description("Initialize project documentation structure").option("-n, --name <name>", "Project name (default: current folder name)").option(
2010
2124
  "-t, --type <type>",
@@ -2012,7 +2126,16 @@ function initCommand(program2) {
2012
2126
  ).option(
2013
2127
  "--components <list>",
2014
2128
  "Component list for multi (comma-separated, e.g. app,api,worker)"
2015
- ).option("-l, --lang <lang>", "Language: ko | en (default: en)").option("--workflow <mode>", "Workflow mode: github | local").option("-d, --dir <dir>", "Target directory (default: ./docs)", "./docs").option("--docs-repo <mode>", "Docs repository mode: embedded | standalone").option(
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(
2130
+ "--reviews <list>",
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)"
2135
+ ).option(
2136
+ "--completion-strategy <strategy>",
2137
+ "Local completion: local-ff | local-squash | none"
2138
+ ).option("-d, --dir <dir>", "Target directory (default: ./docs)", "./docs").option("--docs-repo <mode>", "Docs repository mode: embedded | standalone").option(
2016
2139
  "--project-root <path>",
2017
2140
  "Project root path (standalone single) or JSON map for standalone multi"
2018
2141
  ).option(
@@ -2042,6 +2165,7 @@ ${tr(lang2, "cli", "common.canceled")}`));
2042
2165
  });
2043
2166
  }
2044
2167
  async function runInit(options) {
2168
+ assertValidInitWorkflowOptions(options);
2045
2169
  const cwd = process.cwd();
2046
2170
  const defaultName = path8.basename(cwd);
2047
2171
  let projectName = options.name || defaultName;
@@ -2049,6 +2173,10 @@ async function runInit(options) {
2049
2173
  let components = parseComponentsOption(options.components);
2050
2174
  let lang = options.lang || "en";
2051
2175
  let workflowMode = options.workflow || "github";
2176
+ let taskAgentEnabled = options.taskAgent !== "off";
2177
+ let enabledReviews = parseInitReviews(options.reviews) ?? ["plan", "feature"];
2178
+ let maxReviewRounds = Number(options.maxReviewRounds || 1);
2179
+ let completionStrategy = options.completionStrategy || "local-ff";
2052
2180
  let docsRepo = options.docsRepo || "embedded";
2053
2181
  let pushDocs = typeof options.pushDocs === "boolean" ? options.pushDocs : void 0;
2054
2182
  let docsRemote = options.docsRemote;
@@ -2168,6 +2296,24 @@ async function runInit(options) {
2168
2296
  }
2169
2297
  ],
2170
2298
  initial: 0
2299
+ },
2300
+ {
2301
+ type: options.workflow ? null : "select",
2302
+ name: "workflowMode",
2303
+ message: tr(lang, "cli", "init.prompt.workflowMode"),
2304
+ choices: [
2305
+ {
2306
+ title: tr(lang, "cli", "init.choice.workflow.github.title"),
2307
+ value: "github",
2308
+ description: tr(lang, "cli", "init.choice.workflow.github.desc")
2309
+ },
2310
+ {
2311
+ title: tr(lang, "cli", "init.choice.workflow.local.title"),
2312
+ value: "local",
2313
+ description: tr(lang, "cli", "init.choice.workflow.local.desc")
2314
+ }
2315
+ ],
2316
+ initial: 0
2171
2317
  }
2172
2318
  ],
2173
2319
  {
@@ -2179,6 +2325,7 @@ async function runInit(options) {
2179
2325
  projectName = response.projectName || projectName;
2180
2326
  projectType = response.projectType || projectType;
2181
2327
  docsRepo = response.docsRepo || "embedded";
2328
+ workflowMode = response.workflowMode || workflowMode;
2182
2329
  if (docsRepo === "standalone") {
2183
2330
  const resolvedType = normalizeProjectType(
2184
2331
  String(projectType || response.projectType || "multi")
@@ -2290,6 +2437,138 @@ async function runInit(options) {
2290
2437
  docsRemote = remoteResponse.docsRemote || docsRemote;
2291
2438
  }
2292
2439
  }
2440
+ const hasExplicitWorkflowSetup = typeof options.taskAgent !== "undefined" || typeof options.reviews !== "undefined" || typeof options.maxReviewRounds !== "undefined" || typeof options.completionStrategy !== "undefined";
2441
+ let customizeWorkflow = hasExplicitWorkflowSetup;
2442
+ if (!hasExplicitWorkflowSetup) {
2443
+ const setupResponse = await prompts(
2444
+ [
2445
+ {
2446
+ type: "select",
2447
+ name: "workflowSetup",
2448
+ message: tr(lang, "cli", "init.prompt.workflowSetup"),
2449
+ choices: [
2450
+ {
2451
+ title: tr(
2452
+ lang,
2453
+ "cli",
2454
+ "init.choice.workflowSetup.recommended.title"
2455
+ ),
2456
+ value: "recommended",
2457
+ description: tr(
2458
+ lang,
2459
+ "cli",
2460
+ "init.choice.workflowSetup.recommended.desc"
2461
+ )
2462
+ },
2463
+ {
2464
+ title: tr(
2465
+ lang,
2466
+ "cli",
2467
+ "init.choice.workflowSetup.custom.title"
2468
+ ),
2469
+ value: "custom",
2470
+ description: tr(
2471
+ lang,
2472
+ "cli",
2473
+ "init.choice.workflowSetup.custom.desc"
2474
+ )
2475
+ }
2476
+ ],
2477
+ initial: 0
2478
+ }
2479
+ ],
2480
+ {
2481
+ onCancel: () => {
2482
+ throw new Error("canceled");
2483
+ }
2484
+ }
2485
+ );
2486
+ customizeWorkflow = setupResponse.workflowSetup === "custom";
2487
+ }
2488
+ if (customizeWorkflow) {
2489
+ const workflowResponse = await prompts(
2490
+ [
2491
+ {
2492
+ type: typeof options.taskAgent === "undefined" ? "select" : null,
2493
+ name: "taskAgent",
2494
+ message: tr(lang, "cli", "init.prompt.taskAgent"),
2495
+ choices: [
2496
+ {
2497
+ title: tr(lang, "cli", "init.choice.taskAgent.on"),
2498
+ value: true
2499
+ },
2500
+ {
2501
+ title: tr(lang, "cli", "init.choice.taskAgent.off"),
2502
+ value: false
2503
+ }
2504
+ ],
2505
+ initial: 0
2506
+ },
2507
+ {
2508
+ type: typeof options.reviews === "undefined" ? "multiselect" : null,
2509
+ name: "reviews",
2510
+ message: tr(lang, "cli", "init.prompt.reviews"),
2511
+ choices: INIT_REVIEW_PHASES.map((phase) => ({
2512
+ title: tr(lang, "cli", `init.choice.review.${phase}`),
2513
+ value: phase,
2514
+ selected: phase !== "task"
2515
+ }))
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
+ },
2524
+ {
2525
+ type: workflowMode === "local" && typeof options.completionStrategy === "undefined" ? "select" : null,
2526
+ name: "completionStrategy",
2527
+ message: tr(lang, "cli", "init.prompt.completionStrategy"),
2528
+ choices: [
2529
+ {
2530
+ title: tr(
2531
+ lang,
2532
+ "cli",
2533
+ "init.choice.completionStrategy.localFf"
2534
+ ),
2535
+ value: "local-ff"
2536
+ },
2537
+ {
2538
+ title: tr(
2539
+ lang,
2540
+ "cli",
2541
+ "init.choice.completionStrategy.localSquash"
2542
+ ),
2543
+ value: "local-squash"
2544
+ },
2545
+ {
2546
+ title: tr(lang, "cli", "init.choice.completionStrategy.none"),
2547
+ value: "none"
2548
+ }
2549
+ ],
2550
+ initial: 0
2551
+ }
2552
+ ],
2553
+ {
2554
+ onCancel: () => {
2555
+ throw new Error("canceled");
2556
+ }
2557
+ }
2558
+ );
2559
+ if (typeof workflowResponse.taskAgent === "boolean") {
2560
+ taskAgentEnabled = workflowResponse.taskAgent;
2561
+ }
2562
+ if (Array.isArray(workflowResponse.reviews)) {
2563
+ enabledReviews = INIT_REVIEW_PHASES.filter(
2564
+ (phase) => workflowResponse.reviews.includes(phase)
2565
+ );
2566
+ }
2567
+ if (Number.isInteger(workflowResponse.maxReviewRounds) && workflowResponse.maxReviewRounds > 0) {
2568
+ maxReviewRounds = workflowResponse.maxReviewRounds;
2569
+ }
2570
+ completionStrategy = workflowResponse.completionStrategy || completionStrategy;
2571
+ }
2293
2572
  }
2294
2573
  if (!projectType) {
2295
2574
  projectType = "multi";
@@ -2315,6 +2594,12 @@ async function runInit(options) {
2315
2594
  tr(lang, "cli", "validation.context.workflowMode"),
2316
2595
  lang
2317
2596
  );
2597
+ if (workflowMode !== "local" && options.completionStrategy) {
2598
+ throw createCliError(
2599
+ "INVALID_ARGUMENT",
2600
+ "`--completion-strategy` can only be used with `--workflow local`."
2601
+ );
2602
+ }
2318
2603
  if (projectType === "single") {
2319
2604
  if (components.length > 0) {
2320
2605
  throw createCliError(
@@ -2478,7 +2763,7 @@ async function runInit(options) {
2478
2763
  }
2479
2764
  }
2480
2765
  console.log();
2481
- console.log(chalk.blue(tr(lang, "cli", "init.log.creatingDocs")));
2766
+ console.log(chalk.blue(tr(lang, "cli", "init.log.configSummaryTitle")));
2482
2767
  console.log(
2483
2768
  chalk.gray(
2484
2769
  ` ${tr(lang, "cli", "init.log.projectLabel")}: ${projectName}`
@@ -2493,6 +2778,40 @@ async function runInit(options) {
2493
2778
  console.log(
2494
2779
  chalk.gray(` ${tr(lang, "cli", "init.log.pathLabel")}: ${targetDir}`)
2495
2780
  );
2781
+ console.log(
2782
+ chalk.gray(
2783
+ ` ${tr(lang, "cli", "init.log.workflowLabel")}: ${workflowMode}`
2784
+ )
2785
+ );
2786
+ console.log(
2787
+ chalk.gray(
2788
+ ` ${tr(lang, "cli", "init.log.taskAgentLabel")}: ${tr(
2789
+ lang,
2790
+ "cli",
2791
+ taskAgentEnabled ? "init.summary.taskAgent.on" : "init.summary.taskAgent.off"
2792
+ )}`
2793
+ )
2794
+ );
2795
+ const reviewSummary = enabledReviews.length > 0 ? enabledReviews.map((phase) => tr(lang, "cli", `init.choice.review.${phase}`)).join(", ") : tr(lang, "cli", "init.summary.reviews.none");
2796
+ console.log(
2797
+ chalk.gray(
2798
+ ` ${tr(lang, "cli", "init.log.reviewsLabel")}: ${reviewSummary}`
2799
+ )
2800
+ );
2801
+ console.log(
2802
+ chalk.gray(
2803
+ ` ${tr(lang, "cli", "init.log.maxReviewRoundsLabel")}: ${maxReviewRounds}`
2804
+ )
2805
+ );
2806
+ if (workflowMode === "local") {
2807
+ console.log(
2808
+ chalk.gray(
2809
+ ` ${tr(lang, "cli", "init.log.completionStrategyLabel")}: ${completionStrategy}`
2810
+ )
2811
+ );
2812
+ }
2813
+ console.log();
2814
+ console.log(chalk.blue(tr(lang, "cli", "init.log.creatingDocs")));
2496
2815
  console.log();
2497
2816
  const templatesDir = getTemplatesDir();
2498
2817
  const commonPath = path8.join(templatesDir, lang, "common");
@@ -2541,14 +2860,34 @@ async function runInit(options) {
2541
2860
  taskCommitGate: "warn",
2542
2861
  ...workflowMode === "local" ? {
2543
2862
  baseBranch: resolveInitialBaseBranch(cwd, docsRepo, projectRoot),
2544
- completionStrategy: "local-ff",
2863
+ completionStrategy,
2545
2864
  deleteFeatureBranchAfterMerge: true,
2546
2865
  featureChecks: [],
2547
2866
  postMergeChecks: []
2548
2867
  } : {},
2549
- prePrReview: {
2550
- evidenceMode: "path_required",
2551
- reviewer: createDefaultPrePrReviewerConfig()
2868
+ agentExecution: {
2869
+ task: {
2870
+ ...createDefaultAgentExecutionTaskConfig(),
2871
+ enabled: taskAgentEnabled
2872
+ }
2873
+ },
2874
+ agentReview: {
2875
+ maxRounds: maxReviewRounds,
2876
+ plan: {
2877
+ enabled: enabledReviews.includes("plan"),
2878
+ evidenceMode: "path_required",
2879
+ reviewer: createDefaultAgentReviewerConfig()
2880
+ },
2881
+ task: {
2882
+ enabled: enabledReviews.includes("task"),
2883
+ evidenceMode: "path_required",
2884
+ reviewer: createDefaultAgentReviewerConfig()
2885
+ },
2886
+ feature: {
2887
+ enabled: enabledReviews.includes("feature"),
2888
+ evidenceMode: "path_required",
2889
+ reviewer: createDefaultAgentReviewerConfig()
2890
+ }
2552
2891
  }
2553
2892
  },
2554
2893
  pr: {
@@ -2765,17 +3104,12 @@ function sanitizeTasksForLocal(content, lang) {
2765
3104
  const lines = next.split("\n");
2766
3105
  const filtered = [];
2767
3106
  for (const line of lines) {
2768
- if (/^\s*-\s*\*\*(Issue|PR|PR Status|PR 상태|Pre-PR Review|PR 리뷰)\*\*\s*:/.test(
2769
- line
2770
- )) {
2771
- continue;
2772
- }
2773
- if (/^\s*-\s*\*\*(Pre-PR Evidence|PR 전 리뷰 Evidence)\*\*\s*:/.test(
3107
+ if (/^\s*-\s*\*\*(Issue|PR|PR Status|PR 상태|PR Review|PR 리뷰|PR Review Evidence|PR 리뷰 Evidence)\*\*\s*:/.test(
2774
3108
  line
2775
3109
  )) {
2776
3110
  continue;
2777
3111
  }
2778
- if (/^\s*-\s*\*\*(Pre-PR Decision|PR Review Decision|PR 리뷰 Decision|PR 리뷰 Decision)\*\*\s*:/.test(
3112
+ if (/^\s*-\s*\*\*(PR Review Decision|PR 리뷰 Decision)\*\*\s*:/.test(
2779
3113
  line
2780
3114
  )) {
2781
3115
  continue;
@@ -2789,9 +3123,17 @@ function sanitizeTasksForLocal(content, lang) {
2789
3123
  if (/^\s*-\s*Example:\s*review note link/i.test(line)) continue;
2790
3124
  if (/^\s*-\s*사전 리뷰 최종 결과/.test(line)) continue;
2791
3125
  if (/^\s*-\s*예:\s*리뷰 노트 링크/.test(line)) continue;
3126
+ if (/^\s*-\s*PR creation requires/i.test(line)) continue;
3127
+ if (/^\s*-\s*PR 생성 전/.test(line)) continue;
3128
+ if (/agents\/skills\/create-pr\.md/.test(line)) continue;
3129
+ if (/^\s*-\s*Mark .*PR review handoff/i.test(line)) continue;
3130
+ if (/^\s*-\s*PR 리뷰 handoff/.test(line)) continue;
3131
+ if (/^\s*-\s*Record why\/how review comments/i.test(line)) continue;
3132
+ if (/^\s*-\s*리뷰 지적사항을/.test(line)) continue;
2792
3133
  filtered.push(line);
2793
3134
  }
2794
3135
  next = filtered.join("\n");
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");
2795
3137
  next = next.replace(/feat\/\{issue-number\}-/g, "feat/").replace(/feat\/\{이슈번호\}-/g, "feat/").replace(/feat\/-/g, "feat/");
2796
3138
  return normalizeTrailingBlankLines(next);
2797
3139
  }
@@ -3798,52 +4140,125 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
3798
4140
  }
3799
4141
  }
3800
4142
  }
3801
- if (!isPlainObject(workflow.prePrReview)) {
3802
- workflow.prePrReview = {};
3803
- changedPaths.push("workflow.prePrReview");
4143
+ if (!isPlainObject(workflow.agentExecution)) {
4144
+ workflow.agentExecution = {};
4145
+ changedPaths.push("workflow.agentExecution");
3804
4146
  }
3805
- const prePrReview = workflow.prePrReview;
4147
+ const agentExecution = workflow.agentExecution;
4148
+ if (!isPlainObject(agentExecution.task)) {
4149
+ agentExecution.task = {};
4150
+ changedPaths.push("workflow.agentExecution.task");
4151
+ }
4152
+ const taskExecution = agentExecution.task;
4153
+ const defaultTaskExecution = createDefaultAgentExecutionTaskConfig();
3806
4154
  setIfMissing(
3807
- prePrReview,
3808
- "evidenceMode",
3809
- "path_required",
3810
- "workflow.prePrReview.evidenceMode"
3811
- );
3812
- if (prePrReview.evidenceMode !== void 0 && prePrReview.evidenceMode !== "path_required" && prePrReview.evidenceMode !== "any") {
3813
- prePrReview.evidenceMode = "path_required";
3814
- changedPaths.push("workflow.prePrReview.evidenceMode");
3815
- }
3816
- if ("findings" in prePrReview) {
3817
- delete prePrReview.findings;
3818
- changedPaths.push("workflow.prePrReview.findings");
3819
- }
3820
- const defaultReviewer = createDefaultPrePrReviewerConfig();
3821
- if (!isPlainObject(prePrReview.reviewer)) {
3822
- prePrReview.reviewer = defaultReviewer;
3823
- changedPaths.push("workflow.prePrReview.reviewer");
3824
- } else {
3825
- const reviewer = prePrReview.reviewer;
4155
+ taskExecution,
4156
+ "enabled",
4157
+ defaultTaskExecution.enabled,
4158
+ "workflow.agentExecution.task.enabled"
4159
+ );
4160
+ if (typeof taskExecution.enabled !== "boolean") {
4161
+ taskExecution.enabled = defaultTaskExecution.enabled;
4162
+ changedPaths.push("workflow.agentExecution.task.enabled");
4163
+ }
4164
+ if (taskExecution.type !== "subagent") {
4165
+ taskExecution.type = defaultTaskExecution.type;
4166
+ changedPaths.push("workflow.agentExecution.task.type");
4167
+ }
4168
+ if (typeof taskExecution.model !== "string" || !taskExecution.model.trim()) {
4169
+ taskExecution.model = defaultTaskExecution.model;
4170
+ changedPaths.push("workflow.agentExecution.task.model");
4171
+ } else if (taskExecution.model !== taskExecution.model.trim()) {
4172
+ taskExecution.model = taskExecution.model.trim();
4173
+ changedPaths.push("workflow.agentExecution.task.model");
4174
+ }
4175
+ if (typeof taskExecution.reasoningEffort !== "string" || !AGENT_REVIEW_REASONING_EFFORTS.includes(
4176
+ taskExecution.reasoningEffort
4177
+ )) {
4178
+ taskExecution.reasoningEffort = defaultTaskExecution.reasoningEffort;
4179
+ changedPaths.push("workflow.agentExecution.task.reasoningEffort");
4180
+ }
4181
+ if (taskExecution.onUnavailable !== "inherit" && taskExecution.onUnavailable !== "error") {
4182
+ taskExecution.onUnavailable = defaultTaskExecution.onUnavailable;
4183
+ changedPaths.push("workflow.agentExecution.task.onUnavailable");
4184
+ }
4185
+ const legacyPrePrReview = isPlainObject(workflow.prePrReview) ? { ...workflow.prePrReview } : null;
4186
+ if (!isPlainObject(workflow.agentReview)) {
4187
+ workflow.agentReview = {};
4188
+ changedPaths.push("workflow.agentReview");
4189
+ }
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
+ }
4196
+ const normalizeAgentReviewPhase = (key, enabledDefault, legacySeed = null) => {
4197
+ const phasePath = `workflow.agentReview.${key}`;
4198
+ if (!isPlainObject(agentReview[key])) {
4199
+ agentReview[key] = legacySeed ? { ...legacySeed } : {};
4200
+ changedPaths.push(phasePath);
4201
+ }
4202
+ const phase = agentReview[key];
4203
+ setIfMissing(phase, "enabled", enabledDefault, `${phasePath}.enabled`);
4204
+ if (typeof phase.enabled !== "boolean") {
4205
+ phase.enabled = enabledDefault;
4206
+ changedPaths.push(`${phasePath}.enabled`);
4207
+ }
4208
+ setIfMissing(
4209
+ phase,
4210
+ "evidenceMode",
4211
+ "path_required",
4212
+ `${phasePath}.evidenceMode`
4213
+ );
4214
+ if (phase.evidenceMode !== "path_required" && phase.evidenceMode !== "any") {
4215
+ phase.evidenceMode = "path_required";
4216
+ changedPaths.push(`${phasePath}.evidenceMode`);
4217
+ }
4218
+ if ("findings" in phase) {
4219
+ delete phase.findings;
4220
+ changedPaths.push(`${phasePath}.findings`);
4221
+ }
4222
+ const defaultReviewer = createDefaultAgentReviewerConfig();
4223
+ if (!isPlainObject(phase.reviewer)) {
4224
+ phase.reviewer = defaultReviewer;
4225
+ changedPaths.push(`${phasePath}.reviewer`);
4226
+ return;
4227
+ }
4228
+ const reviewer = phase.reviewer;
3826
4229
  if (reviewer.type !== "subagent") {
3827
4230
  reviewer.type = defaultReviewer.type;
3828
- changedPaths.push("workflow.prePrReview.reviewer.type");
4231
+ changedPaths.push(`${phasePath}.reviewer.type`);
3829
4232
  }
3830
4233
  if (typeof reviewer.model !== "string" || !reviewer.model.trim()) {
3831
4234
  reviewer.model = defaultReviewer.model;
3832
- changedPaths.push("workflow.prePrReview.reviewer.model");
4235
+ changedPaths.push(`${phasePath}.reviewer.model`);
3833
4236
  } else if (reviewer.model !== reviewer.model.trim()) {
3834
4237
  reviewer.model = reviewer.model.trim();
3835
- changedPaths.push("workflow.prePrReview.reviewer.model");
4238
+ changedPaths.push(`${phasePath}.reviewer.model`);
3836
4239
  }
3837
- if (typeof reviewer.reasoningEffort !== "string" || !PRE_PR_REVIEW_REASONING_EFFORTS.includes(
4240
+ if (typeof reviewer.reasoningEffort !== "string" || !AGENT_REVIEW_REASONING_EFFORTS.includes(
3838
4241
  reviewer.reasoningEffort
3839
4242
  )) {
3840
4243
  reviewer.reasoningEffort = defaultReviewer.reasoningEffort;
3841
- changedPaths.push("workflow.prePrReview.reviewer.reasoningEffort");
4244
+ changedPaths.push(`${phasePath}.reviewer.reasoningEffort`);
3842
4245
  }
3843
4246
  if (reviewer.onUnavailable !== "inherit" && reviewer.onUnavailable !== "error") {
3844
4247
  reviewer.onUnavailable = defaultReviewer.onUnavailable;
3845
- changedPaths.push("workflow.prePrReview.reviewer.onUnavailable");
4248
+ changedPaths.push(`${phasePath}.reviewer.onUnavailable`);
3846
4249
  }
4250
+ };
4251
+ normalizeAgentReviewPhase("plan", true);
4252
+ normalizeAgentReviewPhase("task", false);
4253
+ const legacyFeatureEnabled = typeof legacyPrePrReview?.enabled === "boolean" ? legacyPrePrReview.enabled : workflow.mode !== "local";
4254
+ normalizeAgentReviewPhase(
4255
+ "feature",
4256
+ legacyFeatureEnabled,
4257
+ legacyPrePrReview
4258
+ );
4259
+ if (hasOwnKey2(workflow, "prePrReview")) {
4260
+ delete workflow.prePrReview;
4261
+ changedPaths.push("workflow.prePrReview");
3847
4262
  }
3848
4263
  if (!isPlainObject(raw.pr)) {
3849
4264
  raw.pr = {};
@@ -4980,7 +5395,7 @@ function extractTaskTitles(tasksContent) {
4980
5395
  return uniqItems(
4981
5396
  tasksContent.split("\n").map((line) => {
4982
5397
  const match = line.match(
4983
- /^\s*-\s*\[(?:TODO|DOING|DONE)\][^\n]*?\s+(?:T-[A-Z0-9-]+\s+)?(.+?)\s*$/
5398
+ /^\s*-\s*\[(?:TODO|DOING|REVIEW|DONE)\][^\n]*?\s+(?:T-[A-Z0-9-]+\s+)?(.+?)\s*$/
4984
5399
  );
4985
5400
  if (!match) return void 0;
4986
5401
  return sanitizeDraftItem(match[1]);
@@ -7120,7 +7535,11 @@ function formatTaskBlock(input) {
7120
7535
  " - Acceptance:",
7121
7536
  ...input.acceptanceItems.map((item) => ` - ${item}`),
7122
7537
  " - Checklist:",
7123
- ...input.checklistItems.map((item) => ` - [ ] ${item}`)
7538
+ ...input.checklistItems.map((item) => ` - [ ] ${item}`),
7539
+ " - Review Evidence: -",
7540
+ " - Review Decision: -",
7541
+ " - Reviewed Head: -",
7542
+ " - Reviewed Tree: -"
7124
7543
  ];
7125
7544
  }
7126
7545
  async function runTaskAdd(featureName, options) {
@@ -7548,7 +7967,7 @@ function registerCodexHooksIntegration(parent) {
7548
7967
  removeLeeSpecKitCodexHooks,
7549
7968
  resolveCodexHooksRepoRoot,
7550
7969
  upsertLeeSpecKitCodexHooks
7551
- } = await import('./hooks-373Z6JG2.js');
7970
+ } = await import('./hooks-67CU3GZC.js');
7552
7971
  const workflowRoot = config.docsRepo === "standalone" ? resolveConfiguredStandaloneWorkspaceRoot(config) : resolveCodexHooksRepoRoot(process.cwd());
7553
7972
  if (!workflowRoot) {
7554
7973
  throw createCliError(
@@ -7953,9 +8372,68 @@ var ISSUE_LABELS = ["Issue", "Issue Number", "\uC774\uC288", "\uC774\uC288 \uBC8
7953
8372
  var BRANCH_LABELS3 = ["Branch", "\uBE0C\uB79C\uCE58"];
7954
8373
  var PR_LABELS = ["PR", "Pull Request"];
7955
8374
  var PR_STATUS_LABELS = ["PR Status", "PR \uC0C1\uD0DC"];
7956
- var PRE_PR_REVIEW_LABELS = ["Pre-PR Review", "PR \uC804 \uB9AC\uBDF0"];
7957
- var PRE_PR_EVIDENCE_LABELS = ["Pre-PR Evidence", "PR \uC804 \uB9AC\uBDF0 Evidence"];
7958
- var PRE_PR_DECISION_LABELS = ["Pre-PR Decision", "PR \uC804 \uB9AC\uBDF0 Decision"];
8375
+ var PRE_PR_REVIEW_LABELS = [
8376
+ "Pre-PR Review",
8377
+ "PR \uC804 \uB9AC\uBDF0",
8378
+ "Feature Review",
8379
+ "Feature \uB9AC\uBDF0"
8380
+ ];
8381
+ var PRE_PR_EVIDENCE_LABELS = [
8382
+ "Pre-PR Evidence",
8383
+ "PR \uC804 \uB9AC\uBDF0 Evidence",
8384
+ "Feature Review Evidence",
8385
+ "Feature \uB9AC\uBDF0 Evidence"
8386
+ ];
8387
+ var PRE_PR_DECISION_LABELS = [
8388
+ "Pre-PR Decision",
8389
+ "PR \uC804 \uB9AC\uBDF0 Decision",
8390
+ "Feature Review Decision",
8391
+ "Feature \uB9AC\uBDF0 Decision"
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
+ ];
8399
+ var PRE_PR_REVIEWED_HEAD_LABELS = [
8400
+ "Pre-PR Reviewed Head",
8401
+ "PR \uC804 \uB9AC\uBDF0 Head",
8402
+ "Feature Reviewed Head",
8403
+ "Feature \uB9AC\uBDF0 Head"
8404
+ ];
8405
+ var PRE_PR_REVIEWED_TREE_LABELS = [
8406
+ "Pre-PR Reviewed Tree",
8407
+ "PR \uC804 \uB9AC\uBDF0 Tree",
8408
+ "Feature Reviewed Tree",
8409
+ "Feature \uB9AC\uBDF0 Tree"
8410
+ ];
8411
+ var PLAN_REVIEW_STATUS_LABELS = ["Plan Review", "Plan \uAC80\uC218", "Plan \uB9AC\uBDF0"];
8412
+ var PLAN_REVIEW_EVIDENCE_LABELS = [
8413
+ "Plan Review Evidence",
8414
+ "Plan \uAC80\uC218 Evidence",
8415
+ "Plan \uB9AC\uBDF0 Evidence"
8416
+ ];
8417
+ var PLAN_REVIEW_DECISION_LABELS = [
8418
+ "Plan Review Decision",
8419
+ "Plan \uAC80\uC218 Decision",
8420
+ "Plan \uB9AC\uBDF0 Decision"
8421
+ ];
8422
+ var PLAN_REVIEW_ROUND_LABELS = [
8423
+ "Plan Review Round",
8424
+ "Plan \uAC80\uC218 Round",
8425
+ "Plan \uB9AC\uBDF0 Round"
8426
+ ];
8427
+ var PLAN_REVIEWED_SPEC_HASH_LABELS = [
8428
+ "Plan Reviewed Spec Hash",
8429
+ "Plan \uAC80\uC218 Spec Hash",
8430
+ "Plan \uB9AC\uBDF0 Spec Hash"
8431
+ ];
8432
+ var PLAN_REVIEWED_PLAN_HASH_LABELS = [
8433
+ "Plan Reviewed Plan Hash",
8434
+ "Plan \uAC80\uC218 Plan Hash",
8435
+ "Plan \uB9AC\uBDF0 Plan Hash"
8436
+ ];
7959
8437
  var COMPLETION_MARKERS = {
7960
8438
  allTasks: "lee-spec-kit:completion:all-tasks",
7961
8439
  tests: "lee-spec-kit:completion:tests",
@@ -7974,7 +8452,10 @@ function resolveWorkflowRequirements(config) {
7974
8452
  requirePr: workflow.requirePr ?? !isLocalWorkflow,
7975
8453
  requireReview: workflow.requireReview ?? !isLocalWorkflow,
7976
8454
  requireMerge: workflow.requireMerge ?? !isLocalWorkflow,
7977
- prePrReviewEnabled: workflow.prePrReview?.enabled ?? !isLocalWorkflow
8455
+ planReviewEnabled: workflow.agentReview?.plan?.enabled ?? true,
8456
+ taskReviewEnabled: workflow.agentReview?.task?.enabled ?? false,
8457
+ featureReviewEnabled: workflow.agentReview?.feature?.enabled ?? workflow.prePrReview?.enabled ?? !isLocalWorkflow,
8458
+ taskExecutionEnabled: workflow.agentExecution?.task?.enabled ?? true
7978
8459
  };
7979
8460
  }
7980
8461
  function parseApprovalStatus(raw) {
@@ -8025,7 +8506,70 @@ function parseCompletionCheckbox(lines, marker, legacyPattern) {
8025
8506
  (line) => legacyPattern.test(line) && parseMarkdownCheckbox(line) === true
8026
8507
  );
8027
8508
  }
8028
- function parseTasksDoc(content) {
8509
+ function parseReviewDecisionOutcome(value) {
8510
+ const match = (value || "").trim().toLowerCase().match(/\b(approve|changes_requested|blocked)\b/);
8511
+ return match?.[1] || null;
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 parseWorkflowTaskLine(line, index = -1) {
8522
+ const canonical = parseTaskLine(line, index);
8523
+ if (canonical) {
8524
+ return {
8525
+ index,
8526
+ raw: line,
8527
+ status: canonical.status,
8528
+ taskId: canonical.taskId,
8529
+ title: canonical.title
8530
+ };
8531
+ }
8532
+ const legacy = line.match(
8533
+ /^\s*-\s*\[(TODO|DOING|DONE|REVIEW)\](?:\[[^\]]+\])*\s+(?:(T-[A-Za-z0-9-]+)\s+)?(.+?)\s*$/i
8534
+ );
8535
+ if (!legacy) return null;
8536
+ return {
8537
+ index,
8538
+ raw: line,
8539
+ status: legacy[1].toUpperCase(),
8540
+ taskId: legacy[2] || null,
8541
+ title: legacy[3].trim()
8542
+ };
8543
+ }
8544
+ function extractTaskReviewValue(lines, startIndex, labels) {
8545
+ let endIndex = lines.length;
8546
+ for (let index = startIndex + 1; index < lines.length; index += 1) {
8547
+ if (parseWorkflowTaskLine(lines[index]) || /^\s*##\s+/.test(lines[index])) {
8548
+ endIndex = index;
8549
+ break;
8550
+ }
8551
+ }
8552
+ for (let index = startIndex + 1; index < endIndex; index += 1) {
8553
+ for (const label of labels) {
8554
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8555
+ const match = lines[index].match(
8556
+ new RegExp(
8557
+ `^\\s*-\\s*(?:\\*\\*)?${escaped}(?:\\*\\*)?:\\s*(.*?)\\s*$`,
8558
+ "i"
8559
+ )
8560
+ );
8561
+ if (match) return sanitizeMetadataValue2(match[1]);
8562
+ }
8563
+ }
8564
+ return null;
8565
+ }
8566
+ function buildSyntheticTaskId(feature, title, sameTitleOccurrence) {
8567
+ const fingerprint = createHash("sha256").update(
8568
+ `${feature.folderName}\0${normalizeCommitTopicText(title).toLowerCase()}\0${sameTitleOccurrence}`
8569
+ ).digest("hex").slice(0, 10);
8570
+ return `T-${feature.id}-legacy-${fingerprint}`;
8571
+ }
8572
+ function parseTasksDoc(content, feature) {
8029
8573
  const issueRaw = extractFieldValue2(content, ISSUE_LABELS);
8030
8574
  const issueNumberMatch = issueRaw?.match(/^#(\d+)$/);
8031
8575
  const issueNumber = issueNumberMatch ? Number(issueNumberMatch[1]) : null;
@@ -8034,15 +8578,51 @@ function parseTasksDoc(content) {
8034
8578
  const prePrDecision = extractFieldValue2(content, PRE_PR_DECISION_LABELS);
8035
8579
  const tasks = [];
8036
8580
  const nonCodeLines = withoutFencedCodeBlocks(content);
8037
- for (const line of nonCodeLines) {
8038
- const match = line.match(
8039
- /^\s*-\s*\[(TODO|DOING|DONE|REVIEW)\](?:\[[^\]]+\])*\s+(.+?)\s*$/i
8581
+ const legacyTitleOccurrences = /* @__PURE__ */ new Map();
8582
+ for (let index = 0; index < nonCodeLines.length; index += 1) {
8583
+ const line = nonCodeLines[index];
8584
+ const parsed = parseWorkflowTaskLine(line, index);
8585
+ if (!parsed) continue;
8586
+ const reviewDecision = extractTaskReviewValue(
8587
+ nonCodeLines,
8588
+ index,
8589
+ ["Review Decision", "Task Review Decision", "\uD0DC\uC2A4\uD06C \uB9AC\uBDF0 Decision"]
8040
8590
  );
8041
- if (!match) continue;
8591
+ const reviewRound = parseReviewRound(
8592
+ extractTaskReviewValue(nonCodeLines, index, [
8593
+ "Review Round",
8594
+ "Task Review Round",
8595
+ "\uD0DC\uC2A4\uD06C \uB9AC\uBDF0 Round"
8596
+ ])
8597
+ );
8598
+ const legacyTitleKey = normalizeCommitTopicText(parsed.title).toLowerCase();
8599
+ const sameTitleOccurrence = (legacyTitleOccurrences.get(legacyTitleKey) || 0) + 1;
8600
+ legacyTitleOccurrences.set(legacyTitleKey, sameTitleOccurrence);
8601
+ const taskId = parsed.taskId || buildSyntheticTaskId(feature, parsed.title, sameTitleOccurrence);
8042
8602
  tasks.push({
8043
8603
  raw: line,
8044
- status: match[1].toUpperCase(),
8045
- title: match[2].trim()
8604
+ status: parsed.status,
8605
+ taskId,
8606
+ taskIdSource: parsed.taskId ? "document" : "synthetic",
8607
+ title: parsed.title,
8608
+ reviewEvidence: extractTaskReviewValue(
8609
+ nonCodeLines,
8610
+ index,
8611
+ ["Review Evidence", "Task Review Evidence", "\uD0DC\uC2A4\uD06C \uB9AC\uBDF0 Evidence"]
8612
+ ),
8613
+ reviewDecision,
8614
+ reviewDecisionOutcome: parseReviewDecisionOutcome(reviewDecision),
8615
+ reviewRound,
8616
+ reviewedHead: extractTaskReviewValue(
8617
+ nonCodeLines,
8618
+ index,
8619
+ ["Reviewed Head", "Task Reviewed Head", "\uD0DC\uC2A4\uD06C \uB9AC\uBDF0 Head"]
8620
+ ),
8621
+ reviewedTree: extractTaskReviewValue(
8622
+ nonCodeLines,
8623
+ index,
8624
+ ["Reviewed Tree", "Task Reviewed Tree", "\uD0DC\uC2A4\uD06C \uB9AC\uBDF0 Tree"]
8625
+ )
8046
8626
  });
8047
8627
  }
8048
8628
  const allTasksChecked = parseCompletionCheckbox(
@@ -8073,11 +8653,7 @@ function parseTasksDoc(content) {
8073
8653
  if (value === "done") return "done";
8074
8654
  return null;
8075
8655
  })();
8076
- const prePrDecisionOutcome = (() => {
8077
- const value = (prePrDecision || "").trim().toLowerCase();
8078
- const match = value.match(/\b(approve|changes_requested|blocked)\b/);
8079
- return match?.[1] || null;
8080
- })();
8656
+ const prePrDecisionOutcome = parseReviewDecisionOutcome(prePrDecision);
8081
8657
  return {
8082
8658
  docStatus: parseApprovalStatus(
8083
8659
  extractFieldValue2(content, DOC_STATUS_LABELS) || void 0
@@ -8092,6 +8668,15 @@ function parseTasksDoc(content) {
8092
8668
  ),
8093
8669
  prePrDecision: sanitizeMetadataValue2(prePrDecision),
8094
8670
  prePrDecisionOutcome,
8671
+ prePrReviewRound: parseReviewRound(
8672
+ extractFieldValue2(content, PRE_PR_REVIEW_ROUND_LABELS)
8673
+ ),
8674
+ prePrReviewedHead: sanitizeMetadataValue2(
8675
+ extractFieldValue2(content, PRE_PR_REVIEWED_HEAD_LABELS)
8676
+ ),
8677
+ prePrReviewedTree: sanitizeMetadataValue2(
8678
+ extractFieldValue2(content, PRE_PR_REVIEWED_TREE_LABELS)
8679
+ ),
8095
8680
  tasks,
8096
8681
  completion: {
8097
8682
  allTasksChecked,
@@ -8106,6 +8691,63 @@ function sanitizeMetadataValue2(value) {
8106
8691
  if (!trimmed || trimmed === "-") return null;
8107
8692
  return trimmed;
8108
8693
  }
8694
+ function parsePlanReview(content) {
8695
+ const statusValue = sanitizeMetadataValue2(
8696
+ extractFieldValue2(content, PLAN_REVIEW_STATUS_LABELS)
8697
+ )?.toLowerCase();
8698
+ const status = statusValue === "pending" || statusValue === "running" || statusValue === "done" ? statusValue : null;
8699
+ const evidence = sanitizeMetadataValue2(
8700
+ extractFieldValue2(content, PLAN_REVIEW_EVIDENCE_LABELS)
8701
+ );
8702
+ const decision = sanitizeMetadataValue2(
8703
+ extractFieldValue2(content, PLAN_REVIEW_DECISION_LABELS)
8704
+ );
8705
+ const reviewRound = parseReviewRound(
8706
+ extractFieldValue2(content, PLAN_REVIEW_ROUND_LABELS)
8707
+ );
8708
+ const reviewedSpecHash = sanitizeMetadataValue2(
8709
+ extractFieldValue2(content, PLAN_REVIEWED_SPEC_HASH_LABELS)
8710
+ );
8711
+ const reviewedPlanHash = sanitizeMetadataValue2(
8712
+ extractFieldValue2(content, PLAN_REVIEWED_PLAN_HASH_LABELS)
8713
+ );
8714
+ return {
8715
+ status,
8716
+ evidence,
8717
+ decision,
8718
+ decisionOutcome: parseReviewDecisionOutcome(decision),
8719
+ reviewRound,
8720
+ reviewedSpecHash,
8721
+ reviewedPlanHash,
8722
+ hasMetadata: PLAN_REVIEW_STATUS_LABELS.some((label) => {
8723
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8724
+ return new RegExp(`^\\s*-\\s*\\*\\*${escaped}\\*\\*:`, "mi").test(content);
8725
+ })
8726
+ };
8727
+ }
8728
+ function buildReviewDocumentHash(content, excludedLabels) {
8729
+ const excluded = new Set(
8730
+ [...excludedLabels, "Status", "\uC0C1\uD0DC"].map((label) => label.toLowerCase())
8731
+ );
8732
+ const normalized = content.replace(/\r\n/g, "\n").split("\n").filter((line) => {
8733
+ const match = line.match(/^\s*-\s*\*\*(.+?)\*\*\s*:/);
8734
+ return !match || !excluded.has(match[1].trim().toLowerCase());
8735
+ }).join("\n").trim();
8736
+ return createHash("sha256").update(normalized).digest("hex");
8737
+ }
8738
+ function buildPlanReviewTarget(specContent, planContent) {
8739
+ return {
8740
+ specHash: buildReviewDocumentHash(specContent, []),
8741
+ planHash: buildReviewDocumentHash(planContent, [
8742
+ ...PLAN_REVIEW_STATUS_LABELS,
8743
+ ...PLAN_REVIEW_EVIDENCE_LABELS,
8744
+ ...PLAN_REVIEW_DECISION_LABELS,
8745
+ ...PLAN_REVIEW_ROUND_LABELS,
8746
+ ...PLAN_REVIEWED_SPEC_HASH_LABELS,
8747
+ ...PLAN_REVIEWED_PLAN_HASH_LABELS
8748
+ ])
8749
+ };
8750
+ }
8109
8751
  function normalizeCommitTopicText(value) {
8110
8752
  return value.replace(/\s+/g, " ").trim();
8111
8753
  }
@@ -8241,7 +8883,7 @@ function hasOpenTask(tasks) {
8241
8883
  function hasUncommittedChanges(gitCwd) {
8242
8884
  if (!gitCwd) return false;
8243
8885
  const status = runGitCapture(
8244
- ["status", "--porcelain", "--untracked-files=no"],
8886
+ ["status", "--porcelain", "--untracked-files=normal"],
8245
8887
  gitCwd
8246
8888
  ) || "";
8247
8889
  return status.trim().length > 0;
@@ -8305,7 +8947,7 @@ function resolveProjectCommitTopic(feature, tasks) {
8305
8947
  const activeTask = tasks.tasks.find(
8306
8948
  (task) => task.status === "DOING" || task.status === "REVIEW"
8307
8949
  );
8308
- const raw = activeTask?.title || getLastDoneTask(tasks)?.title || nextTodoTask(tasks)?.title || feature.folderName;
8950
+ const raw = activeTask?.title || getLastDoneTask(tasks)?.title || tasks.tasks.find((task) => task.status === "TODO")?.title || feature.folderName;
8309
8951
  const withoutTaskId = normalizeCommitTopicText(raw || "").replace(
8310
8952
  /^T-[A-Za-z0-9-]+\s+/,
8311
8953
  ""
@@ -8362,6 +9004,19 @@ function resolveExpectedBranch(feature, tasks) {
8362
9004
  if (!tasks.issueNumber) return null;
8363
9005
  return `feat/${tasks.issueNumber}-${feature.slug}`;
8364
9006
  }
9007
+ function buildExpectedBranchCommand(config, feature, expectedBranch, requireWorktree) {
9008
+ if (requireWorktree) {
9009
+ return buildManagedWorktreeCreateCommand(
9010
+ config,
9011
+ feature.git.projectGitCwd,
9012
+ expectedBranch
9013
+ );
9014
+ }
9015
+ return localBranchExists(
9016
+ resolveProjectRootGitCwd(config, feature),
9017
+ expectedBranch
9018
+ ) ? `git checkout ${expectedBranch}` : `git checkout -b ${expectedBranch}`;
9019
+ }
8365
9020
  function resolveProjectRootFromGitCwd2(projectGitCwd) {
8366
9021
  return runGitCapture(["rev-parse", "--show-toplevel"], projectGitCwd) || path8.resolve(projectGitCwd);
8367
9022
  }
@@ -8526,19 +9181,96 @@ function buildPostMergeCleanupSummary(state) {
8526
9181
  }
8527
9182
  return `Finish the post-merge cleanup before closing the feature: ${remaining.join(", ")}.`;
8528
9183
  }
8529
- function nextTodoTask(tasks) {
8530
- return tasks.tasks.find((task) => task.status === "DOING") || tasks.tasks.find((task) => task.status === "TODO") || null;
9184
+ function nextExecutableTask(tasks) {
9185
+ return tasks.tasks.find((task) => task.status === "DOING") || tasks.tasks.find((task) => task.status === "REVIEW") || tasks.tasks.find((task) => task.status === "TODO") || tasks.tasks.find((task) => task.status !== "DONE");
8531
9186
  }
8532
9187
  function allTasksDone(tasks) {
8533
9188
  return tasks.tasks.length > 0 && tasks.tasks.every((task) => task.status === "DONE");
8534
9189
  }
8535
- function prePrSatisfied(config, feature, tasks) {
8536
- return tasks.prePrReviewStatus === "done" && isPrePrEvidenceSatisfied({
9190
+ function resolveAgentReviewPhase(config, scope) {
9191
+ if (scope === "plan") {
9192
+ return config.workflow?.agentReview?.plan || {};
9193
+ }
9194
+ if (scope === "task") {
9195
+ return config.workflow?.agentReview?.task || {};
9196
+ }
9197
+ return config.workflow?.agentReview?.feature || config.workflow?.prePrReview || {};
9198
+ }
9199
+ function resolveProjectReviewTarget(config, projectGitCwd, scope, taskBase = null) {
9200
+ const pathArgs = ["--", "."];
9201
+ const relativeDocsDir = path8.relative(projectGitCwd, config.docsDir);
9202
+ const normalizedDocsDir = normalizeGitRelativePath(relativeDocsDir);
9203
+ if (normalizedDocsDir && normalizedDocsDir !== "." && normalizedDocsDir !== ".." && !normalizedDocsDir.startsWith("../")) {
9204
+ pathArgs.push(`:(exclude)${normalizedDocsDir}/**`);
9205
+ }
9206
+ const targetSha = runGitCapture(["log", "-n", "1", "--pretty=%H", ...pathArgs], projectGitCwd) || runGitCapture(["rev-parse", "HEAD"], projectGitCwd) || "";
9207
+ if (!targetSha) return null;
9208
+ const targetTree = runGitCapture(["rev-parse", `${targetSha}^{tree}`], projectGitCwd) || "";
9209
+ if (!targetTree) return null;
9210
+ let baseSha = "";
9211
+ if (scope === "task" && taskBase) {
9212
+ const actualBaseTree = runGitCapture(
9213
+ ["rev-parse", `${taskBase.reviewedHead}^{tree}`],
9214
+ projectGitCwd
9215
+ ) || "";
9216
+ if (actualBaseTree === taskBase.reviewedTree && taskBase.reviewedHead !== targetSha && isAncestor(projectGitCwd, taskBase.reviewedHead, targetSha)) {
9217
+ baseSha = taskBase.reviewedHead;
9218
+ }
9219
+ }
9220
+ if (!baseSha) {
9221
+ const baseBranch = config.workflow?.baseBranch?.trim() || "main";
9222
+ for (const candidate of [`origin/${baseBranch}`, baseBranch]) {
9223
+ const candidateBase = runGitCapture(["merge-base", candidate, targetSha], projectGitCwd) || "";
9224
+ if (candidateBase && candidateBase !== targetSha) {
9225
+ baseSha = candidateBase;
9226
+ break;
9227
+ }
9228
+ }
9229
+ }
9230
+ if (!baseSha) {
9231
+ baseSha = runGitCapture(["rev-parse", `${targetSha}^`], projectGitCwd) || targetSha;
9232
+ }
9233
+ return { baseSha, targetSha, targetTree };
9234
+ }
9235
+ function reviewEvidenceSatisfied(config, feature, scope, evidence) {
9236
+ return isPrePrEvidenceSatisfied({
8537
9237
  docsDir: config.docsDir,
8538
9238
  featureDir: feature.path,
8539
- evidence: tasks.prePrEvidence,
8540
- evidenceMode: config.workflow?.prePrReview?.evidenceMode
8541
- }) && !!tasks.prePrDecision && tasks.prePrDecisionOutcome === "approve";
9239
+ evidence,
9240
+ evidenceMode: resolveAgentReviewPhase(config, scope).evidenceMode
9241
+ });
9242
+ }
9243
+ 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;
9245
+ }
9246
+ 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;
9248
+ }
9249
+ 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;
9251
+ }
9252
+ function recordedTaskReviewSatisfied(config, feature, projectGitCwd, task, currentTargetSha) {
9253
+ if (!currentTargetSha || !task.reviewedHead || !task.reviewedTree || task.reviewDecisionOutcome !== "approve" || !reviewEvidenceSatisfied(config, feature, "task", task.reviewEvidence)) {
9254
+ return false;
9255
+ }
9256
+ const actualTree = runGitCapture(
9257
+ ["rev-parse", `${task.reviewedHead}^{tree}`],
9258
+ projectGitCwd
9259
+ ) || "";
9260
+ return actualTree === task.reviewedTree && isAncestor(projectGitCwd, task.reviewedHead, currentTargetSha);
9261
+ }
9262
+ function resolvePreviousTaskReviewBase(config, feature, tasks, task) {
9263
+ const taskIndex = tasks.tasks.indexOf(task);
9264
+ for (let index = taskIndex - 1; index >= 0; index -= 1) {
9265
+ const previous = tasks.tasks[index];
9266
+ if (previous.reviewDecisionOutcome === "approve" && previous.reviewedHead && previous.reviewedTree && reviewEvidenceSatisfied(config, feature, "task", previous.reviewEvidence)) {
9267
+ return {
9268
+ reviewedHead: previous.reviewedHead,
9269
+ reviewedTree: previous.reviewedTree
9270
+ };
9271
+ }
9272
+ }
9273
+ return null;
8542
9274
  }
8543
9275
  function issueExistsRemotely(issueNumber, feature) {
8544
9276
  if (!issueNumber) return false;
@@ -8558,25 +9290,40 @@ function prExistsRemotely(prRef, feature) {
8558
9290
  );
8559
9291
  return result.code === 0;
8560
9292
  }
8561
- function buildAction(category, summary, approvalRequired, command = null, reviewer) {
9293
+ function buildAction(category, summary, approvalRequired, command = null, agent, actionContext) {
8562
9294
  return {
8563
9295
  category,
8564
9296
  summary,
8565
9297
  approvalRequired,
8566
9298
  command,
8567
- ...reviewer ? {
8568
- executor: reviewer.type,
8569
- model: reviewer.model,
8570
- reasoningEffort: reviewer.reasoningEffort,
8571
- onUnavailable: reviewer.onUnavailable
8572
- } : {}
9299
+ ...agent ? {
9300
+ executor: agent.type,
9301
+ model: agent.model,
9302
+ reasoningEffort: agent.reasoningEffort,
9303
+ onUnavailable: agent.onUnavailable
9304
+ } : {},
9305
+ ...actionContext || {}
9306
+ };
9307
+ }
9308
+ function resolveAgentReviewer(config, scope) {
9309
+ const defaults = createDefaultAgentReviewerConfig();
9310
+ const configured = resolveAgentReviewPhase(config, scope).reviewer;
9311
+ const model = typeof configured?.model === "string" && configured.model.trim() ? configured.model.trim() : defaults.model;
9312
+ const reasoningEffort = AGENT_REVIEW_REASONING_EFFORTS.includes(
9313
+ configured?.reasoningEffort
9314
+ ) ? configured?.reasoningEffort : defaults.reasoningEffort;
9315
+ return {
9316
+ type: "subagent",
9317
+ model,
9318
+ reasoningEffort,
9319
+ onUnavailable: configured?.onUnavailable === "error" ? "error" : defaults.onUnavailable
8573
9320
  };
8574
9321
  }
8575
- function resolvePrePrReviewer(config) {
8576
- const defaults = createDefaultPrePrReviewerConfig();
8577
- const configured = config.workflow?.prePrReview?.reviewer;
9322
+ function resolveTaskExecutor(config) {
9323
+ const defaults = createDefaultAgentExecutionTaskConfig();
9324
+ const configured = config.workflow?.agentExecution?.task;
8578
9325
  const model = typeof configured?.model === "string" && configured.model.trim() ? configured.model.trim() : defaults.model;
8579
- const reasoningEffort = PRE_PR_REVIEW_REASONING_EFFORTS.includes(
9326
+ const reasoningEffort = AGENT_REVIEW_REASONING_EFFORTS.includes(
8580
9327
  configured?.reasoningEffort
8581
9328
  ) ? configured?.reasoningEffort : defaults.reasoningEffort;
8582
9329
  return {
@@ -8586,6 +9333,23 @@ function resolvePrePrReviewer(config) {
8586
9333
  onUnavailable: configured?.onUnavailable === "error" ? "error" : defaults.onUnavailable
8587
9334
  };
8588
9335
  }
9336
+ function createTaskWorkerContract() {
9337
+ return {
9338
+ role: "task_implementation_worker",
9339
+ executeDirectly: true,
9340
+ spawnSubagents: false,
9341
+ runWorkflowStage: false,
9342
+ editProjectCode: true,
9343
+ runTaskScopedVerification: true,
9344
+ followVerificationContract: true,
9345
+ addUnplannedDurableTests: false,
9346
+ editDocs: false,
9347
+ changeTaskState: false,
9348
+ commit: false,
9349
+ requestApproval: false,
9350
+ remoteActions: false
9351
+ };
9352
+ }
8589
9353
  function buildStageOption(label, reply, category, summary, command = null) {
8590
9354
  return {
8591
9355
  label,
@@ -8893,11 +9657,115 @@ function resolveFeatureSelectionError(selection) {
8893
9657
  blockedReasonCode: null
8894
9658
  };
8895
9659
  }
8896
- async function collectWorkflowStage(cwd, selector, component) {
8897
- const config = await getConfig(cwd);
8898
- if (!config) {
8899
- return {
8900
- status: "error",
9660
+ function resolvePlanReviewPayload(config, feature, review, target) {
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;
9665
+ const actionContext = {
9666
+ reviewScope: "plan",
9667
+ reviewRound: nextReviewRound,
9668
+ maxReviewRounds,
9669
+ specHash: target.specHash,
9670
+ planHash: target.planHash,
9671
+ docsDirectory: config.docsDir
9672
+ };
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
+ }
9724
+ }
9725
+ if (targetMatches && review.decisionOutcome === "blocked") {
9726
+ return {
9727
+ status: "ok",
9728
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
9729
+ docsDir: config.docsDir,
9730
+ featureRef: buildFeatureRef(feature),
9731
+ stage: "plan_review",
9732
+ nextAction: buildAction(
9733
+ "plan_review",
9734
+ "Resolve the recorded Plan review blocker before Plan approval.",
9735
+ false,
9736
+ null,
9737
+ void 0,
9738
+ { ...actionContext, reviewRound }
9739
+ ),
9740
+ approvalRequired: false,
9741
+ implementationAllowed: false,
9742
+ blockedReasonCode: "PLAN_REVIEW_NOT_APPROVED"
9743
+ };
9744
+ }
9745
+ return {
9746
+ status: "ok",
9747
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
9748
+ docsDir: config.docsDir,
9749
+ featureRef: buildFeatureRef(feature),
9750
+ stage: "plan_review",
9751
+ nextAction: buildAction(
9752
+ "plan_review",
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.`,
9754
+ false,
9755
+ null,
9756
+ resolveAgentReviewer(config, "plan"),
9757
+ actionContext
9758
+ ),
9759
+ approvalRequired: false,
9760
+ implementationAllowed: false,
9761
+ blockedReasonCode: "PLAN_REVIEW_NOT_APPROVED"
9762
+ };
9763
+ }
9764
+ async function collectWorkflowStage(cwd, selector, component) {
9765
+ const config = await getConfig(cwd);
9766
+ if (!config) {
9767
+ return {
9768
+ status: "error",
8901
9769
  reasonCode: "CONFIG_NOT_FOUND",
8902
9770
  docsDir: null,
8903
9771
  featureRef: null,
@@ -8927,7 +9795,13 @@ async function collectWorkflowStage(cwd, selector, component) {
8927
9795
  const planStatus = parseApprovalStatus(
8928
9796
  extractFieldValue2(planContent || "", ["Status", "\uC0C1\uD0DC"]) || void 0
8929
9797
  );
8930
- const tasks = parseTasksDoc(tasksContent || "");
9798
+ const tasks = parseTasksDoc(tasksContent || "", feature);
9799
+ const planReview = parsePlanReview(planContent || "");
9800
+ const planReviewTarget = buildPlanReviewTarget(
9801
+ specContent || "",
9802
+ planContent || ""
9803
+ );
9804
+ const enforcePlanReview = requirements.planReviewEnabled && (tasks.docStatus !== "approved" || planReview.hasMetadata);
8931
9805
  const issueDraft = parseWorkflowDraftMetadataExtended(issueContent || "");
8932
9806
  const prDraft = parseWorkflowDraftMetadataExtended(prContent || "");
8933
9807
  const remoteReviewState = requirements.requireReview && tasks.prLink ? resolveRemotePrReviewState(tasks.prLink, feature) : "unknown";
@@ -8963,6 +9837,14 @@ async function collectWorkflowStage(cwd, selector, component) {
8963
9837
  }
8964
9838
  if (planStatus !== "approved") {
8965
9839
  const isReviewStage = planStatus === "review";
9840
+ if (isReviewStage && enforcePlanReview && !planReviewSatisfied(config, feature, planReview, planReviewTarget)) {
9841
+ return resolvePlanReviewPayload(
9842
+ config,
9843
+ feature,
9844
+ planReview,
9845
+ planReviewTarget
9846
+ );
9847
+ }
8966
9848
  const approvalRequired = isReviewStage ? resolveActionApprovalRequired(config, "plan_approve", true) : false;
8967
9849
  const actionOptions = approvalRequired ? buildApprovalActionOptions({
8968
9850
  approveSummary: "Approve plan.md and continue to the tasks stage.",
@@ -8986,6 +9868,9 @@ async function collectWorkflowStage(cwd, selector, component) {
8986
9868
  blockedReasonCode: "PLAN_NOT_APPROVED"
8987
9869
  };
8988
9870
  }
9871
+ if (enforcePlanReview && !planReviewSatisfied(config, feature, planReview, planReviewTarget)) {
9872
+ return resolvePlanReviewPayload(config, feature, planReview, planReviewTarget);
9873
+ }
8989
9874
  if (tasks.tasks.length === 0 || tasks.docStatus !== "approved") {
8990
9875
  const isReviewStage = tasks.docStatus === "review";
8991
9876
  const approvalRequired = isReviewStage ? resolveActionApprovalRequired(config, "tasks_approve", true) : false;
@@ -9064,7 +9949,12 @@ async function collectWorkflowStage(cwd, selector, component) {
9064
9949
  const expectedBranch = resolveExpectedBranch(feature, tasks);
9065
9950
  const currentBranch = runGitCapture(["branch", "--show-current"], effectiveProjectGitCwd) || runGitCapture(["rev-parse", "--abbrev-ref", "HEAD"], effectiveProjectGitCwd) || null;
9066
9951
  if (expectedBranch && currentBranch !== expectedBranch) {
9067
- const branchCommand = requirements.requireWorktree ? buildManagedWorktreeCreateCommand(config, feature.git.projectGitCwd, expectedBranch) : `git checkout -b ${expectedBranch}`;
9952
+ const branchCommand = buildExpectedBranchCommand(
9953
+ config,
9954
+ feature,
9955
+ expectedBranch,
9956
+ requirements.requireWorktree
9957
+ );
9068
9958
  return {
9069
9959
  status: "ok",
9070
9960
  reasonCode: "WORKFLOW_STAGE_RESOLVED",
@@ -9087,6 +9977,183 @@ async function collectWorkflowStage(cwd, selector, component) {
9087
9977
  const lastDoneTask = getLastDoneTask(tasks);
9088
9978
  const docsDirty = hasUncommittedChanges(feature.git.docsGitCwd);
9089
9979
  const projectDirty = hasUncommittedChanges(effectiveProjectGitCwd);
9980
+ const currentTaskReviewTip = requirements.taskReviewEnabled ? resolveProjectReviewTarget(config, effectiveProjectGitCwd, "task") : null;
9981
+ const unreviewedDoneTask = requirements.taskReviewEnabled ? tasks.tasks.find(
9982
+ (task) => task.status === "DONE" && !recordedTaskReviewSatisfied(
9983
+ config,
9984
+ feature,
9985
+ effectiveProjectGitCwd,
9986
+ task,
9987
+ currentTaskReviewTip?.targetSha || null
9988
+ )
9989
+ ) || null : null;
9990
+ const reviewTask = unreviewedDoneTask || tasks.tasks.find((task) => task.status === "REVIEW") || null;
9991
+ if (requirements.taskReviewEnabled && reviewTask) {
9992
+ if (docsDirty || projectDirty) {
9993
+ return {
9994
+ status: "ok",
9995
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
9996
+ docsDir: config.docsDir,
9997
+ featureRef: buildFeatureRef(feature),
9998
+ stage: "task_commit",
9999
+ nextAction: buildAction(
10000
+ "task_commit",
10001
+ buildTaskCommitSummary({
10002
+ feature,
10003
+ tasks,
10004
+ effectiveProjectGitCwd,
10005
+ docsDirty,
10006
+ projectDirty,
10007
+ gateFailureReason: "the task must have a clean checkpoint commit before independent review"
10008
+ }),
10009
+ false
10010
+ ),
10011
+ approvalRequired: false,
10012
+ implementationAllowed: false,
10013
+ blockedReasonCode: "TASK_COMMIT_REQUIRED"
10014
+ };
10015
+ }
10016
+ const reviewTarget = resolveProjectReviewTarget(
10017
+ config,
10018
+ effectiveProjectGitCwd,
10019
+ "task",
10020
+ resolvePreviousTaskReviewBase(config, feature, tasks, reviewTask)
10021
+ );
10022
+ if (!reviewTarget) {
10023
+ return {
10024
+ status: "ok",
10025
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
10026
+ docsDir: config.docsDir,
10027
+ featureRef: buildFeatureRef(feature),
10028
+ stage: "task_commit",
10029
+ nextAction: buildAction(
10030
+ "task_commit",
10031
+ "Create a project commit for the task before requesting independent review.",
10032
+ false
10033
+ ),
10034
+ approvalRequired: false,
10035
+ implementationAllowed: false,
10036
+ blockedReasonCode: "TASK_COMMIT_REQUIRED"
10037
+ };
10038
+ }
10039
+ const reviewContext = {
10040
+ reviewScope: "task",
10041
+ reviewRound: reviewTask.reviewRound || 1,
10042
+ maxReviewRounds: resolveMaxReviewRounds(config),
10043
+ ...reviewTask.taskId ? { taskId: reviewTask.taskId } : {},
10044
+ ...reviewTarget
10045
+ };
10046
+ const evidenceMatchesTarget = reviewTask.reviewedHead === reviewTarget.targetSha && reviewTask.reviewedTree === reviewTarget.targetTree;
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
+ }
10093
+ }
10094
+ if (evidenceMatchesTarget && reviewTask.reviewDecisionOutcome === "blocked") {
10095
+ return {
10096
+ status: "ok",
10097
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
10098
+ docsDir: config.docsDir,
10099
+ featureRef: buildFeatureRef(feature),
10100
+ stage: "task_review",
10101
+ nextAction: buildAction(
10102
+ "task_review",
10103
+ `Resolve the recorded review blocker for ${reviewTask.taskId || reviewTask.title} before continuing.`,
10104
+ false,
10105
+ null,
10106
+ void 0,
10107
+ reviewContext
10108
+ ),
10109
+ approvalRequired: false,
10110
+ implementationAllowed: false,
10111
+ blockedReasonCode: "TASK_REVIEW_NOT_APPROVED"
10112
+ };
10113
+ }
10114
+ 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.`;
10116
+ return {
10117
+ status: "ok",
10118
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
10119
+ docsDir: config.docsDir,
10120
+ featureRef: buildFeatureRef(feature),
10121
+ stage: "task_review",
10122
+ nextAction: buildAction(
10123
+ "task_review_complete",
10124
+ completionSummary,
10125
+ false,
10126
+ null,
10127
+ void 0,
10128
+ reviewContext
10129
+ ),
10130
+ approvalRequired: false,
10131
+ implementationAllowed: false,
10132
+ blockedReasonCode: "TASK_REVIEW_NOT_APPROVED"
10133
+ };
10134
+ }
10135
+ return {
10136
+ status: "ok",
10137
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
10138
+ docsDir: config.docsDir,
10139
+ featureRef: buildFeatureRef(feature),
10140
+ stage: "task_review",
10141
+ nextAction: buildAction(
10142
+ "task_review",
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.`,
10144
+ false,
10145
+ null,
10146
+ resolveAgentReviewer(config, "task"),
10147
+ {
10148
+ ...reviewContext,
10149
+ reviewRound: !evidenceMatchesTarget && reviewTask.reviewDecisionOutcome ? reviewContext.reviewRound + 1 : reviewContext.reviewRound
10150
+ }
10151
+ ),
10152
+ approvalRequired: false,
10153
+ implementationAllowed: false,
10154
+ blockedReasonCode: "TASK_REVIEW_NOT_APPROVED"
10155
+ };
10156
+ }
9090
10157
  const pendingDoneTransitions = countPendingDoneTransitions(feature) || 0;
9091
10158
  const taskCommitCheckpointRequired = !activeTaskOpen && !!lastDoneTask && (projectDirty || pendingDoneTransitions > 0);
9092
10159
  if (taskCommitCheckpointRequired) {
@@ -9116,8 +10183,168 @@ async function collectWorkflowStage(cwd, selector, component) {
9116
10183
  }
9117
10184
  const committedTaskGate = taskCommitGatePolicy !== "off" && lastDoneTask ? checkTaskCommitGate(feature, effectiveProjectGitCwd, lastDoneTask) : { pass: true };
9118
10185
  const committedTaskGateRequiresCheckpoint = taskCommitGatePolicy === "strict" || committedTaskGate.reason === "DONE_TRANSITIONS_COUNT";
10186
+ if (committedTaskGateRequiresCheckpoint && !committedTaskGate.pass) {
10187
+ return {
10188
+ status: "ok",
10189
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
10190
+ docsDir: config.docsDir,
10191
+ featureRef: buildFeatureRef(feature),
10192
+ stage: "task_commit",
10193
+ nextAction: buildAction(
10194
+ "task_commit",
10195
+ buildTaskCommitSummary({
10196
+ feature,
10197
+ tasks,
10198
+ effectiveProjectGitCwd,
10199
+ docsDirty,
10200
+ projectDirty,
10201
+ gateFailureReason: describeTaskCommitGateFailure(committedTaskGate)
10202
+ }),
10203
+ false
10204
+ ),
10205
+ approvalRequired: false,
10206
+ implementationAllowed: false,
10207
+ blockedReasonCode: "TASK_COMMIT_REQUIRED"
10208
+ };
10209
+ }
10210
+ let resolvedLocalState = null;
10211
+ if (allTasksDone(tasks) && config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config) !== "none") {
10212
+ resolvedLocalState = await resolveLocalIntegrationContext(config, feature);
10213
+ }
10214
+ const remoteReviewAlreadyComplete = currentReviewState === "merged" && tasks.prStatus === "approved" && prDraft.prStatus === "approved";
10215
+ const featureReviewNeededForLifecycle = allTasksDone(tasks) && requirements.featureReviewEnabled && !resolvedLocalState?.integrationComplete && !remoteReviewAlreadyComplete;
10216
+ if (featureReviewNeededForLifecycle) {
10217
+ const expectedBranch = resolveExpectedBranch(feature, tasks);
10218
+ const currentBranch = runGitCapture(["branch", "--show-current"], effectiveProjectGitCwd) || runGitCapture(
10219
+ ["rev-parse", "--abbrev-ref", "HEAD"],
10220
+ effectiveProjectGitCwd
10221
+ ) || null;
10222
+ if (requirements.requireBranch && expectedBranch && currentBranch !== expectedBranch) {
10223
+ return {
10224
+ status: "ok",
10225
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
10226
+ docsDir: config.docsDir,
10227
+ featureRef: buildFeatureRef(feature),
10228
+ stage: "branch",
10229
+ nextAction: buildAction(
10230
+ "branch_create",
10231
+ requirements.requireWorktree ? `Restore or create the managed worktree for ${expectedBranch} before Feature review.` : `Switch back to ${expectedBranch} before Feature review.`,
10232
+ false,
10233
+ buildExpectedBranchCommand(
10234
+ config,
10235
+ feature,
10236
+ expectedBranch,
10237
+ requirements.requireWorktree
10238
+ )
10239
+ ),
10240
+ approvalRequired: false,
10241
+ implementationAllowed: false,
10242
+ blockedReasonCode: "BRANCH_NOT_READY"
10243
+ };
10244
+ }
10245
+ const reviewTarget = resolveProjectReviewTarget(
10246
+ config,
10247
+ effectiveProjectGitCwd,
10248
+ "feature"
10249
+ );
10250
+ const reviewContext = reviewTarget ? {
10251
+ reviewScope: "feature",
10252
+ reviewRound: tasks.prePrReviewRound || 1,
10253
+ maxReviewRounds: resolveMaxReviewRounds(config),
10254
+ ...reviewTarget
10255
+ } : null;
10256
+ const evidenceMatchesTarget = !!reviewTarget && tasks.prePrReviewedHead === reviewTarget.targetSha && tasks.prePrReviewedTree === reviewTarget.targetTree;
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
+ }
10303
+ }
10304
+ if (reviewContext && evidenceMatchesTarget && tasks.prePrDecisionOutcome === "blocked") {
10305
+ return {
10306
+ status: "ok",
10307
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
10308
+ docsDir: config.docsDir,
10309
+ featureRef: buildFeatureRef(feature),
10310
+ stage: "pre_pr_review",
10311
+ nextAction: buildAction(
10312
+ "pre_pr_review",
10313
+ "Resolve the recorded Feature review blocker before implementation approval or integration.",
10314
+ false,
10315
+ null,
10316
+ void 0,
10317
+ reviewContext
10318
+ ),
10319
+ approvalRequired: false,
10320
+ implementationAllowed: false,
10321
+ blockedReasonCode: "PRE_PR_REVIEW_NOT_APPROVED"
10322
+ };
10323
+ }
10324
+ if (!featureReviewSatisfied(config, feature, tasks, reviewTarget)) {
10325
+ const requestedRound = reviewContext && !evidenceMatchesTarget && tasks.prePrDecisionOutcome ? reviewContext.reviewRound + 1 : reviewContext?.reviewRound;
10326
+ return {
10327
+ status: "ok",
10328
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
10329
+ docsDir: config.docsDir,
10330
+ featureRef: buildFeatureRef(feature),
10331
+ stage: "pre_pr_review",
10332
+ nextAction: buildAction(
10333
+ "pre_pr_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.",
10335
+ false,
10336
+ null,
10337
+ reviewTarget ? resolveAgentReviewer(config, "feature") : void 0,
10338
+ reviewContext ? { ...reviewContext, reviewRound: requestedRound } : void 0
10339
+ ),
10340
+ approvalRequired: false,
10341
+ implementationAllowed: false,
10342
+ blockedReasonCode: "PRE_PR_REVIEW_NOT_APPROVED"
10343
+ };
10344
+ }
10345
+ }
9119
10346
  if (!allTasksDone(tasks)) {
9120
- const currentTask = nextTodoTask(tasks);
10347
+ const currentTask = nextExecutableTask(tasks);
9121
10348
  if (committedTaskGateRequiresCheckpoint && !committedTaskGate.pass) {
9122
10349
  return {
9123
10350
  status: "ok",
@@ -9144,6 +10371,7 @@ async function collectWorkflowStage(cwd, selector, component) {
9144
10371
  }
9145
10372
  const commitWarning = taskCommitGatePolicy === "warn" && !committedTaskGate.pass ? `
9146
10373
  Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)}` : "";
10374
+ const delegationGuidance = requirements.taskExecutionEnabled ? " Delegate implementation and task-scoped verification to a fresh configured subagent under the returned workerContract. The worker executes directly and must not run workflow-stage or delegate again. The main agent retains docs updates, task state transitions, commits, approvals, and remote actions" : "";
9147
10375
  return {
9148
10376
  status: "ok",
9149
10377
  reasonCode: "WORKFLOW_STAGE_RESOLVED",
@@ -9152,8 +10380,20 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
9152
10380
  stage: "implementation",
9153
10381
  nextAction: buildAction(
9154
10382
  "task_execute",
9155
- currentTask ? `Continue the next implementation task: ${currentTask.title}${commitWarning}` : "Continue the active implementation task.",
9156
- false
10383
+ `Continue the next implementation task: ${currentTask.title}.${delegationGuidance}${requirements.taskReviewEnabled ? " When implementation and checks are complete, move it to REVIEW instead of DONE" : ""}${commitWarning}`,
10384
+ false,
10385
+ null,
10386
+ requirements.taskExecutionEnabled ? resolveTaskExecutor(config) : void 0,
10387
+ {
10388
+ taskId: currentTask.taskId,
10389
+ taskIdSource: currentTask.taskIdSource,
10390
+ taskTitle: currentTask.title,
10391
+ workingDirectory: effectiveProjectGitCwd,
10392
+ ...requirements.taskExecutionEnabled ? {
10393
+ docsDirectory: config.docsDir,
10394
+ workerContract: createTaskWorkerContract()
10395
+ } : {}
10396
+ }
9157
10397
  ),
9158
10398
  approvalRequired: false,
9159
10399
  implementationAllowed: true,
@@ -9217,27 +10457,8 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
9217
10457
  blockedReasonCode: "IMPLEMENTATION_APPROVAL_REQUIRED"
9218
10458
  };
9219
10459
  }
9220
- if (requirements.prePrReviewEnabled && !prePrSatisfied(config, feature, tasks)) {
9221
- return {
9222
- status: "ok",
9223
- reasonCode: "WORKFLOW_STAGE_RESOLVED",
9224
- docsDir: config.docsDir,
9225
- featureRef: buildFeatureRef(feature),
9226
- stage: "pre_pr_review",
9227
- nextAction: buildAction(
9228
- "pre_pr_review",
9229
- "Delegate an independent read-only Pre-PR review to a fresh subagent and record its findings, decision, and reviewer metadata as evidence.",
9230
- false,
9231
- null,
9232
- resolvePrePrReviewer(config)
9233
- ),
9234
- approvalRequired: false,
9235
- implementationAllowed: false,
9236
- blockedReasonCode: "PRE_PR_REVIEW_NOT_APPROVED"
9237
- };
9238
- }
9239
10460
  if (config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config) !== "none") {
9240
- const localState = await resolveLocalIntegrationContext(config, feature);
10461
+ const localState = resolvedLocalState || await resolveLocalIntegrationContext(config, feature);
9241
10462
  const localVerifyCommand = `npx lee-spec-kit local verify ${buildFeatureArgs(feature)} --json`;
9242
10463
  const localMergeBaseCommand = `npx lee-spec-kit local merge ${buildFeatureArgs(feature)} --json`;
9243
10464
  if (localState.cleanedIntegrationStillValid) {