lee-spec-kit 0.3.0 → 0.4.0

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
@@ -887,6 +887,7 @@ async function runUpdate(options) {
887
887
  console.log(chalk6.green(`\u2705 \uCD1D ${updatedCount}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`));
888
888
  }
889
889
  async function updateFolder(sourceDir, targetDir, force, replacements) {
890
+ const protectedFiles = /* @__PURE__ */ new Set(["custom.md", "constitution.md"]);
890
891
  await fs6.ensureDir(targetDir);
891
892
  const files = await fs6.readdir(sourceDir);
892
893
  let updatedCount = 0;
@@ -895,7 +896,7 @@ async function updateFolder(sourceDir, targetDir, force, replacements) {
895
896
  const targetPath = path4.join(targetDir, file);
896
897
  const stat = await fs6.stat(sourcePath);
897
898
  if (stat.isFile()) {
898
- if (file === "custom.md") {
899
+ if (protectedFiles.has(file)) {
899
900
  continue;
900
901
  }
901
902
  let sourceContent = await fs6.readFile(sourcePath, "utf-8");
@@ -1031,189 +1032,449 @@ async function runConfig(options) {
1031
1032
  await fs6.writeJson(configPath, configFile, { spaces: 2 });
1032
1033
  console.log();
1033
1034
  }
1034
- function isCompletionChecklistDone(feature) {
1035
- return !!feature.completionChecklist && feature.completionChecklist.total > 0 && feature.completionChecklist.checked === feature.completionChecklist.total;
1035
+
1036
+ // src/utils/context/i18n.ts
1037
+ function formatTemplate(template, vars) {
1038
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
1039
+ const value = vars[key];
1040
+ return value === void 0 ? `{${key}}` : String(value);
1041
+ });
1036
1042
  }
1037
- var STEP_DEFINITIONS = [
1038
- {
1039
- step: 1,
1040
- name: "Feature \uD3F4\uB354 \uC0DD\uC131",
1041
- checklist: { done: () => true }
1042
- },
1043
- {
1044
- step: 2,
1045
- name: "spec.md \uC791\uC131",
1046
- checklist: { done: (f) => f.specStatus === "Review" || f.specStatus === "Approved" },
1047
- current: {
1048
- when: (f) => !f.docs.specExists || !f.specStatus || f.specStatus === "Draft",
1049
- actions: (f) => [
1050
- {
1051
- type: "instruction",
1052
- message: !f.docs.specExists ? "spec.md \uD30C\uC77C\uC744 \uC791\uC131\uD558\uC138\uC694. (\uC0C1\uD0DC: Draft\uBD80\uD130 \uC2DC\uC791)" : "spec.md\uB97C \uC791\uC131/\uBCF4\uC644\uD558\uACE0, Status\uB97C Review\uB85C \uBCC0\uACBD\uD55C \uB4A4 \uC0AC\uC6A9\uC790 \uB9AC\uBDF0\uB97C \uC694\uCCAD\uD558\uC138\uC694."
1053
- }
1054
- ]
1055
- }
1056
- },
1057
- {
1058
- step: 3,
1059
- name: "spec.md \uC2B9\uC778",
1060
- checklist: { done: (f) => f.specStatus === "Approved" },
1061
- current: {
1062
- when: (f) => f.specStatus === "Review",
1063
- actions: () => [
1064
- {
1065
- type: "instruction",
1066
- message: "\uC0AC\uC6A9\uC790\uC5D0\uAC8C spec.md\uB97C \uACF5\uC720\uD558\uACE0 \uBA85\uC2DC\uC801 \uC2B9\uC778(Approved)\uC744 \uBC1B\uC73C\uC138\uC694."
1067
- }
1068
- ]
1043
+ var I18N = {
1044
+ ko: {
1045
+ steps: {
1046
+ featureFolder: "Feature \uD3F4\uB354 \uC0DD\uC131",
1047
+ specWrite: "spec.md \uC791\uC131",
1048
+ specApprove: "spec.md \uC2B9\uC778",
1049
+ planWrite: "plan.md \uC791\uC131",
1050
+ planApprove: "plan.md \uC2B9\uC778",
1051
+ tasksWrite: "tasks.md \uC791\uC131",
1052
+ docsCommitPlanning: "\uBB38\uC11C \uCEE4\uBC0B(\uAE30\uD68D)",
1053
+ issueCreate: "GitHub Issue \uC0DD\uC131",
1054
+ branchCreate: "\uBE0C\uB79C\uCE58 \uC0DD\uC131",
1055
+ tasksExecute: "\uD0DC\uC2A4\uD06C \uC2E4\uD589",
1056
+ prCreate: "PR \uC0DD\uC131",
1057
+ codeReview: "\uCF54\uB4DC \uB9AC\uBDF0",
1058
+ featureDone: "Feature \uC644\uB8CC"
1059
+ },
1060
+ messages: {
1061
+ specCreate: "spec.md \uD15C\uD50C\uB9BF\uC744 \uBCF5\uC0AC\uD574 \uC791\uC131\uD558\uC138\uC694. (features/feature-base/spec.md \uCC38\uACE0)",
1062
+ specImprove: "spec.md\uB97C \uBCF4\uC644\uD558\uACE0 \uC0C1\uD0DC\uB97C Review\uB85C \uBCC0\uACBD\uD558\uC138\uC694.",
1063
+ specApproval: "spec.md \uB0B4\uC6A9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uC2B9\uC778(OK)\uC744 \uBC1B\uC73C\uC138\uC694.",
1064
+ planCreate: "plan.md \uD15C\uD50C\uB9BF\uC744 \uBCF5\uC0AC\uD574 \uC791\uC131\uD558\uC138\uC694. (features/feature-base/plan.md \uCC38\uACE0)",
1065
+ planImprove: "plan.md\uB97C \uBCF4\uC644\uD558\uACE0 \uC0C1\uD0DC\uB97C Review\uB85C \uBCC0\uACBD\uD558\uC138\uC694.",
1066
+ planApproval: "plan.md \uB0B4\uC6A9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uC2B9\uC778(OK)\uC744 \uBC1B\uC73C\uC138\uC694.",
1067
+ tasksCreate: "tasks.md \uD15C\uD50C\uB9BF\uC744 \uBCF5\uC0AC\uD574 \uD0DC\uC2A4\uD06C\uB97C \uC791\uC131\uD558\uC138\uC694. (features/feature-base/tasks.md \uCC38\uACE0)",
1068
+ tasksNeedAtLeastOne: "tasks.md\uC5D0 \uCD5C\uC18C 1\uAC1C \uC774\uC0C1\uC758 \uD0DC\uC2A4\uD06C\uB97C \uC791\uC131\uD558\uC138\uC694.",
1069
+ docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}): \uAE30\uD68D \uBB38\uC11C \uC791\uC131"',
1070
+ issueCreateAndWrite: "GitHub Issue\uB97C \uC0DD\uC131\uD55C \uB4A4, spec.md/tasks.md\uC758 \uC774\uC288 \uBC88\uD638\uB97C \uCC44\uC6B0\uACE0 \uBB38\uC11C \uCEE4\uBC0B\uC744 \uC900\uBE44\uD558\uC138\uC694. (skills/create-issue.md \uCC38\uACE0)",
1071
+ docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}): \uC774\uC288 #{issueNumber} \uBC18\uC601"',
1072
+ standaloneNeedsProjectRoot: "standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)",
1073
+ createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
1074
+ tasksAllDoneButNoChecklist: '\uBAA8\uB4E0 \uD0DC\uC2A4\uD06C\uAC00 DONE\uC774\uC9C0\uB9CC \uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uC139\uC158\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. tasks.md\uC758 "\uC644\uB8CC \uC870\uAC74" \uC139\uC158\uC744 \uCD94\uAC00/\uD655\uC778\uD558\uC138\uC694.',
1075
+ tasksAllDoneButChecklist: "\uBAA8\uB4E0 \uD0DC\uC2A4\uD06C\uAC00 DONE\uC774\uC9C0\uB9CC \uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8\uAC00 \uC644\uC804\uD788 \uCCB4\uD06C\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. ({checked}/{total})",
1076
+ finishDoingTask: '\uD604\uC7AC DOING/REVIEW \uC911\uC778 \uD0DC\uC2A4\uD06C\uB97C \uC644\uB8CC\uD558\uC138\uC694: "{title}" ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)',
1077
+ startNextTodoTask: '\uB2E4\uC74C TODO \uD0DC\uC2A4\uD06C\uB97C \uC2DC\uC791\uD558\uC138\uC694: "{title}" ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)',
1078
+ checkTaskStatuses: "\uD0DC\uC2A4\uD06C \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694. ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)",
1079
+ prLegacyAsk: "tasks.md\uC5D0 PR/PR \uC0C1\uD0DC \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uD15C\uD50C\uB9BF\uC744 \uCD5C\uC2E0 \uD3EC\uB9F7\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD560\uAE4C\uC694? (OK \uD544\uC694)",
1080
+ prCreate: "PR\uC744 \uC0DD\uC131\uD558\uACE0 tasks.md\uC5D0 PR \uB9C1\uD06C\uB97C \uAE30\uB85D\uD558\uC138\uC694. (skills/create-pr.md \uCC38\uACE0)",
1081
+ prResolveReview: "\uB9AC\uBDF0 \uCF54\uBA58\uD2B8\uB97C \uD574\uACB0\uD558\uACE0 PR \uC0C1\uD0DC\uB97C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694. (PR \uC0C1\uD0DC: Review \u2192 Approved)",
1082
+ prRequestReview: "\uB9AC\uBDF0\uC5B4\uC5D0\uAC8C \uB9AC\uBDF0\uB97C \uC694\uCCAD\uD558\uACE0 PR \uC0C1\uD0DC\uB97C Review\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.",
1083
+ featureDone: "PR\uC774 Approved\uC774\uACE0 \uBAA8\uB4E0 \uD0DC\uC2A4\uD06C/\uC644\uB8CC \uC870\uAC74\uC774 \uCDA9\uC871\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC774 Feature\uB294 \uC644\uB8CC \uC0C1\uD0DC\uC785\uB2C8\uB2E4.",
1084
+ fallbackRerunContext: "\uC0C1\uD0DC\uB97C \uD310\uBCC4\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBB38\uC11C\uB97C \uD655\uC778\uD55C \uB4A4 \uB2E4\uC2DC context\uB97C \uC2E4\uD589\uD558\uC138\uC694."
1085
+ },
1086
+ warnings: {
1087
+ projectBranchUnavailable: "\uD504\uB85C\uC81D\uD2B8 \uBE0C\uB79C\uCE58\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.)",
1088
+ docsGitUnavailable: "docs \uB808\uD3EC\uC758 git \uC0C1\uD0DC\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (\uB808\uD3EC \uC704\uCE58 / git init \uD655\uC778)",
1089
+ 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."
1069
1090
  }
1070
1091
  },
1071
- {
1072
- step: 4,
1073
- name: "GitHub Issue \uC0DD\uC131",
1074
- checklist: { done: (f) => !!f.issueNumber },
1075
- current: {
1076
- when: (f) => f.specStatus === "Approved" && !f.issueNumber,
1077
- actions: () => [
1078
- {
1079
- type: "instruction",
1080
- message: "GitHub Issue\uB97C \uC0DD\uC131\uD558\uACE0 spec.md\uC758 **\uC774\uC288 \uBC88\uD638**\uC5D0 #\uBC88\uD638\uB97C \uAE30\uC785\uD558\uC138\uC694."
1081
- }
1082
- ]
1092
+ en: {
1093
+ steps: {
1094
+ featureFolder: "Create feature folder",
1095
+ specWrite: "Write spec.md",
1096
+ specApprove: "Approve spec.md",
1097
+ planWrite: "Write plan.md",
1098
+ planApprove: "Approve plan.md",
1099
+ tasksWrite: "Write tasks.md",
1100
+ docsCommitPlanning: "Commit planning docs",
1101
+ issueCreate: "Create GitHub Issue",
1102
+ branchCreate: "Create branch",
1103
+ tasksExecute: "Execute tasks",
1104
+ prCreate: "Create PR",
1105
+ codeReview: "Code review",
1106
+ featureDone: "Feature done"
1107
+ },
1108
+ messages: {
1109
+ specCreate: "Copy the spec.md template and write it. (See features/feature-base/spec.md)",
1110
+ specImprove: "Improve spec.md and set Status to Review.",
1111
+ specApproval: "Share spec.md with the user and get approval (OK).",
1112
+ planCreate: "Copy the plan.md template and write it. (See features/feature-base/plan.md)",
1113
+ planImprove: "Improve plan.md and set Status to Review.",
1114
+ planApproval: "Share plan.md with the user and get approval (OK).",
1115
+ tasksCreate: "Copy the tasks.md template and write tasks. (See features/feature-base/tasks.md)",
1116
+ tasksNeedAtLeastOne: "Add at least one task to tasks.md.",
1117
+ docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}): planning docs"',
1118
+ issueCreateAndWrite: "Create a GitHub Issue, then fill in the issue number in spec.md/tasks.md and prepare to commit docs. (See skills/create-issue.md)",
1119
+ docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}): issue #{issueNumber}"',
1120
+ standaloneNeedsProjectRoot: "In standalone mode, projectRoot is required. (npx lee-spec-kit config --project-root ...)",
1121
+ createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
1122
+ tasksAllDoneButNoChecklist: 'All tasks are DONE but no completion checklist section was found. Add/verify the "Completion Criteria" section in tasks.md.',
1123
+ tasksAllDoneButChecklist: "All tasks are DONE but the completion checklist is not fully checked. ({checked}/{total})",
1124
+ finishDoingTask: 'Finish the active DOING/REVIEW task: "{title}" ({done}/{total}) (See skills/execute-task.md)',
1125
+ startNextTodoTask: 'Start the next TODO task: "{title}" ({done}/{total}) (See skills/execute-task.md)',
1126
+ checkTaskStatuses: "Check task statuses. ({done}/{total}) (See skills/execute-task.md)",
1127
+ prLegacyAsk: "Legacy tasks.md format detected (missing PR/PR Status fields). Update to the latest format? (OK required)",
1128
+ prCreate: "Create a PR and record the PR link in tasks.md. (See skills/create-pr.md)",
1129
+ prResolveReview: "Resolve review comments and update PR status. (PR Status: Review \u2192 Approved)",
1130
+ prRequestReview: "Request reviews and update PR status to Review.",
1131
+ featureDone: "PR is Approved and all tasks/completion criteria are satisfied. This feature is done.",
1132
+ fallbackRerunContext: "Unable to determine current state. Verify docs and run context again."
1133
+ },
1134
+ warnings: {
1135
+ projectBranchUnavailable: "Cannot determine project branch. (In standalone mode, projectRoot is required.)",
1136
+ docsGitUnavailable: "Cannot read git status for the docs repo. (Check repo location / git init.)",
1137
+ legacyTasksPrFields: "Legacy tasks.md format detected. Add `PR` and `PR Status` fields before PR steps."
1083
1138
  }
1084
- },
1085
- {
1086
- step: 5,
1087
- name: "\uBE0C\uB79C\uCE58 \uC0DD\uC131",
1088
- checklist: { done: (f) => f.git.onExpectedBranch },
1089
- current: {
1090
- when: (f) => !!f.issueNumber && (!f.git.projectBranchAvailable || !f.git.onExpectedBranch),
1091
- actions: (f) => {
1092
- if (!f.git.projectBranchAvailable || !f.git.projectGitCwd) {
1139
+ }
1140
+ };
1141
+ function tr(lang, category, key, vars = {}) {
1142
+ const template = I18N[lang][category][key] ?? I18N.ko[category][key] ?? `${category}.${key}`;
1143
+ return formatTemplate(template, vars);
1144
+ }
1145
+
1146
+ // src/utils/context/steps.ts
1147
+ function isCompletionChecklistDone(feature) {
1148
+ return !!feature.completionChecklist && feature.completionChecklist.total > 0 && feature.completionChecklist.checked === feature.completionChecklist.total;
1149
+ }
1150
+ function isPrMetadataConfigured(feature) {
1151
+ return feature.docs.prFieldExists && feature.docs.prStatusFieldExists;
1152
+ }
1153
+ function isFeatureDone(feature) {
1154
+ return feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature) && isPrMetadataConfigured(feature) && !!feature.pr.link && feature.pr.status === "Approved";
1155
+ }
1156
+ function getStepDefinitions(lang) {
1157
+ return [
1158
+ {
1159
+ step: 1,
1160
+ name: tr(lang, "steps", "featureFolder"),
1161
+ checklist: { done: () => true }
1162
+ },
1163
+ {
1164
+ step: 2,
1165
+ name: tr(lang, "steps", "specWrite"),
1166
+ checklist: {
1167
+ done: (f) => f.specStatus === "Review" || f.specStatus === "Approved"
1168
+ },
1169
+ current: {
1170
+ when: (f) => !f.docs.specExists || !f.specStatus || f.specStatus === "Draft",
1171
+ actions: (f) => [
1172
+ {
1173
+ type: "instruction",
1174
+ message: !f.docs.specExists ? tr(lang, "messages", "specCreate") : tr(lang, "messages", "specImprove")
1175
+ }
1176
+ ]
1177
+ }
1178
+ },
1179
+ {
1180
+ step: 3,
1181
+ name: tr(lang, "steps", "specApprove"),
1182
+ checklist: { done: (f) => f.specStatus === "Approved" },
1183
+ current: {
1184
+ when: (f) => f.specStatus === "Review",
1185
+ actions: () => [
1186
+ {
1187
+ type: "instruction",
1188
+ requiresUserOk: true,
1189
+ message: tr(lang, "messages", "specApproval")
1190
+ }
1191
+ ]
1192
+ }
1193
+ },
1194
+ {
1195
+ step: 4,
1196
+ name: tr(lang, "steps", "planWrite"),
1197
+ checklist: {
1198
+ done: (f) => f.planStatus === "Review" || f.planStatus === "Approved"
1199
+ },
1200
+ current: {
1201
+ when: (f) => f.specStatus === "Approved" && (!f.docs.planExists || !f.planStatus || f.planStatus === "Draft"),
1202
+ actions: (f) => [
1203
+ {
1204
+ type: "instruction",
1205
+ message: !f.docs.planExists ? tr(lang, "messages", "planCreate") : tr(lang, "messages", "planImprove")
1206
+ }
1207
+ ]
1208
+ }
1209
+ },
1210
+ {
1211
+ step: 5,
1212
+ name: tr(lang, "steps", "planApprove"),
1213
+ checklist: { done: (f) => f.planStatus === "Approved" },
1214
+ current: {
1215
+ when: (f) => f.planStatus === "Review",
1216
+ actions: () => [
1217
+ {
1218
+ type: "instruction",
1219
+ requiresUserOk: true,
1220
+ message: tr(lang, "messages", "planApproval")
1221
+ }
1222
+ ]
1223
+ }
1224
+ },
1225
+ {
1226
+ step: 6,
1227
+ name: tr(lang, "steps", "tasksWrite"),
1228
+ checklist: {
1229
+ done: (f) => f.docs.tasksExists && f.tasks.total > 0,
1230
+ detail: (f) => f.tasks.total > 0 ? `(${f.tasks.total})` : ""
1231
+ },
1232
+ current: {
1233
+ when: (f) => f.planStatus === "Approved" && (!f.docs.tasksExists || f.tasks.total === 0),
1234
+ actions: (f) => {
1235
+ if (!f.docs.tasksExists) {
1236
+ return [
1237
+ {
1238
+ type: "instruction",
1239
+ message: tr(lang, "messages", "tasksCreate")
1240
+ }
1241
+ ];
1242
+ }
1093
1243
  return [
1094
1244
  {
1095
1245
  type: "instruction",
1096
- message: "standalone \uBAA8\uB4DC\uB77C\uBA74 projectRoot\uB97C \uC124\uC815\uD55C \uB4A4 \uB2E4\uC2DC context\uB97C \uD655\uC778\uD558\uC138\uC694. (npx lee-spec-kit config --project-root ...)"
1246
+ message: tr(lang, "messages", "tasksNeedAtLeastOne")
1097
1247
  }
1098
1248
  ];
1099
1249
  }
1100
- return [
1250
+ }
1251
+ },
1252
+ {
1253
+ step: 7,
1254
+ name: tr(lang, "steps", "docsCommitPlanning"),
1255
+ checklist: {
1256
+ done: (f) => f.issueNumber ? true : f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.git.docsHasUncommittedChanges
1257
+ },
1258
+ current: {
1259
+ when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.issueNumber && f.git.docsHasUncommittedChanges,
1260
+ actions: (f) => [
1101
1261
  {
1102
1262
  type: "command",
1103
- scope: "project",
1104
- cwd: f.git.projectGitCwd,
1105
- cmd: `git -C "${f.git.projectGitCwd}" checkout -b feat/${f.issueNumber}-${f.slug}`
1263
+ requiresUserOk: true,
1264
+ scope: "docs",
1265
+ cwd: f.git.docsGitCwd,
1266
+ cmd: tr(lang, "messages", "docsCommitPlanning", {
1267
+ docsGitCwd: f.git.docsGitCwd,
1268
+ featurePath: f.docs.featurePathFromDocs,
1269
+ folderName: f.folderName
1270
+ })
1106
1271
  }
1107
- ];
1272
+ ]
1108
1273
  }
1109
- }
1110
- },
1111
- {
1112
- step: 6,
1113
- name: "plan.md \uC791\uC131",
1114
- checklist: { done: (f) => f.planStatus === "Review" || f.planStatus === "Approved" },
1115
- current: {
1116
- when: (f) => f.git.onExpectedBranch && (!f.docs.planExists || !f.planStatus || f.planStatus === "Draft"),
1117
- actions: (f) => [
1118
- {
1119
- type: "instruction",
1120
- message: !f.docs.planExists ? "plan.md\uB97C \uC791\uC131\uD558\uC138\uC694. (\uC0C1\uD0DC: Draft\uBD80\uD130 \uC2DC\uC791)" : "plan.md\uB97C \uC791\uC131/\uBCF4\uC644\uD558\uACE0, Status\uB97C Review\uB85C \uBCC0\uACBD\uD55C \uB4A4 \uC0AC\uC6A9\uC790 \uB9AC\uBDF0\uB97C \uC694\uCCAD\uD558\uC138\uC694."
1121
- }
1122
- ]
1123
- }
1124
- },
1125
- {
1126
- step: 7,
1127
- name: "plan.md \uC2B9\uC778",
1128
- checklist: { done: (f) => f.planStatus === "Approved" },
1129
- current: {
1130
- when: (f) => f.planStatus === "Review",
1131
- actions: () => [
1132
- {
1133
- type: "instruction",
1134
- message: "\uC0AC\uC6A9\uC790\uC5D0\uAC8C plan.md\uB97C \uACF5\uC720\uD558\uACE0 \uBA85\uC2DC\uC801 \uC2B9\uC778(Approved)\uC744 \uBC1B\uC73C\uC138\uC694."
1135
- }
1136
- ]
1137
- }
1138
- },
1139
- {
1140
- step: 8,
1141
- name: "tasks.md \uC791\uC131/\uC2E4\uD589",
1142
- checklist: {
1143
- done: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.tasks.total === f.tasks.done,
1144
- detail: (f) => f.tasks.total > 0 ? `(${f.tasks.done}/${f.tasks.total})` : ""
1145
1274
  },
1146
- current: {
1147
- when: (f) => f.planStatus === "Approved" && (!f.docs.tasksExists || f.tasks.total === 0 || f.tasks.done < f.tasks.total),
1148
- actions: (f) => {
1149
- let message = "";
1150
- if (!f.docs.tasksExists) {
1151
- message = "tasks.md\uB97C \uC791\uC131\uD558\uC138\uC694. (\uAC01 \uD0DC\uC2A4\uD06C \uC0C1\uD0DC: [TODO])";
1152
- return [{ type: "instruction", message }];
1153
- }
1154
- if (f.tasks.total === 0) {
1155
- message = "tasks.md\uC5D0 \uCD5C\uC18C 1\uAC1C \uC774\uC0C1\uC758 \uD0DC\uC2A4\uD06C\uB97C \uC791\uC131\uD558\uC138\uC694.";
1156
- return [{ type: "instruction", message }];
1157
- }
1158
- if (f.activeTask) {
1159
- message = `[DOING] \uD0DC\uC2A4\uD06C\uB97C \uC644\uB8CC\uD558\uC138\uC694: ${f.activeTask.title} (\uC9C4\uD589\uB960: ${f.tasks.done}/${f.tasks.total})`;
1160
- return [{ type: "instruction", message }];
1275
+ {
1276
+ step: 8,
1277
+ name: tr(lang, "steps", "issueCreate"),
1278
+ checklist: {
1279
+ done: (f) => !!f.issueNumber && !f.git.docsHasUncommittedChanges
1280
+ },
1281
+ current: {
1282
+ when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && (!f.issueNumber || f.git.docsHasUncommittedChanges),
1283
+ actions: (f) => {
1284
+ if (!f.issueNumber) {
1285
+ return [
1286
+ {
1287
+ type: "instruction",
1288
+ requiresUserOk: true,
1289
+ message: tr(lang, "messages", "issueCreateAndWrite")
1290
+ }
1291
+ ];
1292
+ }
1293
+ return [
1294
+ {
1295
+ type: "command",
1296
+ requiresUserOk: true,
1297
+ scope: "docs",
1298
+ cwd: f.git.docsGitCwd,
1299
+ cmd: tr(lang, "messages", "docsCommitIssueUpdate", {
1300
+ docsGitCwd: f.git.docsGitCwd,
1301
+ featurePath: f.docs.featurePathFromDocs,
1302
+ issueNumber: f.issueNumber,
1303
+ folderName: f.folderName
1304
+ })
1305
+ }
1306
+ ];
1161
1307
  }
1162
- if (f.nextTodoTask) {
1163
- message = `\uB2E4\uC74C \uD0DC\uC2A4\uD06C\uB97C [DOING]\uC73C\uB85C \uBCC0\uACBD\uD558\uACE0 \uC2DC\uC791\uD558\uC138\uC694: ${f.nextTodoTask.title} (\uC9C4\uD589\uB960: ${f.tasks.done}/${f.tasks.total})`;
1164
- return [{ type: "instruction", message }];
1308
+ }
1309
+ },
1310
+ {
1311
+ step: 9,
1312
+ name: tr(lang, "steps", "branchCreate"),
1313
+ checklist: { done: (f) => f.git.onExpectedBranch },
1314
+ current: {
1315
+ when: (f) => !!f.issueNumber && (!f.git.projectBranchAvailable || !f.git.onExpectedBranch),
1316
+ actions: (f) => {
1317
+ if (!f.git.projectBranchAvailable || !f.git.projectGitCwd) {
1318
+ return [
1319
+ {
1320
+ type: "instruction",
1321
+ message: tr(lang, "messages", "standaloneNeedsProjectRoot")
1322
+ }
1323
+ ];
1324
+ }
1325
+ return [
1326
+ {
1327
+ type: "command",
1328
+ scope: "project",
1329
+ cwd: f.git.projectGitCwd,
1330
+ cmd: tr(lang, "messages", "createBranch", {
1331
+ projectGitCwd: f.git.projectGitCwd,
1332
+ issueNumber: f.issueNumber,
1333
+ slug: f.slug
1334
+ })
1335
+ }
1336
+ ];
1165
1337
  }
1166
- message = `tasks.md\uC758 \uD0DC\uC2A4\uD06C \uC0C1\uD0DC([TODO]/[DOING]/[DONE])\uB97C \uD655\uC778\uD558\uC138\uC694. (\uC9C4\uD589\uB960: ${f.tasks.done}/${f.tasks.total})`;
1167
- return [{ type: "instruction", message }];
1168
1338
  }
1169
- }
1170
- },
1171
- {
1172
- step: 9,
1173
- name: "\uBB38\uC11C \uCEE4\uBC0B \uC804 \uD655\uC778",
1174
- checklist: {
1175
- done: (f) => isCompletionChecklistDone(f),
1176
- detail: (f) => f.completionChecklist ? `(${f.completionChecklist.checked}/${f.completionChecklist.total})` : ""
1177
1339
  },
1178
- current: {
1179
- when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.tasks.total === f.tasks.done && !isCompletionChecklistDone(f),
1180
- actions: (f) => [
1181
- {
1182
- type: "instruction",
1183
- message: !f.completionChecklist ? '\uBAA8\uB4E0 \uD0DC\uC2A4\uD06C\uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. tasks.md\uC5D0 "\uC644\uB8CC \uC870\uAC74" \uCCB4\uD06C\uB9AC\uC2A4\uD2B8\uB97C \uCD94\uAC00/\uD655\uC778\uD55C \uB4A4 \uC9C4\uD589\uD558\uC138\uC694.' : `\uBB38\uC11C \uCEE4\uBC0B \uC804 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8(\uC644\uB8CC \uC870\uAC74)\uB97C \uC2E4\uC81C\uB85C \uD655\uC778\uD558\uACE0 \uCCB4\uD06C\uD558\uC138\uC694. (${f.completionChecklist.checked}/${f.completionChecklist.total})`
1340
+ {
1341
+ step: 10,
1342
+ name: tr(lang, "steps", "tasksExecute"),
1343
+ checklist: {
1344
+ done: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.tasks.total === f.tasks.done && isCompletionChecklistDone(f),
1345
+ detail: (f) => f.tasks.total > 0 ? `(${f.tasks.done}/${f.tasks.total})` : ""
1346
+ },
1347
+ current: {
1348
+ when: (f) => f.git.onExpectedBranch && f.docs.tasksExists && f.tasks.total > 0 && (f.tasks.done < f.tasks.total || !isCompletionChecklistDone(f)),
1349
+ actions: (f) => {
1350
+ if (f.tasks.total === f.tasks.done && !isCompletionChecklistDone(f)) {
1351
+ return [
1352
+ {
1353
+ type: "instruction",
1354
+ requiresUserOk: true,
1355
+ message: !f.completionChecklist ? tr(lang, "messages", "tasksAllDoneButNoChecklist") : tr(lang, "messages", "tasksAllDoneButChecklist", {
1356
+ checked: f.completionChecklist.checked,
1357
+ total: f.completionChecklist.total
1358
+ })
1359
+ }
1360
+ ];
1361
+ }
1362
+ if (f.activeTask) {
1363
+ return [
1364
+ {
1365
+ type: "instruction",
1366
+ requiresUserOk: true,
1367
+ message: tr(lang, "messages", "finishDoingTask", {
1368
+ title: f.activeTask.title,
1369
+ done: f.tasks.done,
1370
+ total: f.tasks.total
1371
+ })
1372
+ }
1373
+ ];
1374
+ }
1375
+ if (f.nextTodoTask) {
1376
+ return [
1377
+ {
1378
+ type: "instruction",
1379
+ requiresUserOk: true,
1380
+ message: tr(lang, "messages", "startNextTodoTask", {
1381
+ title: f.nextTodoTask.title,
1382
+ done: f.tasks.done,
1383
+ total: f.tasks.total
1384
+ })
1385
+ }
1386
+ ];
1387
+ }
1388
+ return [
1389
+ {
1390
+ type: "instruction",
1391
+ requiresUserOk: true,
1392
+ message: tr(lang, "messages", "checkTaskStatuses", {
1393
+ done: f.tasks.done,
1394
+ total: f.tasks.total
1395
+ })
1396
+ }
1397
+ ];
1184
1398
  }
1185
- ]
1186
- }
1187
- },
1188
- {
1189
- step: 10,
1190
- name: "\uBB38\uC11C \uCEE4\uBC0B",
1191
- checklist: {
1192
- done: (f) => isCompletionChecklistDone(f) && !f.git.docsHasUncommittedChanges
1399
+ }
1193
1400
  },
1194
- current: {
1195
- when: (f) => isCompletionChecklistDone(f),
1196
- actions: (f) => f.git.docsHasUncommittedChanges ? [
1197
- {
1198
- type: "command",
1199
- scope: "docs",
1200
- cwd: f.git.docsGitCwd,
1201
- cmd: `git -C "${f.git.docsGitCwd}" add "${f.docs.featurePathFromDocs}" && git -C "${f.git.docsGitCwd}" commit -m "docs(#${f.issueNumber ?? "<issue>"}): ${f.folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"`
1401
+ {
1402
+ step: 11,
1403
+ name: tr(lang, "steps", "prCreate"),
1404
+ checklist: { done: (f) => isPrMetadataConfigured(f) && !!f.pr.link },
1405
+ current: {
1406
+ when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.tasks.total === f.tasks.done && isCompletionChecklistDone(f) && (!isPrMetadataConfigured(f) || !f.pr.link),
1407
+ actions: (f) => {
1408
+ if (!isPrMetadataConfigured(f)) {
1409
+ return [
1410
+ {
1411
+ type: "instruction",
1412
+ requiresUserOk: true,
1413
+ message: tr(lang, "messages", "prLegacyAsk")
1414
+ }
1415
+ ];
1416
+ }
1417
+ return [
1418
+ {
1419
+ type: "instruction",
1420
+ requiresUserOk: true,
1421
+ message: tr(lang, "messages", "prCreate")
1422
+ }
1423
+ ];
1202
1424
  }
1203
- ] : [
1204
- {
1205
- type: "instruction",
1206
- message: "\uBB38\uC11C \uCEE4\uBC0B\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uB2E4\uC74C Feature\uB85C \uC774\uB3D9\uD558\uC138\uC694."
1425
+ }
1426
+ },
1427
+ {
1428
+ step: 12,
1429
+ name: tr(lang, "steps", "codeReview"),
1430
+ checklist: {
1431
+ done: (f) => isPrMetadataConfigured(f) && f.pr.status === "Approved"
1432
+ },
1433
+ current: {
1434
+ when: (f) => isPrMetadataConfigured(f) && !!f.pr.link && f.pr.status !== "Approved",
1435
+ actions: (f) => {
1436
+ if (f.pr.status === "Review") {
1437
+ return [
1438
+ {
1439
+ type: "instruction",
1440
+ message: tr(lang, "messages", "prResolveReview")
1441
+ }
1442
+ ];
1443
+ }
1444
+ return [
1445
+ {
1446
+ type: "instruction",
1447
+ message: tr(lang, "messages", "prRequestReview")
1448
+ }
1449
+ ];
1207
1450
  }
1208
- ]
1451
+ }
1452
+ },
1453
+ {
1454
+ step: 13,
1455
+ name: tr(lang, "steps", "featureDone"),
1456
+ checklist: { done: (f) => isFeatureDone(f) },
1457
+ current: {
1458
+ when: (f) => isFeatureDone(f),
1459
+ actions: () => [
1460
+ {
1461
+ type: "instruction",
1462
+ message: tr(lang, "messages", "featureDone")
1463
+ }
1464
+ ]
1465
+ }
1209
1466
  }
1210
- }
1211
- ];
1212
- var STEPS = Object.fromEntries(
1213
- STEP_DEFINITIONS.map((d) => [d.step, d.name])
1214
- );
1215
- function resolveFeatureProgress(feature) {
1216
- const ordered = [...STEP_DEFINITIONS].sort((a, b) => a.step - b.step);
1467
+ ];
1468
+ }
1469
+ function getStepsMap(lang) {
1470
+ return Object.fromEntries(getStepDefinitions(lang).map((d) => [d.step, d.name]));
1471
+ }
1472
+ getStepDefinitions("ko");
1473
+ getStepsMap("ko");
1474
+
1475
+ // src/utils/context/progress.ts
1476
+ function resolveFeatureProgress(feature, stepDefinitions, lang) {
1477
+ const ordered = [...stepDefinitions].sort((a, b) => a.step - b.step);
1217
1478
  for (const definition of ordered) {
1218
1479
  if (!definition.current) continue;
1219
1480
  if (definition.current.when(feature)) {
@@ -1229,9 +1490,12 @@ function resolveFeatureProgress(feature) {
1229
1490
  return {
1230
1491
  currentStep: lastStep?.step ?? 10,
1231
1492
  actions: [
1232
- { type: "instruction", message: "npx lee-spec-kit context\uB85C \uC0C1\uD0DC\uB97C \uB2E4\uC2DC \uD655\uC778\uD558\uC138\uC694." }
1493
+ {
1494
+ type: "instruction",
1495
+ message: tr(lang, "messages", "fallbackRerunContext")
1496
+ }
1233
1497
  ],
1234
- nextAction: "npx lee-spec-kit context\uB85C \uC0C1\uD0DC\uB97C \uB2E4\uC2DC \uD655\uC778\uD558\uC138\uC694."
1498
+ nextAction: tr(lang, "messages", "fallbackRerunContext")
1235
1499
  };
1236
1500
  }
1237
1501
  function getCurrentBranch(cwd) {
@@ -1245,6 +1509,72 @@ function getCurrentBranch(cwd) {
1245
1509
  return "";
1246
1510
  }
1247
1511
  }
1512
+ function getGitStatusPorcelain(cwd, relativePaths) {
1513
+ try {
1514
+ const args = relativePaths.length > 0 ? ` -- ${relativePaths.map((p) => `"${p}"`).join(" ")}` : "";
1515
+ return execSync(`git status --porcelain=v1${args}`, {
1516
+ cwd,
1517
+ encoding: "utf-8",
1518
+ stdio: ["ignore", "pipe", "pipe"]
1519
+ });
1520
+ } catch {
1521
+ return void 0;
1522
+ }
1523
+ }
1524
+ function getGitTopLevel(cwd) {
1525
+ try {
1526
+ return execSync("git rev-parse --show-toplevel", {
1527
+ cwd,
1528
+ encoding: "utf-8",
1529
+ stdio: ["ignore", "pipe", "pipe"]
1530
+ }).trim();
1531
+ } catch {
1532
+ return null;
1533
+ }
1534
+ }
1535
+ function resolveProjectGitCwd(config, repo) {
1536
+ const docsRepo = config.docsRepo;
1537
+ if (docsRepo !== "standalone") {
1538
+ const topLevel = getGitTopLevel(process.cwd());
1539
+ return { cwd: topLevel || process.cwd() };
1540
+ }
1541
+ if (!config.projectRoot) {
1542
+ return {
1543
+ cwd: null,
1544
+ warning: "standalone \uBAA8\uB4DC\uC785\uB2C8\uB2E4. projectRoot\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC544 \uD504\uB85C\uC81D\uD2B8 \uBE0C\uB79C\uCE58 \uD655\uC778\uC774 \uBD88\uAC00\uB2A5\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)"
1545
+ };
1546
+ }
1547
+ if (config.projectType === "fullstack") {
1548
+ if (typeof config.projectRoot === "string") {
1549
+ return {
1550
+ cwd: null,
1551
+ warning: 'fullstack standalone \uBAA8\uB4DC\uC778\uB370 projectRoot \uD615\uD0DC\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (\uC608: { "fe": "...", "be": "..." })'
1552
+ };
1553
+ }
1554
+ const root = config.projectRoot[repo];
1555
+ if (!root) {
1556
+ return {
1557
+ cwd: null,
1558
+ warning: `projectRoot.${repo}\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ... --repo ${repo})`
1559
+ };
1560
+ }
1561
+ return { cwd: getGitTopLevel(root) || root };
1562
+ }
1563
+ if (typeof config.projectRoot !== "string") {
1564
+ return {
1565
+ cwd: null,
1566
+ warning: 'single standalone \uBAA8\uB4DC\uC778\uB370 projectRoot \uD615\uD0DC\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (\uC608: "/path/to/project")'
1567
+ };
1568
+ }
1569
+ return { cwd: getGitTopLevel(config.projectRoot) || config.projectRoot };
1570
+ }
1571
+ function isExpectedFeatureBranch(branchName, issueNumber, slug, folderName) {
1572
+ if (!branchName || !issueNumber) return false;
1573
+ const match = branchName.match(new RegExp(`^feat\\/${issueNumber}-(.+)$`));
1574
+ if (!match) return false;
1575
+ const rest = match[1];
1576
+ return rest === slug || rest === folderName;
1577
+ }
1248
1578
  function escapeRegExp(value) {
1249
1579
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1250
1580
  }
@@ -1279,15 +1609,21 @@ function parseIssueNumber(value) {
1279
1609
  const match = value.match(/#?(\d+)/);
1280
1610
  return match ? match[1] : void 0;
1281
1611
  }
1612
+ function parsePrLink(value) {
1613
+ if (!value) return void 0;
1614
+ const trimmed = value.trim();
1615
+ if (!trimmed) return void 0;
1616
+ if (trimmed === "#" || trimmed === "-") return void 0;
1617
+ if (trimmed.includes("{") || trimmed.includes("}")) return void 0;
1618
+ return trimmed;
1619
+ }
1282
1620
  function parseTasks(content) {
1283
1621
  const summary = { total: 0, todo: 0, doing: 0, done: 0 };
1284
1622
  let activeTask;
1285
1623
  let nextTodoTask;
1286
1624
  const lines = content.split("\n");
1287
1625
  for (const line of lines) {
1288
- const match = line.match(
1289
- /^\s*-\s*\[([A-Z]+)\]((?:\[[^\]]+\])*)\s*(.+?)\s*$/
1290
- );
1626
+ const match = line.match(/^\s*-\s*\[([A-Z]+)\]((?:\[[^\]]+\])*)\s*(.+?)\s*$/);
1291
1627
  if (!match) continue;
1292
1628
  const status = match[1].toUpperCase();
1293
1629
  const title = match[3].trim();
@@ -1322,75 +1658,10 @@ function parseCompletionChecklist(content) {
1322
1658
  }
1323
1659
  return total > 0 ? { total, checked } : void 0;
1324
1660
  }
1325
- function getGitStatusPorcelain(cwd, relativePaths) {
1326
- try {
1327
- const args = relativePaths.length > 0 ? ` -- ${relativePaths.map((p) => `"${p}"`).join(" ")}` : "";
1328
- return execSync(`git status --porcelain=v1${args}`, {
1329
- cwd,
1330
- encoding: "utf-8",
1331
- stdio: ["ignore", "pipe", "pipe"]
1332
- });
1333
- } catch {
1334
- return void 0;
1335
- }
1336
- }
1337
- function getGitTopLevel(cwd) {
1338
- try {
1339
- return execSync("git rev-parse --show-toplevel", {
1340
- cwd,
1341
- encoding: "utf-8",
1342
- stdio: ["ignore", "pipe", "pipe"]
1343
- }).trim();
1344
- } catch {
1345
- return null;
1346
- }
1347
- }
1348
- function resolveProjectGitCwd(config, repo) {
1349
- const docsRepo = config.docsRepo;
1350
- if (docsRepo !== "standalone") {
1351
- const topLevel = getGitTopLevel(process.cwd());
1352
- return { cwd: topLevel || process.cwd() };
1353
- }
1354
- if (!config.projectRoot) {
1355
- return {
1356
- cwd: null,
1357
- warning: "standalone \uBAA8\uB4DC\uC785\uB2C8\uB2E4. projectRoot\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC544 \uD504\uB85C\uC81D\uD2B8 \uBE0C\uB79C\uCE58 \uD655\uC778\uC774 \uBD88\uAC00\uB2A5\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)"
1358
- };
1359
- }
1360
- if (config.projectType === "fullstack") {
1361
- if (typeof config.projectRoot === "string") {
1362
- return {
1363
- cwd: null,
1364
- warning: 'fullstack standalone \uBAA8\uB4DC\uC778\uB370 projectRoot \uD615\uD0DC\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (\uC608: { "fe": "...", "be": "..." })'
1365
- };
1366
- }
1367
- const root = config.projectRoot[repo];
1368
- if (!root) {
1369
- return {
1370
- cwd: null,
1371
- warning: `projectRoot.${repo}\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ... --repo ${repo})`
1372
- };
1373
- }
1374
- return { cwd: getGitTopLevel(root) || root };
1375
- }
1376
- if (typeof config.projectRoot !== "string") {
1377
- return {
1378
- cwd: null,
1379
- warning: 'single standalone \uBAA8\uB4DC\uC778\uB370 projectRoot \uD615\uD0DC\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (\uC608: "/path/to/project")'
1380
- };
1381
- }
1382
- return { cwd: getGitTopLevel(config.projectRoot) || config.projectRoot };
1383
- }
1384
- function isExpectedFeatureBranch(branchName, issueNumber, slug, folderName) {
1385
- if (!branchName || !issueNumber) return false;
1386
- const match = branchName.match(new RegExp(`^feat\\/${issueNumber}-(.+)$`));
1387
- if (!match) return false;
1388
- const rest = match[1];
1389
- return rest === slug || rest === folderName;
1390
- }
1391
- async function parseFeature(featurePath, type, context) {
1661
+ async function parseFeature(featurePath, type, context, options) {
1662
+ const lang = options.lang;
1392
1663
  const folderName = path4.basename(featurePath);
1393
- const match = folderName.match(/^(F\d+)-(.+)$/);
1664
+ const match = folderName.match(/^(F\\d+)-(.+)$/);
1394
1665
  const id = match?.[1];
1395
1666
  const slug = match?.[2] || folderName;
1396
1667
  const specPath = path4.join(featurePath, "spec.md");
@@ -1403,11 +1674,7 @@ async function parseFeature(featurePath, type, context) {
1403
1674
  const content = await fs6.readFile(specPath, "utf-8");
1404
1675
  const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
1405
1676
  specStatus = parseDocStatus(statusValue);
1406
- const issueValue = extractFirstSpecValue(content, [
1407
- "\uC774\uC288 \uBC88\uD638",
1408
- "Issue Number",
1409
- "Issue"
1410
- ]);
1677
+ const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
1411
1678
  issueNumber = parseIssueNumber(issueValue);
1412
1679
  }
1413
1680
  let planStatus;
@@ -1422,6 +1689,10 @@ async function parseFeature(featurePath, type, context) {
1422
1689
  let activeTask;
1423
1690
  let nextTodoTask;
1424
1691
  let completionChecklist;
1692
+ let prLink;
1693
+ let prStatus;
1694
+ let prFieldExists = false;
1695
+ let prStatusFieldExists = false;
1425
1696
  if (tasksExists) {
1426
1697
  const content = await fs6.readFile(tasksPath, "utf-8");
1427
1698
  const { summary, activeTask: active, nextTodoTask: nextTodo } = parseTasks(content);
@@ -1432,12 +1703,20 @@ async function parseFeature(featurePath, type, context) {
1432
1703
  activeTask = active;
1433
1704
  nextTodoTask = nextTodo;
1434
1705
  completionChecklist = parseCompletionChecklist(content);
1706
+ if (!issueNumber) {
1707
+ const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
1708
+ issueNumber = parseIssueNumber(issueValue);
1709
+ }
1710
+ const prValue = extractFirstSpecValue(content, ["PR", "Pull Request"]);
1711
+ prFieldExists = prValue !== void 0;
1712
+ prLink = parsePrLink(prValue);
1713
+ const prStatusValue = extractFirstSpecValue(content, ["PR \uC0C1\uD0DC", "PR Status"]);
1714
+ prStatusFieldExists = prStatusValue !== void 0;
1715
+ prStatus = parseDocStatus(prStatusValue);
1435
1716
  }
1436
1717
  const warnings = [];
1437
1718
  if (context.projectBranchAvailable === false) {
1438
- warnings.push(
1439
- "\uD504\uB85C\uC81D\uD2B8 \uBE0C\uB79C\uCE58\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (standalone \uBAA8\uB4DC\uB77C\uBA74 projectRoot \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.)"
1440
- );
1719
+ warnings.push(tr(lang, "warnings", "projectBranchUnavailable"));
1441
1720
  }
1442
1721
  const onExpectedBranch = isExpectedFeatureBranch(
1443
1722
  context.projectBranch,
@@ -1446,14 +1725,13 @@ async function parseFeature(featurePath, type, context) {
1446
1725
  folderName
1447
1726
  );
1448
1727
  const relativeFeaturePathFromDocs = path4.relative(context.docsDir, featurePath);
1449
- const docsStatus = getGitStatusPorcelain(context.docsGitCwd, [
1450
- relativeFeaturePathFromDocs
1451
- ]);
1728
+ const docsStatus = getGitStatusPorcelain(context.docsGitCwd, [relativeFeaturePathFromDocs]);
1452
1729
  const docsHasUncommittedChanges = docsStatus === void 0 ? true : docsStatus.trim().length > 0;
1453
1730
  if (docsStatus === void 0) {
1454
- warnings.push(
1455
- "docs \uB808\uD3EC\uC5D0\uC11C git \uC0C1\uD0DC\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (git \uCD08\uAE30\uD654/\uB808\uD3EC \uC704\uCE58\uB97C \uD655\uC778\uD558\uC138\uC694.)"
1456
- );
1731
+ warnings.push(tr(lang, "warnings", "docsGitUnavailable"));
1732
+ }
1733
+ if (tasksExists && (!prFieldExists || !prStatusFieldExists)) {
1734
+ warnings.push(tr(lang, "warnings", "legacyTasksPrFields"));
1457
1735
  }
1458
1736
  const featureState = {
1459
1737
  id,
@@ -1468,6 +1746,7 @@ async function parseFeature(featurePath, type, context) {
1468
1746
  activeTask,
1469
1747
  nextTodoTask,
1470
1748
  completionChecklist,
1749
+ pr: { link: prLink, status: prStatus },
1471
1750
  git: {
1472
1751
  docsBranch: context.docsBranch,
1473
1752
  projectBranch: context.projectBranch,
@@ -1481,21 +1760,22 @@ async function parseFeature(featurePath, type, context) {
1481
1760
  featurePathFromDocs: relativeFeaturePathFromDocs,
1482
1761
  specExists,
1483
1762
  planExists,
1484
- tasksExists
1763
+ tasksExists,
1764
+ prFieldExists,
1765
+ prStatusFieldExists
1485
1766
  }
1486
1767
  };
1487
- const { currentStep, actions, nextAction } = resolveFeatureProgress(featureState);
1488
- return {
1489
- ...featureState,
1490
- currentStep,
1491
- actions,
1492
- nextAction,
1493
- warnings
1494
- };
1768
+ const { currentStep, actions, nextAction } = resolveFeatureProgress(
1769
+ featureState,
1770
+ options.stepDefinitions,
1771
+ lang
1772
+ );
1773
+ return { ...featureState, currentStep, actions, nextAction, warnings };
1495
1774
  }
1496
1775
  async function scanFeatures(config) {
1497
1776
  const features = [];
1498
1777
  const warnings = [];
1778
+ const stepDefinitions = getStepDefinitions(config.lang);
1499
1779
  const docsBranch = getCurrentBranch(config.docsDir);
1500
1780
  const projectBranches = {
1501
1781
  single: "",
@@ -1526,51 +1806,60 @@ async function scanFeatures(config) {
1526
1806
  for (const dir of featureDirs) {
1527
1807
  if ((await fs6.stat(dir)).isDirectory()) {
1528
1808
  features.push(
1529
- await parseFeature(dir, "single", {
1530
- projectBranch: projectBranches.single,
1531
- docsBranch,
1532
- docsGitCwd: config.docsDir,
1533
- projectGitCwd: singleProject?.cwd ?? void 0,
1534
- docsDir: config.docsDir,
1535
- projectBranchAvailable: Boolean(singleProject?.cwd)
1536
- })
1809
+ await parseFeature(
1810
+ dir,
1811
+ "single",
1812
+ {
1813
+ projectBranch: projectBranches.single,
1814
+ docsBranch,
1815
+ docsGitCwd: config.docsDir,
1816
+ projectGitCwd: singleProject?.cwd ?? void 0,
1817
+ docsDir: config.docsDir,
1818
+ projectBranchAvailable: Boolean(singleProject?.cwd)
1819
+ },
1820
+ { lang: config.lang, stepDefinitions }
1821
+ )
1537
1822
  );
1538
1823
  }
1539
1824
  }
1540
1825
  } else {
1541
- const feDirs = await glob("features/fe/*/", {
1542
- cwd: config.docsDir,
1543
- absolute: true
1544
- });
1545
- const beDirs = await glob("features/be/*/", {
1546
- cwd: config.docsDir,
1547
- absolute: true
1548
- });
1826
+ const feDirs = await glob("features/fe/*/", { cwd: config.docsDir, absolute: true });
1827
+ const beDirs = await glob("features/be/*/", { cwd: config.docsDir, absolute: true });
1549
1828
  for (const dir of feDirs) {
1550
1829
  if ((await fs6.stat(dir)).isDirectory()) {
1551
1830
  features.push(
1552
- await parseFeature(dir, "fe", {
1553
- projectBranch: projectBranches.fe,
1554
- docsBranch,
1555
- docsGitCwd: config.docsDir,
1556
- projectGitCwd: feProject?.cwd ?? void 0,
1557
- docsDir: config.docsDir,
1558
- projectBranchAvailable: Boolean(feProject?.cwd)
1559
- })
1831
+ await parseFeature(
1832
+ dir,
1833
+ "fe",
1834
+ {
1835
+ projectBranch: projectBranches.fe,
1836
+ docsBranch,
1837
+ docsGitCwd: config.docsDir,
1838
+ projectGitCwd: feProject?.cwd ?? void 0,
1839
+ docsDir: config.docsDir,
1840
+ projectBranchAvailable: Boolean(feProject?.cwd)
1841
+ },
1842
+ { lang: config.lang, stepDefinitions }
1843
+ )
1560
1844
  );
1561
1845
  }
1562
1846
  }
1563
1847
  for (const dir of beDirs) {
1564
1848
  if ((await fs6.stat(dir)).isDirectory()) {
1565
1849
  features.push(
1566
- await parseFeature(dir, "be", {
1567
- projectBranch: projectBranches.be,
1568
- docsBranch,
1569
- docsGitCwd: config.docsDir,
1570
- projectGitCwd: beProject?.cwd ?? void 0,
1571
- docsDir: config.docsDir,
1572
- projectBranchAvailable: Boolean(beProject?.cwd)
1573
- })
1850
+ await parseFeature(
1851
+ dir,
1852
+ "be",
1853
+ {
1854
+ projectBranch: projectBranches.be,
1855
+ docsBranch,
1856
+ docsGitCwd: config.docsDir,
1857
+ projectGitCwd: beProject?.cwd ?? void 0,
1858
+ docsDir: config.docsDir,
1859
+ projectBranchAvailable: Boolean(beProject?.cwd)
1860
+ },
1861
+ { lang: config.lang, stepDefinitions }
1862
+ )
1574
1863
  );
1575
1864
  }
1576
1865
  }
@@ -1579,9 +1868,7 @@ async function scanFeatures(config) {
1579
1868
  features,
1580
1869
  branches: {
1581
1870
  docs: docsBranch,
1582
- project: {
1583
- ...config.projectType === "single" ? { single: projectBranches.single } : { fe: projectBranches.fe, be: projectBranches.be }
1584
- }
1871
+ project: config.projectType === "single" ? { single: projectBranches.single } : { fe: projectBranches.fe, be: projectBranches.be }
1585
1872
  },
1586
1873
  warnings
1587
1874
  };
@@ -1628,9 +1915,12 @@ function detectFromBranch(branchName, features) {
1628
1915
  async function runContext(featureName, options) {
1629
1916
  const cwd = process.cwd();
1630
1917
  const config = await getConfig(cwd);
1918
+ const lang = config?.lang ?? "ko";
1631
1919
  if (!config) {
1632
1920
  throw new Error("\uC124\uC815 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.");
1633
1921
  }
1922
+ const stepDefinitions = getStepDefinitions(lang);
1923
+ const stepsMap = getStepsMap(lang);
1634
1924
  const { features, branches, warnings } = await scanFeatures(config);
1635
1925
  let targetFeatures = [];
1636
1926
  if (featureName) {
@@ -1728,7 +2018,7 @@ async function runContext(featureName, options) {
1728
2018
  );
1729
2019
  console.log();
1730
2020
  targetFeatures.forEach((f2) => {
1731
- const stepName2 = STEPS[f2.currentStep] || "Unknown";
2021
+ const stepName2 = stepsMap[f2.currentStep] || "Unknown";
1732
2022
  const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
1733
2023
  console.log(
1734
2024
  ` \u2022 ${chalk6.bold(f2.folderName)} ${typeStr} - ${chalk6.yellow(stepName2)}`
@@ -1743,7 +2033,8 @@ async function runContext(featureName, options) {
1743
2033
  return;
1744
2034
  }
1745
2035
  const f = targetFeatures[0];
1746
- const stepName = STEPS[f.currentStep] || "Unknown";
2036
+ const stepName = stepsMap[f.currentStep] || "Unknown";
2037
+ const okTag = (requiresUserOk) => requiresUserOk ? chalk6.yellow(lang === "ko" ? "[OK \uD544\uC694] " : "[OK required] ") : "";
1747
2038
  console.log(
1748
2039
  `\u{1F539} Feature: ${chalk6.bold(f.folderName)} ${config.projectType === "fullstack" ? chalk6.cyan(`(${f.type})`) : ""}`
1749
2040
  );
@@ -1762,12 +2053,12 @@ async function runContext(featureName, options) {
1762
2053
  console.log(
1763
2054
  ` \u2022 Active Task: ${chalk6.yellow(`[${f.activeTask.status}]`)} ${f.activeTask.title}`
1764
2055
  );
1765
- } else if (f.nextTodoTask && f.currentStep === 8) {
2056
+ } else if (f.nextTodoTask && f.currentStep === 10) {
1766
2057
  console.log(
1767
2058
  ` \u2022 Next TODO: ${chalk6.gray(`[${f.nextTodoTask.status}]`)} ${f.nextTodoTask.title}`
1768
2059
  );
1769
2060
  }
1770
- printChecklist(f);
2061
+ printChecklist(f, stepDefinitions);
1771
2062
  if (f.warnings.length > 0) {
1772
2063
  console.log();
1773
2064
  console.log(chalk6.yellow("\u26A0\uFE0F Feature Warnings:"));
@@ -1784,10 +2075,12 @@ async function runContext(featureName, options) {
1784
2075
  const action = f.actions[0];
1785
2076
  if (action.type === "command") {
1786
2077
  console.log(
1787
- `\u{1F449} Next Action (${chalk6.cyan(action.scope)}): ${chalk6.green(chalk6.bold(action.cmd))}`
2078
+ `\u{1F449} Next Action (${chalk6.cyan(action.scope)}): ${okTag(action.requiresUserOk)}${chalk6.green(chalk6.bold(action.cmd))}`
1788
2079
  );
1789
2080
  } else {
1790
- console.log(`\u{1F449} Next Action: ${chalk6.green(chalk6.bold(action.message))}`);
2081
+ console.log(
2082
+ `\u{1F449} Next Action: ${okTag(action.requiresUserOk)}${chalk6.green(chalk6.bold(action.message))}`
2083
+ );
1791
2084
  }
1792
2085
  console.log();
1793
2086
  return;
@@ -1795,15 +2088,15 @@ async function runContext(featureName, options) {
1795
2088
  console.log(chalk6.green(chalk6.bold("\u{1F449} Next Actions:")));
1796
2089
  f.actions.forEach((action) => {
1797
2090
  if (action.type === "command") {
1798
- console.log(` \u2022 (${action.scope}) ${action.cmd}`);
2091
+ console.log(` \u2022 (${action.scope}) ${okTag(action.requiresUserOk)}${action.cmd}`);
1799
2092
  } else {
1800
- console.log(` \u2022 ${action.message}`);
2093
+ console.log(` \u2022 ${okTag(action.requiresUserOk)}${action.message}`);
1801
2094
  }
1802
2095
  });
1803
2096
  console.log();
1804
2097
  }
1805
- function printChecklist(f) {
1806
- const checklistSteps = [...STEP_DEFINITIONS].sort((a, b) => a.step - b.step);
2098
+ function printChecklist(f, stepDefinitions) {
2099
+ const checklistSteps = [...stepDefinitions].sort((a, b) => a.step - b.step);
1807
2100
  checklistSteps.forEach((definition) => {
1808
2101
  const done = definition.checklist.done(f);
1809
2102
  const detail = definition.checklist.detail?.(f) ?? "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lee-spec-kit",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Project documentation structure generator for AI-assisted development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,8 @@
12
12
  - **Repo**: {{projectName}}-{be|fe}
13
13
  - **Issue**: #{issue-number}
14
14
  - **Branch**: `feat/{issue-number}-{feature-name}`
15
+ - **PR**: #
16
+ - **PR Status**: Draft | Review | Approved
15
17
 
16
18
  ---
17
19
 
@@ -12,6 +12,8 @@
12
12
  - **Repo**: {{projectName}}
13
13
  - **Issue**: #{issue-number}
14
14
  - **Branch**: `feat/{issue-number}-{feature-name}`
15
+ - **PR**: #
16
+ - **PR Status**: Draft | Review | Approved
15
17
 
16
18
  ---
17
19
 
@@ -12,6 +12,8 @@
12
12
  - **레포**: {{projectName}}-{be|fe}
13
13
  - **Issue**: #{이슈번호}
14
14
  - **브랜치**: `feat/{이슈번호}-{기능명}`
15
+ - **PR**: #
16
+ - **PR 상태**: Draft | Review | Approved
15
17
 
16
18
  ---
17
19
 
@@ -12,6 +12,8 @@
12
12
  - **레포**: {{projectName}}
13
13
  - **Issue**: #{이슈번호}
14
14
  - **브랜치**: `feat/{이슈번호}-{기능명}`
15
+ - **PR**: #
16
+ - **PR 상태**: Draft | Review | Approved
15
17
 
16
18
  ---
17
19