lee-spec-kit 0.6.22 → 0.6.24

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
@@ -206,8 +206,10 @@ var ko = {
206
206
  "context.list.recordPrLink": "PR \uB9C1\uD06C \uAE30\uB85D",
207
207
  "context.list.addPrePrReviewField": "Pre-PR Review \uD544\uB4DC \uCD94\uAC00",
208
208
  "context.list.completePrePrReview": "Pre-PR \uB9AC\uBDF0 \uC644\uB8CC \uCC98\uB9AC",
209
+ "context.list.addPrePrFindings": "Pre-PR Findings \uAE30\uB85D",
209
210
  "context.list.addPrePrEvidence": "Pre-PR Evidence \uADFC\uAC70 \uCD94\uAC00",
210
211
  "context.list.addPrePrDecision": "Pre-PR Decision \uAE30\uB85D",
212
+ "context.list.resolvePrePrDecision": "Pre-PR Decision\uC744 approve\uB85C \uC815\uB9AC",
211
213
  "context.list.addPrReviewEvidence": "PR \uB9AC\uBDF0 Evidence \uC694\uC57D \uCD94\uAC00",
212
214
  "context.list.addPrReviewDecision": "PR \uB9AC\uBDF0 Decision \uAE30\uB85D",
213
215
  "context.list.setPrStatus": "PR \uC0C1\uD0DC \uC124\uC815",
@@ -487,7 +489,7 @@ var ko = {
487
489
  reviewFixCommitGuidance: "PR \uB9AC\uBDF0 \uC218\uC815 \uCEE4\uBC0B\uC744 \uC9C4\uD589\uD558\uC138\uC694. \uB9AC\uBDF0 \uBC18\uC601 \uD30C\uC77C\uB9CC \uC2A4\uD14C\uC774\uC9D5\uD55C \uB4A4 `fix(review): <review-fix-summary>` \uD615\uC2DD\uC73C\uB85C \uCEE4\uBC0B\uD558\uC138\uC694. `<review-fix-summary>`\uC5D0\uB294 \uC774\uBC88 \uCEE4\uBC0B\uC5D0\uC11C \uC2E4\uC81C\uB85C \uD574\uACB0\uD55C \uB9AC\uBDF0 \uD56D\uBAA9 \uC694\uC57D\uC744 \uC791\uC131\uD558\uC138\uC694. (\uD0DC\uC2A4\uD06C \uC81C\uBAA9 \uC7AC\uC0AC\uC6A9 \uAE08\uC9C0)",
488
490
  standaloneNeedsProjectRoot: "standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)",
489
491
  createBranch: 'cd "{projectGitCwd}" && mkdir -p .worktrees && (git worktree add ".worktrees/feat-{issueNumber}-{slug}" "feat/{issueNumber}-{slug}" || git worktree add -b "feat/{issueNumber}-{slug}" ".worktrees/feat-{issueNumber}-{slug}") && echo "worktree: {projectGitCwd}/.worktrees/feat-{issueNumber}-{slug}"',
490
- worktreeCleanupCommand: 'cd "{projectGitCwd}" && git worktree remove "{worktreePath}" && git worktree prune',
492
+ worktreeCleanupCommand: 'cd "{projectGitCwd}" && git worktree remove "{worktreePath}" && git worktree prune && CURRENT_BRANCH=$(git branch --show-current) && DEFAULT_BRANCH=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | cut -d/ -f2-) && TARGET_BRANCH="${DEFAULT_BRANCH:-$CURRENT_BRANCH}" && if [ -n "$TARGET_BRANCH" ]; then git checkout "$TARGET_BRANCH" >/dev/null 2>&1 || true; fi && if git rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1 && [ -z "$(git status --porcelain)" ]; then git pull --ff-only || true; fi',
491
493
  tasksAllDoneButNoChecklist: '\uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8\uB97C \uC791\uC131\uD558\uC138\uC694. tasks.md\uC758 "\uC644\uB8CC \uC870\uAC74" \uC139\uC158\uC5D0 \uAC80\uC99D \uD56D\uBAA9\uC744 \uCD94\uAC00\uD558\uACE0, \uC0AC\uC6A9\uC790\uC640 \uD655\uC778 \uD6C4 \uCDA9\uC871 \uD56D\uBAA9\uC744 [x]\uB85C \uCCB4\uD06C\uD558\uC138\uC694. \uCD5C\uC885 \uC2B9\uC778(OK)\uB3C4 \uBC18\uC601\uD558\uC138\uC694.',
492
494
  tasksAllDoneButChecklist: "\uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8\uC758 \uB0A8\uC740 \uD56D\uBAA9\uC744 \uC9C4\uD589\uD558\uC138\uC694. \uD604\uC7AC \uC9C4\uD589: ({checked}/{total}) \uC0AC\uC6A9\uC790\uC640 \uD655\uC778 \uD6C4 \uCDA9\uC871 \uD56D\uBAA9\uC744 [x]\uB85C \uCCB4\uD06C\uD558\uACE0 \uCD5C\uC885 \uC2B9\uC778(OK)\uC744 \uBC18\uC601\uD558\uC138\uC694.",
493
495
  finishDoingTask: '\uD604\uC7AC DOING/REVIEW \uD0DC\uC2A4\uD06C\uB97C \uC218\uD589\uD558\uC138\uC694: "{title}" ({done}/{total}) \uC644\uB8CC \uC2DC \uACB0\uACFC/\uAC80\uC99D\uC744 \uACF5\uC720\uD558\uACE0 \uC2B9\uC778(`A` \uB610\uB294 `A OK` \uD615\uC2DD)\uC744 \uBC1B\uC740 \uB4A4 DONE \uCC98\uB9AC',
@@ -542,6 +544,7 @@ var ko = {
542
544
  legacyTasksDocStatusField: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. `\uBB38\uC11C \uC0C1\uD0DC` \uD544\uB4DC(Draft/Review/Approved)\uB97C \uCD94\uAC00\uD574 \uD0DC\uC2A4\uD06C \uC2B9\uC778 \uB2E8\uACC4\uB97C \uD65C\uC131\uD654\uD558\uC138\uC694.",
543
545
  legacyTasksPrFields: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. PR \uB2E8\uACC4 \uC804\uC5D0 `PR` \uBC0F `PR \uC0C1\uD0DC` \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694.",
544
546
  legacyTasksPrePrReviewField: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. PR \uB2E8\uACC4 \uC804\uC5D0 `PR \uC804 \uB9AC\uBDF0` \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694. (`- **PR \uC804 \uB9AC\uBDF0**: Pending | Done`)",
547
+ legacyTasksPrePrFindingsField: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. PR \uB2E8\uACC4 \uC804\uC5D0 `PR \uC804 \uB9AC\uBDF0 Findings` \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694. (`- **PR \uC804 \uB9AC\uBDF0 Findings**: major=0, minor=0`)",
545
548
  legacyTasksPrePrEvidenceField: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. PR \uB2E8\uACC4 \uC804\uC5D0 `PR \uC804 \uB9AC\uBDF0 Evidence` \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694.",
546
549
  legacyTasksPrePrDecisionField: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. PR \uB2E8\uACC4 \uC804\uC5D0 `PR \uC804 \uB9AC\uBDF0 Decision` \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694. (`- **PR \uC804 \uB9AC\uBDF0 Decision**: \uACB0\uC815: ...`)",
547
550
  legacyTasksPrReviewEvidenceField: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. \uB9AC\uBDF0 \uB2E8\uACC4 \uC804\uC5D0 `PR \uB9AC\uBDF0 Evidence` \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694.",
@@ -560,8 +563,10 @@ var ko = {
560
563
  workflowPrRemoteChecksPending: "\uC6D0\uACA9 PR \uCCB4\uD06C \uB300\uAE30\uAC00 {count}\uAC74 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uCCB4\uD06C \uC644\uB8CC \uD6C4 \uB2E4\uC2DC \uD655\uC778\uD558\uC138\uC694.",
561
564
  workflowPrePrReviewMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC `PR \uC804 \uB9AC\uBDF0` \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. (tasks.md\uC5D0 `- **PR \uC804 \uB9AC\uBDF0**: Pending | Done`\uC744 \uCD94\uAC00\uD558\uC138\uC694.)",
562
565
  workflowPrePrReviewNotDone: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC `PR \uC804 \uB9AC\uBDF0`\uAC00 Done\uC774 \uC544\uB2D9\uB2C8\uB2E4. (\uC0AC\uC804 \uCF54\uB4DC\uB9AC\uBDF0 \uD6C4 Done\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.)",
563
- workflowPrePrEvidenceMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC `PR \uC804 \uB9AC\uBDF0 Evidence`\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. (\uB9AC\uBDF0 \uADFC\uAC70\uB97C \uAE30\uB85D\uD558\uC138\uC694.)",
564
- workflowPrePrDecisionMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC `PR \uC804 \uB9AC\uBDF0 Decision`\uC774 \uBE44\uC5B4\uC788\uAC70\uB098 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (`\uACB0\uC815: ...` \uB610\uB294 `decision: ...` \uD615\uC2DD)"
566
+ workflowPrePrFindingsMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC `PR \uC804 \uB9AC\uBDF0 Findings`\uAC00 \uC5C6\uAC70\uB098 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (`major=<n>, minor=<n>` \uD615\uC2DD\uC73C\uB85C \uAE30\uB85D)",
567
+ workflowPrePrEvidenceMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC `PR \uC804 \uB9AC\uBDF0 Evidence`\uAC00 \uBE44\uC5B4\uC788\uAC70\uB098 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (path_required \uC815\uCC45\uC774\uBA74 \uC2E4\uC81C \uC874\uC7AC\uD558\uB294 \uACBD\uB85C\uB97C \uAE30\uB85D\uD558\uC138\uC694.)",
568
+ workflowPrePrDecisionMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC `PR \uC804 \uB9AC\uBDF0 Decision`\uC774 \uBE44\uC5B4\uC788\uAC70\uB098 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (`decision: approve|changes_requested|blocked ...` \uD615\uC2DD)",
569
+ workflowPrePrDecisionNotApproved: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC `PR \uC804 \uB9AC\uBDF0 Decision`\uC774 `{outcome}`\uC785\uB2C8\uB2E4. Findings\uB97C \uD574\uACB0\uD558\uACE0 pre-pr-review\uB97C \uC7AC\uC2E4\uD589\uD574 `approve`\uB85C \uB9DE\uCD94\uC138\uC694."
565
570
  }
566
571
  };
567
572
  var ko_default = ko;
@@ -688,8 +693,10 @@ var en = {
688
693
  "context.list.recordPrLink": "Record PR link",
689
694
  "context.list.addPrePrReviewField": "Add Pre-PR Review field",
690
695
  "context.list.completePrePrReview": "Complete Pre-PR review",
696
+ "context.list.addPrePrFindings": "Record Pre-PR Findings",
691
697
  "context.list.addPrePrEvidence": "Add Pre-PR Evidence",
692
698
  "context.list.addPrePrDecision": "Add Pre-PR Decision",
699
+ "context.list.resolvePrePrDecision": "Resolve Pre-PR decision to approve",
693
700
  "context.list.addPrReviewEvidence": "Add PR Review Evidence summary",
694
701
  "context.list.addPrReviewDecision": "Add PR Review Decision",
695
702
  "context.list.setPrStatus": "Set PR Status",
@@ -969,7 +976,7 @@ var en = {
969
976
  reviewFixCommitGuidance: "Commit PR review fixes. Stage only review-fix files, then commit with `fix(review): <review-fix-summary>`. `<review-fix-summary>` must describe review comments resolved in this commit (do not reuse task titles).",
970
977
  standaloneNeedsProjectRoot: "Standalone mode requires projectRoot. (npx lee-spec-kit config --project-root ...)",
971
978
  createBranch: 'cd "{projectGitCwd}" && mkdir -p .worktrees && (git worktree add ".worktrees/feat-{issueNumber}-{slug}" "feat/{issueNumber}-{slug}" || git worktree add -b "feat/{issueNumber}-{slug}" ".worktrees/feat-{issueNumber}-{slug}") && echo "worktree: {projectGitCwd}/.worktrees/feat-{issueNumber}-{slug}"',
972
- worktreeCleanupCommand: 'cd "{projectGitCwd}" && git worktree remove "{worktreePath}" && git worktree prune',
979
+ worktreeCleanupCommand: 'cd "{projectGitCwd}" && git worktree remove "{worktreePath}" && git worktree prune && CURRENT_BRANCH=$(git branch --show-current) && DEFAULT_BRANCH=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | cut -d/ -f2-) && TARGET_BRANCH="${DEFAULT_BRANCH:-$CURRENT_BRANCH}" && if [ -n "$TARGET_BRANCH" ]; then git checkout "$TARGET_BRANCH" >/dev/null 2>&1 || true; fi && if git rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1 && [ -z "$(git status --porcelain)" ]; then git pull --ff-only || true; fi',
973
980
  tasksAllDoneButNoChecklist: 'Create the completion checklist. Add verification items to the tasks.md "Completion Criteria" section, then mark satisfied items as [x] after user confirmation. Record final approval (OK) as well.',
974
981
  tasksAllDoneButChecklist: "Proceed with remaining completion checklist items. Current progress: ({checked}/{total}) Mark items as [x] only after user confirmation and real verification. Record final approval (OK) as well.",
975
982
  finishDoingTask: 'Continue working on the current DOING/REVIEW task: "{title}" ({done}/{total}) After it is complete, share outcome/verification and get approval before marking DONE',
@@ -1024,6 +1031,7 @@ var en = {
1024
1031
  legacyTasksDocStatusField: "Legacy tasks.md format detected. Add a `Doc Status` field (Draft/Review/Approved) to enable tasks approval.",
1025
1032
  legacyTasksPrFields: "Legacy tasks.md format detected. Add `PR` and `PR Status` fields before PR steps.",
1026
1033
  legacyTasksPrePrReviewField: "Legacy tasks.md format detected. Add `Pre-PR Review` before PR steps. (`- **Pre-PR Review**: Pending | Done`)",
1034
+ legacyTasksPrePrFindingsField: "Legacy tasks.md format detected. Add `Pre-PR Findings` before PR steps. (`- **Pre-PR Findings**: major=0, minor=0`)",
1027
1035
  legacyTasksPrePrEvidenceField: "Legacy tasks.md format detected. Add `Pre-PR Evidence` before PR steps.",
1028
1036
  legacyTasksPrePrDecisionField: "Legacy tasks.md format detected. Add `Pre-PR Decision` before PR steps. (`- **Pre-PR Decision**: decision: ...`)",
1029
1037
  legacyTasksPrReviewEvidenceField: "Legacy tasks.md format detected. Add `PR Review Evidence` before review iteration.",
@@ -1042,8 +1050,10 @@ var en = {
1042
1050
  workflowPrRemoteChecksPending: "Remote PR has {count} pending check(s). Wait for checks to complete, then re-check.",
1043
1051
  workflowPrePrReviewMissing: "Implementation is done but `Pre-PR Review` is missing. (Add `- **Pre-PR Review**: Pending | Done` in tasks.md.)",
1044
1052
  workflowPrePrReviewNotDone: "Implementation is done but `Pre-PR Review` is not Done. (Run pre-PR review, then update it to Done.)",
1045
- workflowPrePrEvidenceMissing: "Implementation is done but `Pre-PR Evidence` is empty. (Record review evidence.)",
1046
- workflowPrePrDecisionMissing: "Implementation is done but `Pre-PR Decision` is empty/invalid. (Use `decision: ...` or `\uACB0\uC815: ...`.)"
1053
+ workflowPrePrFindingsMissing: "Implementation is done but `Pre-PR Findings` is missing/invalid. (Use `major=<n>, minor=<n>`.)",
1054
+ workflowPrePrEvidenceMissing: "Implementation is done but `Pre-PR Evidence` is empty/invalid. (Record a real existing path when path_required policy is enabled.)",
1055
+ workflowPrePrDecisionMissing: "Implementation is done but `Pre-PR Decision` is empty/invalid. (Use `decision: approve|changes_requested|blocked ...`.)",
1056
+ workflowPrePrDecisionNotApproved: "Implementation is done but `Pre-PR Decision` is `{outcome}`. Resolve findings and re-run pre-PR review until decision becomes `approve`."
1047
1057
  }
1048
1058
  };
1049
1059
  var en_default = en;
@@ -1066,7 +1076,10 @@ var I18N = {
1066
1076
  };
1067
1077
  function tr(lang, category, key, vars = {}) {
1068
1078
  const safeLang = normalizeLang(lang);
1069
- const template = I18N[safeLang]?.[category]?.[key] ?? I18N[DEFAULT_LANG]?.[category]?.[key] ?? I18N.ko?.[category]?.[key] ?? `${category}.${key}`;
1079
+ const safeCategory = I18N[safeLang]?.[category];
1080
+ const defaultCategory = I18N[DEFAULT_LANG]?.[category];
1081
+ const koCategory = I18N.ko?.[category];
1082
+ const template = safeCategory?.[key] ?? defaultCategory?.[key] ?? koCategory?.[key] ?? `${category}.${key}`;
1070
1083
  return formatTemplate(template, vars);
1071
1084
  }
1072
1085
 
@@ -2259,7 +2272,10 @@ async function runInit(options) {
2259
2272
  },
2260
2273
  prePrReview: {
2261
2274
  skills: ["code-review-excellence"],
2262
- minorPolicy: "warn"
2275
+ fallback: "builtin-checklist",
2276
+ evidenceMode: "path_required",
2277
+ findings: "required",
2278
+ decisionEnum: ["approve", "changes_requested", "blocked"]
2263
2279
  }
2264
2280
  },
2265
2281
  pr: {
@@ -2878,6 +2894,11 @@ var ACTION_CATEGORIES = [
2878
2894
 
2879
2895
  // src/utils/workflow.ts
2880
2896
  var DEFAULT_PRE_PR_REVIEW_SKILLS = ["code-review-excellence"];
2897
+ var DEFAULT_PRE_PR_DECISION_ENUM = [
2898
+ "approve",
2899
+ "changes_requested",
2900
+ "blocked"
2901
+ ];
2881
2902
  function resolveWorkflowPolicy(workflow) {
2882
2903
  const mode = workflow?.mode === "local" ? "local" : "github";
2883
2904
  const policy = mode === "local" ? {
@@ -2939,17 +2960,42 @@ function normalizeSkillList(input) {
2939
2960
  }
2940
2961
  return [...deduped];
2941
2962
  }
2963
+ function normalizeDecisionEnumList(input) {
2964
+ if (!Array.isArray(input)) return [];
2965
+ const deduped = /* @__PURE__ */ new Set();
2966
+ for (const raw of input) {
2967
+ const value = String(raw || "").trim().toLowerCase();
2968
+ if (!value) continue;
2969
+ if (value === "approve") {
2970
+ deduped.add("approve");
2971
+ continue;
2972
+ }
2973
+ if (value === "changes_requested") {
2974
+ deduped.add("changes_requested");
2975
+ continue;
2976
+ }
2977
+ if (value === "blocked") {
2978
+ deduped.add("blocked");
2979
+ continue;
2980
+ }
2981
+ }
2982
+ return [...deduped];
2983
+ }
2942
2984
  function resolvePrePrReviewPolicy(workflow) {
2943
2985
  const workflowPolicy = resolveWorkflowPolicy(workflow);
2944
2986
  const configured = workflow?.prePrReview;
2945
2987
  const configuredSkills = normalizeSkillList(configured?.skills);
2988
+ const configuredDecisionEnum = normalizeDecisionEnumList(
2989
+ configured?.decisionEnum
2990
+ );
2946
2991
  const configuredEnabled = typeof configured?.enabled === "boolean" ? configured.enabled : workflowPolicy.requirePr;
2947
2992
  return {
2948
2993
  enabled: workflowPolicy.requirePr ? configuredEnabled : false,
2949
2994
  skills: configuredSkills.length > 0 ? configuredSkills : DEFAULT_PRE_PR_REVIEW_SKILLS,
2950
2995
  fallback: configured?.fallback === "builtin-checklist" ? configured.fallback : "builtin-checklist",
2951
- blockOnFindings: typeof configured?.blockOnFindings === "boolean" ? configured.blockOnFindings : true,
2952
- minorPolicy: configured?.minorPolicy === "block" ? "block" : "warn"
2996
+ evidenceMode: configured?.evidenceMode === "any" ? "any" : "path_required",
2997
+ findings: configured?.findings === "optional" ? "optional" : "required",
2998
+ decisionEnum: configuredDecisionEnum.length > 0 ? configuredDecisionEnum : DEFAULT_PRE_PR_DECISION_ENUM
2953
2999
  };
2954
3000
  }
2955
3001
 
@@ -2977,17 +3023,20 @@ function isPrePrReviewSatisfied(feature, prePrReviewPolicy) {
2977
3023
  if (!feature.docs.prePrEvidenceFieldExists || !feature.prePrReview.evidenceProvided) {
2978
3024
  return false;
2979
3025
  }
3026
+ if (prePrReviewPolicy.findings === "required" && (!feature.docs.prePrFindingsFieldExists || !feature.prePrReview.findingsProvided)) {
3027
+ return false;
3028
+ }
2980
3029
  if (!feature.docs.prePrDecisionFieldExists || !feature.prePrReview.decisionProvided) {
2981
3030
  return false;
2982
3031
  }
3032
+ if (feature.prePrReview.decisionOutcome !== "approve") {
3033
+ return false;
3034
+ }
2983
3035
  return true;
2984
3036
  }
2985
3037
  function isFeatureDone(feature, workflowPolicy, prePrReviewPolicy) {
2986
3038
  return feature.specStatus === "Approved" && feature.planStatus === "Approved" && !feature.git.docsHasUncommittedChanges && !feature.git.projectHasUncommittedChanges && feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature) && isTasksDocApproved(feature) && (!workflowPolicy.requireIssue || !!feature.issueNumber) && (!workflowPolicy.requirePr || isPrMetadataConfigured(feature) && !!feature.pr.link) && (!workflowPolicy.requireReview || feature.pr.status === "Approved") && isPrePrReviewSatisfied(feature, prePrReviewPolicy);
2987
3039
  }
2988
- function formatSkillList(skills) {
2989
- return skills.join(", ");
2990
- }
2991
3040
  function getPrReviewRemoteBlockReasons(feature, lang) {
2992
3041
  const remote = feature.pr.remote;
2993
3042
  if (!remote || !remote.available) return [];
@@ -3024,6 +3073,14 @@ function getPrReviewRemoteBlockReasons(feature, lang) {
3024
3073
  function normalizeCommitTopicText(value) {
3025
3074
  return value.replace(/\s+/g, " ").trim();
3026
3075
  }
3076
+ function toShellArg(value) {
3077
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$").replace(/`/g, "\\`")}"`;
3078
+ }
3079
+ function buildSelfCliCommand(args) {
3080
+ const entry = process.argv[1] || "dist/index.js";
3081
+ const base = [process.execPath, entry, "--no-banner", ...args];
3082
+ return base.map((arg) => toShellArg(arg)).join(" ");
3083
+ }
3027
3084
  function toShellSafeCommitTopic(value) {
3028
3085
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$").replace(/`/g, "\\`");
3029
3086
  }
@@ -3792,74 +3849,19 @@ ${tr(lang, "messages", "taskCommitGateWarnProceed", {
3792
3849
  }
3793
3850
  ];
3794
3851
  }
3795
- if (f.prePrReview.status !== "Done") {
3796
- if (!prePrReviewPolicy.skills.length) {
3797
- return [
3798
- {
3799
- type: "instruction",
3800
- category: "pre_pr_review",
3801
- requiresUserCheck: true,
3802
- message: tr(lang, "messages", "prePrReviewRun", {
3803
- skills: "code-review-excellence",
3804
- fallback: prePrReviewPolicy.fallback
3805
- })
3806
- }
3807
- ];
3808
- }
3809
- return [
3810
- {
3811
- type: "instruction",
3812
- category: "pre_pr_review",
3813
- requiresUserCheck: true,
3814
- message: tr(lang, "messages", "prePrReviewRun", {
3815
- skills: formatSkillList(prePrReviewPolicy.skills),
3816
- fallback: prePrReviewPolicy.fallback
3817
- })
3818
- }
3819
- ];
3820
- }
3821
- if (!f.docs.prePrEvidenceFieldExists || !f.prePrReview.evidenceProvided) {
3822
- return [
3823
- {
3824
- type: "instruction",
3825
- category: "pre_pr_review",
3826
- requiresUserCheck: true,
3827
- message: tr(lang, "messages", "prePrReviewEvidenceMissing")
3828
- }
3829
- ];
3830
- }
3831
- if (!f.docs.prePrDecisionFieldExists || !f.prePrReview.decisionProvided) {
3832
- return [
3833
- {
3834
- type: "instruction",
3835
- category: "pre_pr_review",
3836
- requiresUserCheck: true,
3837
- message: tr(lang, "messages", "prePrReviewDecisionMissing")
3838
- }
3839
- ];
3840
- }
3841
- if (!prePrReviewPolicy.skills.length) {
3842
- return [
3843
- {
3844
- type: "instruction",
3845
- category: "pre_pr_review",
3846
- requiresUserCheck: true,
3847
- message: tr(lang, "messages", "prePrReviewRun", {
3848
- skills: "code-review-excellence",
3849
- fallback: prePrReviewPolicy.fallback
3850
- })
3851
- }
3852
- ];
3852
+ const commandArgs = ["pre-pr-review", f.folderName];
3853
+ if (f.type && f.type !== "single") {
3854
+ commandArgs.push("--component", f.type);
3853
3855
  }
3854
3856
  return [
3855
3857
  {
3856
- type: "instruction",
3858
+ type: "command",
3857
3859
  category: "pre_pr_review",
3860
+ operationType: "local",
3858
3861
  requiresUserCheck: true,
3859
- message: tr(lang, "messages", "prePrReviewRun", {
3860
- skills: formatSkillList(prePrReviewPolicy.skills),
3861
- fallback: prePrReviewPolicy.fallback
3862
- })
3862
+ scope: "docs",
3863
+ cwd: f.git.docsGitCwd,
3864
+ cmd: buildSelfCliCommand(commandArgs)
3863
3865
  }
3864
3866
  ];
3865
3867
  }
@@ -4556,6 +4558,52 @@ function hasStructuredReviewDecision(value) {
4556
4558
  if (!trimmed) return false;
4557
4559
  return /^(?:decision|결정)\s*[::]\s*\S.+$/i.test(trimmed);
4558
4560
  }
4561
+ function parsePrePrDecisionOutcome(value) {
4562
+ if (!value) return void 0;
4563
+ const trimmed = value.trim();
4564
+ if (!trimmed) return void 0;
4565
+ const structured = trimmed.match(/^(?:decision|결정)\s*[::]\s*(.+)$/i);
4566
+ if (!structured) return void 0;
4567
+ const payload = structured[1].trim().toLowerCase();
4568
+ const normalized = payload.split(/[,\s-]+/)[0]?.replace(/[^a-z_]/g, "");
4569
+ if (!normalized) return void 0;
4570
+ if (normalized === "approve" || normalized === "approved") {
4571
+ return "approve";
4572
+ }
4573
+ if (normalized === "changes_requested" || normalized === "changes" || normalized === "change") {
4574
+ return "changes_requested";
4575
+ }
4576
+ if (normalized === "blocked" || normalized === "block") {
4577
+ return "blocked";
4578
+ }
4579
+ return void 0;
4580
+ }
4581
+ function resolveEvidencePathValue(value) {
4582
+ const trimmed = value.trim();
4583
+ const mdLink = trimmed.match(/\(([^)]+)\)/);
4584
+ if (mdLink && mdLink[1]) return mdLink[1].trim();
4585
+ return trimmed.split(/\s+/)[0] || "";
4586
+ }
4587
+ async function isPrePrEvidenceProvided(rawValue, policy, context) {
4588
+ if (isPlaceholderReviewEvidence(rawValue)) return false;
4589
+ if (policy.evidenceMode !== "path_required") return true;
4590
+ if (!rawValue) return false;
4591
+ const evidencePath = resolveEvidencePathValue(rawValue);
4592
+ if (!evidencePath) return false;
4593
+ if (/^https?:\/\//i.test(evidencePath)) return false;
4594
+ const candidates = /* @__PURE__ */ new Set();
4595
+ if (path22.isAbsolute(evidencePath)) {
4596
+ candidates.add(path22.resolve(evidencePath));
4597
+ } else {
4598
+ candidates.add(path22.resolve(context.featurePath, evidencePath));
4599
+ candidates.add(path22.resolve(context.docsDir, evidencePath));
4600
+ candidates.add(path22.resolve(path22.dirname(context.docsDir), evidencePath));
4601
+ }
4602
+ for (const candidate of candidates) {
4603
+ if (await fs16.pathExists(candidate)) return true;
4604
+ }
4605
+ return false;
4606
+ }
4559
4607
  function parseIssueNumber(value) {
4560
4608
  if (!value) return void 0;
4561
4609
  const match = value.match(/#?(\d+)/);
@@ -4848,9 +4896,15 @@ function isPrePrReviewSatisfied2(feature, policy) {
4848
4896
  if (!feature.docs.prePrEvidenceFieldExists || !feature.prePrReview.evidenceProvided) {
4849
4897
  return false;
4850
4898
  }
4899
+ if (policy.findings === "required" && (!feature.docs.prePrFindingsFieldExists || !feature.prePrReview.findingsProvided)) {
4900
+ return false;
4901
+ }
4851
4902
  if (!feature.docs.prePrDecisionFieldExists || !feature.prePrReview.decisionProvided) {
4852
4903
  return false;
4853
4904
  }
4905
+ if (feature.prePrReview.decisionOutcome !== "approve") {
4906
+ return false;
4907
+ }
4854
4908
  return true;
4855
4909
  }
4856
4910
  async function parseFeature(featurePath, type, context, options) {
@@ -4917,9 +4971,11 @@ async function parseFeature(featurePath, type, context, options) {
4917
4971
  let completionChecklist;
4918
4972
  let prePrReviewStatus;
4919
4973
  let prePrFindings;
4974
+ let prePrFindingsProvided = false;
4920
4975
  let prePrEvidence;
4921
4976
  let prePrEvidenceProvided = false;
4922
4977
  let prePrDecision;
4978
+ let prePrDecisionOutcome;
4923
4979
  let prePrDecisionProvided = false;
4924
4980
  let prReviewFindings;
4925
4981
  let prReviewEvidence;
@@ -4994,6 +5050,7 @@ async function parseFeature(featurePath, type, context, options) {
4994
5050
  "Pre-PR Findings"
4995
5051
  ]);
4996
5052
  prePrFindings = parseReviewFindings(prePrFindingsValue);
5053
+ prePrFindingsProvided = !!prePrFindings;
4997
5054
  const prePrEvidenceValue = extractFirstSpecValue(content, [
4998
5055
  "PR \uC804 \uB9AC\uBDF0 Evidence",
4999
5056
  "Pre-PR Evidence"
@@ -5003,7 +5060,11 @@ async function parseFeature(featurePath, type, context, options) {
5003
5060
  "Pre-PR Evidence"
5004
5061
  ]);
5005
5062
  prePrEvidence = prePrEvidenceValue?.trim();
5006
- prePrEvidenceProvided = !isPlaceholderReviewEvidence(prePrEvidenceValue);
5063
+ prePrEvidenceProvided = await isPrePrEvidenceProvided(
5064
+ prePrEvidenceValue,
5065
+ prePrReviewPolicy,
5066
+ { featurePath, docsDir: context.docsDir }
5067
+ );
5007
5068
  const prePrDecisionValue = extractFirstSpecValue(content, [
5008
5069
  "PR \uC804 \uB9AC\uBDF0 Decision",
5009
5070
  "Pre-PR Decision"
@@ -5013,7 +5074,8 @@ async function parseFeature(featurePath, type, context, options) {
5013
5074
  "Pre-PR Decision"
5014
5075
  ]);
5015
5076
  prePrDecision = prePrDecisionValue?.trim();
5016
- prePrDecisionProvided = !isPlaceholderReviewEvidence(prePrDecisionValue) && hasStructuredReviewDecision(prePrDecisionValue);
5077
+ prePrDecisionOutcome = parsePrePrDecisionOutcome(prePrDecisionValue);
5078
+ prePrDecisionProvided = !isPlaceholderReviewEvidence(prePrDecisionValue) && hasStructuredReviewDecision(prePrDecisionValue) && !!prePrDecisionOutcome && prePrReviewPolicy.decisionEnum.includes(prePrDecisionOutcome);
5017
5079
  const prReviewFindingsValue = extractFirstSpecValue(content, [
5018
5080
  "PR \uB9AC\uBDF0 Findings",
5019
5081
  "PR Review Findings"
@@ -5188,6 +5250,9 @@ async function parseFeature(featurePath, type, context, options) {
5188
5250
  if (tasksExists && prePrReviewPolicy.enabled && !prePrReviewFieldExists) {
5189
5251
  warnings.push(tr(lang, "warnings", "legacyTasksPrePrReviewField"));
5190
5252
  }
5253
+ if (tasksExists && prePrReviewPolicy.enabled && prePrReviewPolicy.findings === "required" && !prePrFindingsFieldExists) {
5254
+ warnings.push(tr(lang, "warnings", "legacyTasksPrePrFindingsField"));
5255
+ }
5191
5256
  if (tasksExists && prePrReviewPolicy.enabled && !prePrEvidenceFieldExists) {
5192
5257
  warnings.push(tr(lang, "warnings", "legacyTasksPrePrEvidenceField"));
5193
5258
  }
@@ -5232,12 +5297,15 @@ async function parseFeature(featurePath, type, context, options) {
5232
5297
  {
5233
5298
  docs: {
5234
5299
  prePrReviewFieldExists,
5300
+ prePrFindingsFieldExists,
5235
5301
  prePrEvidenceFieldExists,
5236
5302
  prePrDecisionFieldExists
5237
5303
  },
5238
5304
  prePrReview: {
5239
5305
  status: prePrReviewStatus,
5306
+ findingsProvided: prePrFindingsProvided,
5240
5307
  evidenceProvided: prePrEvidenceProvided,
5308
+ decisionOutcome: prePrDecisionOutcome,
5241
5309
  decisionProvided: prePrDecisionProvided
5242
5310
  }
5243
5311
  },
@@ -5278,10 +5346,18 @@ async function parseFeature(featurePath, type, context, options) {
5278
5346
  warnings.push(tr(lang, "warnings", "workflowPrePrReviewMissing"));
5279
5347
  } else if (prePrReviewStatus !== "Done") {
5280
5348
  warnings.push(tr(lang, "warnings", "workflowPrePrReviewNotDone"));
5349
+ } else if (prePrReviewPolicy.findings === "required" && (!prePrFindingsFieldExists || !prePrFindingsProvided)) {
5350
+ warnings.push(tr(lang, "warnings", "workflowPrePrFindingsMissing"));
5281
5351
  } else if (!prePrEvidenceFieldExists || !prePrEvidenceProvided) {
5282
5352
  warnings.push(tr(lang, "warnings", "workflowPrePrEvidenceMissing"));
5283
5353
  } else if (!prePrDecisionFieldExists || !prePrDecisionProvided) {
5284
5354
  warnings.push(tr(lang, "warnings", "workflowPrePrDecisionMissing"));
5355
+ } else if (prePrDecisionOutcome !== "approve") {
5356
+ warnings.push(
5357
+ tr(lang, "warnings", "workflowPrePrDecisionNotApproved", {
5358
+ outcome: prePrDecisionOutcome || "-"
5359
+ })
5360
+ );
5285
5361
  }
5286
5362
  }
5287
5363
  }
@@ -5307,9 +5383,11 @@ async function parseFeature(featurePath, type, context, options) {
5307
5383
  prePrReview: {
5308
5384
  status: prePrReviewStatus,
5309
5385
  findings: prePrFindings,
5386
+ findingsProvided: prePrFindingsProvided,
5310
5387
  evidence: prePrEvidence,
5311
5388
  evidenceProvided: prePrEvidenceProvided,
5312
5389
  decision: prePrDecision,
5390
+ decisionOutcome: prePrDecisionOutcome,
5313
5391
  decisionProvided: prePrDecisionProvided
5314
5392
  },
5315
5393
  prReview: {
@@ -5867,6 +5945,18 @@ function normalizeSkillList2(raw) {
5867
5945
  }
5868
5946
  return [...deduped];
5869
5947
  }
5948
+ function normalizeDecisionEnumList2(raw) {
5949
+ if (!Array.isArray(raw)) return [];
5950
+ const deduped = /* @__PURE__ */ new Set();
5951
+ for (const item of raw) {
5952
+ const value = String(item || "").trim().toLowerCase();
5953
+ if (!value) continue;
5954
+ if (value === "approve" || value === "changes_requested" || value === "blocked") {
5955
+ deduped.add(value);
5956
+ }
5957
+ }
5958
+ return [...deduped];
5959
+ }
5870
5960
  async function backfillMissingConfigDefaults(docsDir) {
5871
5961
  const configPath = path22.join(docsDir, ".lee-spec-kit.json");
5872
5962
  if (!await fs16.pathExists(configPath)) {
@@ -5915,8 +6005,39 @@ async function backfillMissingConfigDefaults(docsDir) {
5915
6005
  }
5916
6006
  }
5917
6007
  setIfMissing(prePrReview, "fallback", "builtin-checklist", "workflow.prePrReview.fallback");
5918
- setIfMissing(prePrReview, "blockOnFindings", true, "workflow.prePrReview.blockOnFindings");
5919
- setIfMissing(prePrReview, "minorPolicy", "warn", "workflow.prePrReview.minorPolicy");
6008
+ setIfMissing(
6009
+ prePrReview,
6010
+ "evidenceMode",
6011
+ "path_required",
6012
+ "workflow.prePrReview.evidenceMode"
6013
+ );
6014
+ if (prePrReview.evidenceMode !== void 0 && prePrReview.evidenceMode !== "path_required" && prePrReview.evidenceMode !== "any") {
6015
+ prePrReview.evidenceMode = "path_required";
6016
+ changedPaths.push("workflow.prePrReview.evidenceMode");
6017
+ }
6018
+ setIfMissing(
6019
+ prePrReview,
6020
+ "findings",
6021
+ "required",
6022
+ "workflow.prePrReview.findings"
6023
+ );
6024
+ if (prePrReview.findings !== void 0 && prePrReview.findings !== "required" && prePrReview.findings !== "optional") {
6025
+ prePrReview.findings = "required";
6026
+ changedPaths.push("workflow.prePrReview.findings");
6027
+ }
6028
+ if (prePrReview.decisionEnum === void 0) {
6029
+ prePrReview.decisionEnum = ["approve", "changes_requested", "blocked"];
6030
+ changedPaths.push("workflow.prePrReview.decisionEnum");
6031
+ } else {
6032
+ const normalizedDecisionEnum = normalizeDecisionEnumList2(prePrReview.decisionEnum);
6033
+ if (normalizedDecisionEnum.length === 0) {
6034
+ prePrReview.decisionEnum = ["approve", "changes_requested", "blocked"];
6035
+ changedPaths.push("workflow.prePrReview.decisionEnum");
6036
+ } else if (JSON.stringify(normalizedDecisionEnum) !== JSON.stringify(prePrReview.decisionEnum)) {
6037
+ prePrReview.decisionEnum = normalizedDecisionEnum;
6038
+ changedPaths.push("workflow.prePrReview.decisionEnum");
6039
+ }
6040
+ }
5920
6041
  if (!isPlainObject(raw.pr)) {
5921
6042
  raw.pr = {};
5922
6043
  changedPaths.push("pr");
@@ -6312,21 +6433,27 @@ function buildActionDetail(action, lang) {
6312
6433
  const branchMatch = command.match(/\bfeat\/([A-Za-z0-9._-]+)/);
6313
6434
  const worktree = worktreeMatch ? `.worktrees/${worktreeMatch[1]}` : null;
6314
6435
  const branch = branchMatch ? `feat/${branchMatch[1]}` : null;
6436
+ if (action.type !== "command") {
6437
+ return tr(lang, "cli", "context.commandDetail.branchCreateGeneric", {
6438
+ scope: "project"
6439
+ });
6440
+ }
6441
+ const scope = action.scope;
6315
6442
  if (worktree && branch) {
6316
6443
  return tr(lang, "cli", "context.commandDetail.branchCreateWithWorktree", {
6317
- scope: action.scope,
6444
+ scope,
6318
6445
  worktree,
6319
6446
  branch
6320
6447
  });
6321
6448
  }
6322
6449
  if (branch) {
6323
6450
  return tr(lang, "cli", "context.commandDetail.branchCreateWithBranch", {
6324
- scope: action.scope,
6451
+ scope,
6325
6452
  branch
6326
6453
  });
6327
6454
  }
6328
6455
  return tr(lang, "cli", "context.commandDetail.branchCreateGeneric", {
6329
- scope: action.scope
6456
+ scope
6330
6457
  });
6331
6458
  };
6332
6459
  const extractCommitMessage = (command) => {
@@ -7182,12 +7309,18 @@ function getListLabel(f, stepsMap, lang, workflowPolicy, prePrReviewPolicy) {
7182
7309
  if (prePrReviewPolicy.enabled && f.prePrReview.status !== "Done") {
7183
7310
  return tr(lang, "cli", "context.list.completePrePrReview");
7184
7311
  }
7312
+ if (prePrReviewPolicy.enabled && prePrReviewPolicy.findings === "required" && (!f.docs.prePrFindingsFieldExists || !f.prePrReview.findingsProvided)) {
7313
+ return tr(lang, "cli", "context.list.addPrePrFindings");
7314
+ }
7185
7315
  if (prePrReviewPolicy.enabled && (!f.docs.prePrEvidenceFieldExists || !f.prePrReview.evidenceProvided)) {
7186
7316
  return tr(lang, "cli", "context.list.addPrePrEvidence");
7187
7317
  }
7188
7318
  if (prePrReviewPolicy.enabled && (!f.docs.prePrDecisionFieldExists || !f.prePrReview.decisionProvided)) {
7189
7319
  return tr(lang, "cli", "context.list.addPrePrDecision");
7190
7320
  }
7321
+ if (prePrReviewPolicy.enabled && f.prePrReview.decisionOutcome !== "approve") {
7322
+ return tr(lang, "cli", "context.list.resolvePrePrDecision");
7323
+ }
7191
7324
  if (workflowPolicy.requirePr && !f.pr.link) {
7192
7325
  return tr(lang, "cli", "context.list.recordPrLink");
7193
7326
  }
@@ -7244,7 +7377,9 @@ function toCompactFeature(feature) {
7244
7377
  prePrReview: {
7245
7378
  status: feature.prePrReview.status,
7246
7379
  findings: feature.prePrReview.findings,
7380
+ findingsProvided: feature.prePrReview.findingsProvided,
7247
7381
  evidenceProvided: feature.prePrReview.evidenceProvided,
7382
+ decisionOutcome: feature.prePrReview.decisionOutcome,
7248
7383
  decisionProvided: feature.prePrReview.decisionProvided
7249
7384
  },
7250
7385
  prReview: {
@@ -7399,7 +7534,18 @@ async function runContext(featureName, options) {
7399
7534
  const jsonMode = !!options.json || !!options.jsonCompact;
7400
7535
  if (jsonMode) {
7401
7536
  const primaryAction = state.actionOptions[0] ?? null;
7402
- const finalApprovalPrompt = buildFinalApprovalPrompt(lang, state.actionOptions);
7537
+ const checkRequiredLabels = state.actionOptions.filter((option) => !!option.action.requiresUserCheck).map((option) => option.label);
7538
+ const checkRequiredCategories = [
7539
+ ...new Set(
7540
+ state.actionOptions.filter((option) => !!option.action.requiresUserCheck).map((option) => option.action.category || "uncategorized")
7541
+ )
7542
+ ];
7543
+ const approvalRequired = checkRequiredLabels.length > 0;
7544
+ const finalApprovalPrompt = approvalRequired ? buildFinalApprovalPrompt(lang, state.actionOptions) : "";
7545
+ const approvalUserFacingLines = approvalRequired ? [
7546
+ ...state.actionOptions.map((o) => o.approvalPrompt),
7547
+ finalApprovalPrompt
7548
+ ].filter((line) => line.length > 0) : [];
7403
7549
  const approveCommand = buildApprovalCommand(
7404
7550
  state,
7405
7551
  featureName,
@@ -7452,19 +7598,20 @@ async function runContext(featureName, options) {
7452
7598
  activeCategories,
7453
7599
  knownCategories: ACTION_CATEGORIES,
7454
7600
  uncategorizedLabels,
7601
+ checkRequiredLabels,
7602
+ checkRequiredCategories,
7603
+ approvalRequired,
7455
7604
  categoryPolicyGuidance: 'For approval.mode="category", match against `actionOptions[].category`.',
7456
- oneApprovalPerAction: true,
7605
+ oneApprovalPerAction: approvalRequired,
7457
7606
  requireFreshContext: true,
7458
7607
  contextVersion: state.contextVersion,
7459
7608
  config: config.approval ?? { mode: "builtin" }
7460
7609
  },
7461
7610
  approvalRequest: {
7611
+ required: approvalRequired,
7462
7612
  finalPrompt: finalApprovalPrompt,
7463
- userFacingLines: [
7464
- ...state.actionOptions.map((o) => o.approvalPrompt),
7465
- finalApprovalPrompt
7466
- ].filter((line) => line.length > 0),
7467
- labels: state.actionOptions.map((o) => o.label),
7613
+ userFacingLines: approvalUserFacingLines,
7614
+ labels: approvalRequired ? state.actionOptions.map((o) => o.label) : [],
7468
7615
  approveCommand,
7469
7616
  executeCommand,
7470
7617
  executeRequiresTicket: !!state.actionOptions[0]?.action?.requiresUserCheck
@@ -7523,27 +7670,28 @@ async function runContext(featureName, options) {
7523
7670
  activeCategories,
7524
7671
  knownCategories: ACTION_CATEGORIES,
7525
7672
  uncategorizedLabels,
7673
+ checkRequiredLabels,
7674
+ checkRequiredCategories,
7675
+ approvalRequired,
7526
7676
  categoryPolicyGuidance: 'For approval.mode="category", match against `actionOptions[].category`.',
7527
- requireExplanationBeforeApproval: true,
7528
- requiredExplanationFields: [
7677
+ requireExplanationBeforeApproval: approvalRequired,
7678
+ requiredExplanationFields: approvalRequired ? [
7529
7679
  "actionOptions[].label",
7530
7680
  "actionOptions[].detail",
7531
7681
  "actionOptions[].approvalPrompt"
7532
- ],
7682
+ ] : [],
7533
7683
  recommendation: "Before asking for approval, show only `actionOptions[].approvalPrompt` lines and `approvalRequest.finalPrompt` to the user. Keep `requiredDocs`, `checkPolicy`, and raw execution commands as internal guidance. For commit actions, include scope (`docs`/`project`) and commit message in the visible prompt. User replies should include the label token (e.g. `A`, `A OK`, `A proceed`, `A \uC9C4\uD589\uD574`). For command execution, prefer one-shot `npx lee-spec-kit flow <featureRef> --approve <LABEL> --execute` to avoid session mismatch after context compression/reset. Use ticket-based `context --execute --ticket` only when explicitly needed.",
7534
- oneApprovalPerAction: true,
7684
+ oneApprovalPerAction: approvalRequired,
7535
7685
  requireFreshContext: true,
7536
7686
  contextVersion: state.contextVersion,
7537
7687
  config: config.approval ?? { mode: "builtin" }
7538
7688
  },
7539
7689
  approvalRequest: {
7540
7690
  guidance: "User-facing output must include only approval prompts (`A: ...`) and `finalPrompt`. Do not expose `requiredDocs`, `checkPolicy`, or raw `cmd` unless explicitly requested. For approved command actions, prefer one-shot `flow --approve <LABEL> --execute`.",
7691
+ required: approvalRequired,
7541
7692
  finalPrompt: finalApprovalPrompt,
7542
- userFacingLines: [
7543
- ...state.actionOptions.map((o) => o.approvalPrompt),
7544
- finalApprovalPrompt
7545
- ].filter((line) => line.length > 0),
7546
- labels: state.actionOptions.map((o) => o.label),
7693
+ userFacingLines: approvalUserFacingLines,
7694
+ labels: approvalRequired ? state.actionOptions.map((o) => o.label) : [],
7547
7695
  approveCommand,
7548
7696
  executeCommand,
7549
7697
  executeRequiresTicket: !!state.actionOptions[0]?.action?.requiresUserCheck,
@@ -7823,7 +7971,7 @@ async function runContext(featureName, options) {
7823
7971
  );
7824
7972
  }
7825
7973
  }
7826
- if (actionOptions.length > 0) {
7974
+ if (actionOptions.length > 0 && hasCheckAction) {
7827
7975
  const finalApprovalPrompt = buildFinalApprovalPrompt(lang, actionOptions);
7828
7976
  const approveCommand = buildApprovalCommand(
7829
7977
  state,
@@ -7950,7 +8098,7 @@ async function runApprovedOption(state, config, lang, featureName, selectionOpti
7950
8098
  contextVersion: freshState.contextVersion,
7951
8099
  executable: selectedAction.type === "command",
7952
8100
  executeRequiresTicket,
7953
- oneApprovalPerAction: true,
8101
+ oneApprovalPerAction: executeRequiresTicket,
7954
8102
  approvalTicket: ticket ? {
7955
8103
  token: ticket.token,
7956
8104
  sessionId: ticket.sessionId,
@@ -9014,6 +9162,9 @@ function toAutoReasonCode(status) {
9014
9162
  return "AUTO_EXECUTION_FAILED";
9015
9163
  }
9016
9164
  }
9165
+ function isAutoRunFailureStatus(status) {
9166
+ return status === "manual_required" || status === "selection_required" || status === "no_progress" || status === "request_label_missing" || status === "request_failed" || status === "execution_failed";
9167
+ }
9017
9168
  async function runAutoUntilCategory(config, featureName, selectionOptions, untilCategories, requestText, metadata) {
9018
9169
  const contextArgs = ["context", ...buildSelectionArgs(featureName, selectionOptions)];
9019
9170
  const gateSet = new Set(untilCategories);
@@ -9422,9 +9573,10 @@ async function runFlow(featureName, options) {
9422
9573
  }
9423
9574
  }
9424
9575
  if (options.json) {
9576
+ const autoRunFailed = !!(autoRun && isAutoRunFailureStatus(autoRun.status));
9425
9577
  const payload = {
9426
- status: "ok",
9427
- reasonCode: "FLOW_SUMMARY",
9578
+ status: autoRunFailed ? "error" : "ok",
9579
+ reasonCode: autoRunFailed ? autoRun?.reasonCode || "AUTO_EXECUTION_FAILED" : "FLOW_SUMMARY",
9428
9580
  context: {
9429
9581
  before: {
9430
9582
  status: before.status,
@@ -9453,6 +9605,9 @@ async function runFlow(featureName, options) {
9453
9605
  suggestion: after.matchedFeature ? `npx lee-spec-kit context ${after.matchedFeature.folderName}${componentHint}` : `npx lee-spec-kit context${componentHint}`
9454
9606
  };
9455
9607
  console.log(JSON.stringify(payload, null, 2));
9608
+ if (autoRunFailed) {
9609
+ process.exitCode = 1;
9610
+ }
9456
9611
  return;
9457
9612
  }
9458
9613
  console.log();
@@ -9498,6 +9653,9 @@ async function runFlow(featureName, options) {
9498
9653
  console.log(chalk6.gray("Auto gate reached. Reply with one of the labels shown above (example: A OK)."));
9499
9654
  console.log();
9500
9655
  }
9656
+ if (autoRun && isAutoRunFailureStatus(autoRun.status)) {
9657
+ process.exitCode = 1;
9658
+ }
9501
9659
  if (after.matchedFeature) {
9502
9660
  console.log(
9503
9661
  chalk6.blue(
@@ -11986,6 +12144,263 @@ async function runOnboard(options) {
11986
12144
  process.exitCode = 1;
11987
12145
  }
11988
12146
  }
12147
+ function escapeRegExp4(value) {
12148
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12149
+ }
12150
+ function normalizeDecision(raw) {
12151
+ if (!raw) return null;
12152
+ const value = raw.trim().toLowerCase().replace(/[\s-]+/g, "_");
12153
+ if (value === "approve" || value === "approved") return "approve";
12154
+ if (value === "changes_requested" || value === "change_requested" || value === "changes") {
12155
+ return "changes_requested";
12156
+ }
12157
+ if (value === "blocked" || value === "block") return "blocked";
12158
+ return null;
12159
+ }
12160
+ function parseNonNegativeInt(raw, label) {
12161
+ if (raw === void 0) return null;
12162
+ const value = Number(raw);
12163
+ if (!Number.isInteger(value) || value < 0) {
12164
+ throw createCliError(
12165
+ "INVALID_ARGUMENT",
12166
+ `\`${label}\` must be a non-negative integer.`
12167
+ );
12168
+ }
12169
+ return value;
12170
+ }
12171
+ function findSpecLineIndex(lines, keys) {
12172
+ const escaped = keys.map((key) => escapeRegExp4(key));
12173
+ const re = new RegExp(`^\\s*-\\s*\\*\\*(?:${escaped.join("|")})\\*\\*\\s*:\\s*`);
12174
+ return lines.findIndex((line) => re.test(line));
12175
+ }
12176
+ function replaceSpecLine(line, keys, preferredKey, value) {
12177
+ const escaped = keys.map((key) => escapeRegExp4(key));
12178
+ const re = new RegExp(
12179
+ `^(\\s*-\\s*\\*\\*)(?:${escaped.join("|")})(\\*\\*\\s*:\\s*)(.*)$`
12180
+ );
12181
+ if (!re.test(line)) return line;
12182
+ return line.replace(re, `$1${preferredKey}$2${value}`);
12183
+ }
12184
+ function computeInsertIndex(lines, anchorKeys) {
12185
+ const anchorIndex = findSpecLineIndex(lines, anchorKeys);
12186
+ if (anchorIndex !== -1) {
12187
+ let cursor = anchorIndex + 1;
12188
+ while (cursor < lines.length && /^\s{2,}-\s+/.test(lines[cursor])) {
12189
+ cursor += 1;
12190
+ }
12191
+ return cursor;
12192
+ }
12193
+ const sectionIndex = lines.findIndex(
12194
+ (line) => /^\s*##\s+(Task List|태스크 목록)\s*$/.test(line)
12195
+ );
12196
+ if (sectionIndex !== -1) return sectionIndex;
12197
+ return lines.length;
12198
+ }
12199
+ function upsertSpecLine(content, keys, preferredKey, value, anchorKeys) {
12200
+ const lines = content.split("\n");
12201
+ const index = findSpecLineIndex(lines, keys);
12202
+ if (index !== -1) {
12203
+ lines[index] = replaceSpecLine(lines[index], keys, preferredKey, value);
12204
+ return lines.join("\n");
12205
+ }
12206
+ const insertAt = computeInsertIndex(lines, anchorKeys);
12207
+ lines.splice(insertAt, 0, `- **${preferredKey}**: ${value}`);
12208
+ return lines.join("\n");
12209
+ }
12210
+ function normalizePathForDoc(value) {
12211
+ return value.replace(/\\/g, "/");
12212
+ }
12213
+ function getPreferredKeys(lang) {
12214
+ if (lang === "ko") {
12215
+ return {
12216
+ review: "PR \uC804 \uB9AC\uBDF0",
12217
+ findings: "PR \uC804 \uB9AC\uBDF0 Findings",
12218
+ evidence: "PR \uC804 \uB9AC\uBDF0 Evidence",
12219
+ decision: "PR \uC804 \uB9AC\uBDF0 Decision",
12220
+ prStatus: "PR \uC0C1\uD0DC"
12221
+ };
12222
+ }
12223
+ return {
12224
+ review: "Pre-PR Review",
12225
+ findings: "Pre-PR Findings",
12226
+ evidence: "Pre-PR Evidence",
12227
+ decision: "Pre-PR Decision",
12228
+ prStatus: "PR Status"
12229
+ };
12230
+ }
12231
+ function buildReportContent(input) {
12232
+ const skills = input.skills.length > 0 ? input.skills.join(", ") : "code-review-excellence";
12233
+ return `# Pre-PR Review Report: ${input.folderName}
12234
+
12235
+ - Date: ${input.date}
12236
+ - Baseline: ${input.fallback}
12237
+ - Skills: ${skills}
12238
+ - Findings: major=${input.major}, minor=${input.minor}
12239
+ - Decision: ${input.decision}
12240
+ - Note: ${input.note}
12241
+
12242
+ ## Baseline Checklist
12243
+
12244
+ - [x] Checked alignment with spec/plan/tasks.
12245
+ - [x] Reviewed risk areas (regression, security, side effects, release readiness).
12246
+ - [x] Reviewed maintainability (structure, reuse, obsolete code cleanup).
12247
+ - [x] Reviewed test/verification coverage (or recorded reason if not run).
12248
+ `;
12249
+ }
12250
+ function prePrReviewCommand(program2) {
12251
+ program2.command("pre-pr-review [feature-name]").description("Run and record pre-PR review evidence for a feature").option("--component <component>", "Component name for multi projects").option(
12252
+ "--decision <outcome>",
12253
+ "Decision outcome: approve | changes_requested | blocked"
12254
+ ).option("--major <count>", "Pre-PR major findings count").option("--minor <count>", "Pre-PR minor findings count").option("--note <text>", "Decision note text").option("--json", "Output JSON").action(async (featureName, options) => {
12255
+ try {
12256
+ await runPrePrReview(featureName, options);
12257
+ } catch (error) {
12258
+ const config = await getConfig(process.cwd());
12259
+ const lang = config?.lang ?? DEFAULT_LANG;
12260
+ const cliError = toCliError(error);
12261
+ const suggestions = getCliErrorSuggestions(cliError.code, lang);
12262
+ if (options.json) {
12263
+ console.log(
12264
+ JSON.stringify({
12265
+ status: "error",
12266
+ reasonCode: cliError.code,
12267
+ error: cliError.message,
12268
+ suggestions
12269
+ })
12270
+ );
12271
+ } else {
12272
+ console.error(chalk6.red(`[${cliError.code}] ${cliError.message}`));
12273
+ printCliErrorSuggestions(suggestions, lang);
12274
+ }
12275
+ process.exitCode = 1;
12276
+ }
12277
+ });
12278
+ }
12279
+ async function runPrePrReview(featureName, options) {
12280
+ const config = await getConfig(process.cwd());
12281
+ if (!config) {
12282
+ throw createCliError(
12283
+ "CONFIG_NOT_FOUND",
12284
+ "No lee-spec-kit config found in this workspace."
12285
+ );
12286
+ }
12287
+ const selectionOptions = {
12288
+ component: resolveComponentOption(options.component)
12289
+ };
12290
+ const state = await resolveContextSelection(config, featureName, selectionOptions);
12291
+ if (state.status !== "single_matched" || !state.matchedFeature) {
12292
+ throw createCliError(
12293
+ "CONTEXT_SELECTION_REQUIRED",
12294
+ "pre-pr-review requires a single matched feature. Pass <feature-name> explicitly."
12295
+ );
12296
+ }
12297
+ const feature = state.matchedFeature;
12298
+ if (!feature.docs.tasksExists) {
12299
+ throw createCliError(
12300
+ "PRECONDITION_FAILED",
12301
+ `tasks.md not found for feature: ${feature.folderName}`
12302
+ );
12303
+ }
12304
+ const tasksPath = path22.join(feature.path, "tasks.md");
12305
+ const tasksContent = await fs16.readFile(tasksPath, "utf-8");
12306
+ const policy = resolvePrePrReviewPolicy(config.workflow);
12307
+ const preferred = getPreferredKeys(config.lang);
12308
+ const date = getLocalDateString();
12309
+ const major = parseNonNegativeInt(options.major, "--major") ?? feature.prePrReview.findings?.major ?? 0;
12310
+ const minor = parseNonNegativeInt(options.minor, "--minor") ?? feature.prePrReview.findings?.minor ?? 0;
12311
+ const explicitDecision = normalizeDecision(options.decision);
12312
+ if (options.decision && !explicitDecision) {
12313
+ throw createCliError(
12314
+ "INVALID_ARGUMENT",
12315
+ "`--decision` must be one of: approve, changes_requested, blocked."
12316
+ );
12317
+ }
12318
+ const inferredDecision = major > 0 ? "changes_requested" : "approve";
12319
+ const decision = explicitDecision || inferredDecision;
12320
+ if (!policy.decisionEnum.includes(decision)) {
12321
+ throw createCliError(
12322
+ "INVALID_ARGUMENT",
12323
+ `Decision "${decision}" is not allowed by workflow.prePrReview.decisionEnum.`
12324
+ );
12325
+ }
12326
+ const note = options.note?.trim() || (decision === "approve" ? "baseline review completed without blocking findings" : decision === "changes_requested" ? "resolve findings before PR creation" : "blocked until prerequisite risk is resolved");
12327
+ const reportPath = path22.join(feature.path, "pre-pr-review.md");
12328
+ const reportContent = buildReportContent({
12329
+ folderName: feature.folderName,
12330
+ date,
12331
+ decision,
12332
+ major,
12333
+ minor,
12334
+ note,
12335
+ fallback: policy.fallback,
12336
+ skills: policy.skills
12337
+ });
12338
+ await fs16.writeFile(reportPath, reportContent, "utf-8");
12339
+ const reportPathFromDocs = normalizePathForDoc(
12340
+ path22.join(feature.docs.featurePathFromDocs, "pre-pr-review.md")
12341
+ );
12342
+ const evidencePath = path22.basename(config.docsDir) === "docs" ? normalizePathForDoc(path22.join("docs", reportPathFromDocs)) : reportPathFromDocs;
12343
+ let nextTasks = tasksContent;
12344
+ nextTasks = upsertSpecLine(
12345
+ nextTasks,
12346
+ ["PR \uC804 \uB9AC\uBDF0", "Pre-PR Review"],
12347
+ preferred.review,
12348
+ "Done",
12349
+ ["PR \uC0C1\uD0DC", "PR Status"]
12350
+ );
12351
+ nextTasks = upsertSpecLine(
12352
+ nextTasks,
12353
+ ["PR \uC804 \uB9AC\uBDF0 Findings", "Pre-PR Findings"],
12354
+ preferred.findings,
12355
+ `major=${major}, minor=${minor}`,
12356
+ ["PR \uC804 \uB9AC\uBDF0", "Pre-PR Review"]
12357
+ );
12358
+ nextTasks = upsertSpecLine(
12359
+ nextTasks,
12360
+ ["PR \uC804 \uB9AC\uBDF0 Evidence", "Pre-PR Evidence"],
12361
+ preferred.evidence,
12362
+ evidencePath,
12363
+ ["PR \uC804 \uB9AC\uBDF0 Findings", "Pre-PR Findings", "PR \uC804 \uB9AC\uBDF0", "Pre-PR Review"]
12364
+ );
12365
+ nextTasks = upsertSpecLine(
12366
+ nextTasks,
12367
+ ["PR \uC804 \uB9AC\uBDF0 Decision", "Pre-PR Decision"],
12368
+ preferred.decision,
12369
+ `decision: ${decision} - ${note}`,
12370
+ ["PR \uC804 \uB9AC\uBDF0 Evidence", "Pre-PR Evidence"]
12371
+ );
12372
+ if (nextTasks !== tasksContent) {
12373
+ await fs16.writeFile(tasksPath, nextTasks, "utf-8");
12374
+ }
12375
+ if (options.json) {
12376
+ console.log(
12377
+ JSON.stringify(
12378
+ {
12379
+ status: "ok",
12380
+ reasonCode: "PRE_PR_REVIEW_RECORDED",
12381
+ feature: feature.folderName,
12382
+ reportPath: normalizePathForDoc(reportPath),
12383
+ evidencePath,
12384
+ decision,
12385
+ findings: { major, minor },
12386
+ tasksUpdated: nextTasks !== tasksContent
12387
+ },
12388
+ null,
12389
+ 2
12390
+ )
12391
+ );
12392
+ return;
12393
+ }
12394
+ console.log();
12395
+ console.log(chalk6.green(`\u2705 pre-pr-review completed: ${feature.folderName}`));
12396
+ console.log(chalk6.gray(`- Decision: ${decision}`));
12397
+ console.log(chalk6.gray(`- Findings: major=${major}, minor=${minor}`));
12398
+ console.log(chalk6.gray(`- Report: ${reportPath}`));
12399
+ if (nextTasks !== tasksContent) {
12400
+ console.log(chalk6.gray(`- tasks.md updated: ${tasksPath}`));
12401
+ }
12402
+ console.log();
12403
+ }
11989
12404
  function isBannerDisabled() {
11990
12405
  const v = (process.env.LEE_SPEC_KIT_NO_BANNER || "").trim();
11991
12406
  return v === "1";
@@ -12166,6 +12581,7 @@ githubCommand(program);
12166
12581
  docsCommand(program);
12167
12582
  detectCommand(program);
12168
12583
  onboardCommand(program);
12584
+ prePrReviewCommand(program);
12169
12585
  await program.parseAsync();
12170
12586
  //# sourceMappingURL=index.js.map
12171
12587
  //# sourceMappingURL=index.js.map