lee-spec-kit 0.3.0 → 0.4.1

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,458 @@ 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 && f.docs.prFieldExists && f.docs.prStatusFieldExists,
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 || f.docs.tasksExists && f.tasks.total > 0 && (!f.docs.prFieldExists || !f.docs.prStatusFieldExists)),
1234
+ actions: (f) => {
1235
+ if (f.docs.tasksExists && f.tasks.total > 0 && (!f.docs.prFieldExists || !f.docs.prStatusFieldExists)) {
1236
+ return [
1237
+ {
1238
+ type: "instruction",
1239
+ requiresUserOk: true,
1240
+ message: tr(lang, "messages", "prLegacyAsk")
1241
+ }
1242
+ ];
1243
+ }
1244
+ if (!f.docs.tasksExists) {
1245
+ return [
1246
+ {
1247
+ type: "instruction",
1248
+ message: tr(lang, "messages", "tasksCreate")
1249
+ }
1250
+ ];
1251
+ }
1093
1252
  return [
1094
1253
  {
1095
1254
  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 ...)"
1255
+ message: tr(lang, "messages", "tasksNeedAtLeastOne")
1097
1256
  }
1098
1257
  ];
1099
1258
  }
1100
- return [
1259
+ }
1260
+ },
1261
+ {
1262
+ step: 7,
1263
+ name: tr(lang, "steps", "docsCommitPlanning"),
1264
+ checklist: {
1265
+ done: (f) => f.issueNumber ? true : f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.git.docsHasUncommittedChanges
1266
+ },
1267
+ current: {
1268
+ when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.issueNumber && f.git.docsHasUncommittedChanges,
1269
+ actions: (f) => [
1101
1270
  {
1102
1271
  type: "command",
1103
- scope: "project",
1104
- cwd: f.git.projectGitCwd,
1105
- cmd: `git -C "${f.git.projectGitCwd}" checkout -b feat/${f.issueNumber}-${f.slug}`
1272
+ requiresUserOk: true,
1273
+ scope: "docs",
1274
+ cwd: f.git.docsGitCwd,
1275
+ cmd: tr(lang, "messages", "docsCommitPlanning", {
1276
+ docsGitCwd: f.git.docsGitCwd,
1277
+ featurePath: f.docs.featurePathFromDocs,
1278
+ folderName: f.folderName
1279
+ })
1106
1280
  }
1107
- ];
1281
+ ]
1108
1282
  }
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
1283
  },
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 }];
1284
+ {
1285
+ step: 8,
1286
+ name: tr(lang, "steps", "issueCreate"),
1287
+ checklist: {
1288
+ done: (f) => !!f.issueNumber && !f.git.docsHasUncommittedChanges
1289
+ },
1290
+ current: {
1291
+ when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && (!f.issueNumber || f.git.docsHasUncommittedChanges),
1292
+ actions: (f) => {
1293
+ if (!f.issueNumber) {
1294
+ return [
1295
+ {
1296
+ type: "instruction",
1297
+ requiresUserOk: true,
1298
+ message: tr(lang, "messages", "issueCreateAndWrite")
1299
+ }
1300
+ ];
1301
+ }
1302
+ return [
1303
+ {
1304
+ type: "command",
1305
+ requiresUserOk: true,
1306
+ scope: "docs",
1307
+ cwd: f.git.docsGitCwd,
1308
+ cmd: tr(lang, "messages", "docsCommitIssueUpdate", {
1309
+ docsGitCwd: f.git.docsGitCwd,
1310
+ featurePath: f.docs.featurePathFromDocs,
1311
+ issueNumber: f.issueNumber,
1312
+ folderName: f.folderName
1313
+ })
1314
+ }
1315
+ ];
1161
1316
  }
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 }];
1317
+ }
1318
+ },
1319
+ {
1320
+ step: 9,
1321
+ name: tr(lang, "steps", "branchCreate"),
1322
+ checklist: { done: (f) => f.git.onExpectedBranch },
1323
+ current: {
1324
+ when: (f) => !!f.issueNumber && (!f.git.projectBranchAvailable || !f.git.onExpectedBranch),
1325
+ actions: (f) => {
1326
+ if (!f.git.projectBranchAvailable || !f.git.projectGitCwd) {
1327
+ return [
1328
+ {
1329
+ type: "instruction",
1330
+ message: tr(lang, "messages", "standaloneNeedsProjectRoot")
1331
+ }
1332
+ ];
1333
+ }
1334
+ return [
1335
+ {
1336
+ type: "command",
1337
+ scope: "project",
1338
+ cwd: f.git.projectGitCwd,
1339
+ cmd: tr(lang, "messages", "createBranch", {
1340
+ projectGitCwd: f.git.projectGitCwd,
1341
+ issueNumber: f.issueNumber,
1342
+ slug: f.slug
1343
+ })
1344
+ }
1345
+ ];
1165
1346
  }
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
1347
  }
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
1348
  },
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})`
1349
+ {
1350
+ step: 10,
1351
+ name: tr(lang, "steps", "tasksExecute"),
1352
+ checklist: {
1353
+ done: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.tasks.total === f.tasks.done && isCompletionChecklistDone(f),
1354
+ detail: (f) => f.tasks.total > 0 ? `(${f.tasks.done}/${f.tasks.total})` : ""
1355
+ },
1356
+ current: {
1357
+ when: (f) => f.git.onExpectedBranch && f.docs.tasksExists && f.tasks.total > 0 && (f.tasks.done < f.tasks.total || !isCompletionChecklistDone(f)),
1358
+ actions: (f) => {
1359
+ if (f.tasks.total === f.tasks.done && !isCompletionChecklistDone(f)) {
1360
+ return [
1361
+ {
1362
+ type: "instruction",
1363
+ requiresUserOk: true,
1364
+ message: !f.completionChecklist ? tr(lang, "messages", "tasksAllDoneButNoChecklist") : tr(lang, "messages", "tasksAllDoneButChecklist", {
1365
+ checked: f.completionChecklist.checked,
1366
+ total: f.completionChecklist.total
1367
+ })
1368
+ }
1369
+ ];
1370
+ }
1371
+ if (f.activeTask) {
1372
+ return [
1373
+ {
1374
+ type: "instruction",
1375
+ requiresUserOk: true,
1376
+ message: tr(lang, "messages", "finishDoingTask", {
1377
+ title: f.activeTask.title,
1378
+ done: f.tasks.done,
1379
+ total: f.tasks.total
1380
+ })
1381
+ }
1382
+ ];
1383
+ }
1384
+ if (f.nextTodoTask) {
1385
+ return [
1386
+ {
1387
+ type: "instruction",
1388
+ requiresUserOk: true,
1389
+ message: tr(lang, "messages", "startNextTodoTask", {
1390
+ title: f.nextTodoTask.title,
1391
+ done: f.tasks.done,
1392
+ total: f.tasks.total
1393
+ })
1394
+ }
1395
+ ];
1396
+ }
1397
+ return [
1398
+ {
1399
+ type: "instruction",
1400
+ requiresUserOk: true,
1401
+ message: tr(lang, "messages", "checkTaskStatuses", {
1402
+ done: f.tasks.done,
1403
+ total: f.tasks.total
1404
+ })
1405
+ }
1406
+ ];
1184
1407
  }
1185
- ]
1186
- }
1187
- },
1188
- {
1189
- step: 10,
1190
- name: "\uBB38\uC11C \uCEE4\uBC0B",
1191
- checklist: {
1192
- done: (f) => isCompletionChecklistDone(f) && !f.git.docsHasUncommittedChanges
1408
+ }
1193
1409
  },
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"`
1410
+ {
1411
+ step: 11,
1412
+ name: tr(lang, "steps", "prCreate"),
1413
+ checklist: { done: (f) => isPrMetadataConfigured(f) && !!f.pr.link },
1414
+ current: {
1415
+ when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.tasks.total === f.tasks.done && isCompletionChecklistDone(f) && (!isPrMetadataConfigured(f) || !f.pr.link),
1416
+ actions: (f) => {
1417
+ if (!isPrMetadataConfigured(f)) {
1418
+ return [
1419
+ {
1420
+ type: "instruction",
1421
+ requiresUserOk: true,
1422
+ message: tr(lang, "messages", "prLegacyAsk")
1423
+ }
1424
+ ];
1425
+ }
1426
+ return [
1427
+ {
1428
+ type: "instruction",
1429
+ requiresUserOk: true,
1430
+ message: tr(lang, "messages", "prCreate")
1431
+ }
1432
+ ];
1202
1433
  }
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."
1434
+ }
1435
+ },
1436
+ {
1437
+ step: 12,
1438
+ name: tr(lang, "steps", "codeReview"),
1439
+ checklist: {
1440
+ done: (f) => isPrMetadataConfigured(f) && f.pr.status === "Approved"
1441
+ },
1442
+ current: {
1443
+ when: (f) => isPrMetadataConfigured(f) && !!f.pr.link && f.pr.status !== "Approved",
1444
+ actions: (f) => {
1445
+ if (f.pr.status === "Review") {
1446
+ return [
1447
+ {
1448
+ type: "instruction",
1449
+ message: tr(lang, "messages", "prResolveReview")
1450
+ }
1451
+ ];
1452
+ }
1453
+ return [
1454
+ {
1455
+ type: "instruction",
1456
+ message: tr(lang, "messages", "prRequestReview")
1457
+ }
1458
+ ];
1207
1459
  }
1208
- ]
1460
+ }
1461
+ },
1462
+ {
1463
+ step: 13,
1464
+ name: tr(lang, "steps", "featureDone"),
1465
+ checklist: { done: (f) => isFeatureDone(f) },
1466
+ current: {
1467
+ when: (f) => isFeatureDone(f),
1468
+ actions: () => [
1469
+ {
1470
+ type: "instruction",
1471
+ message: tr(lang, "messages", "featureDone")
1472
+ }
1473
+ ]
1474
+ }
1209
1475
  }
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);
1476
+ ];
1477
+ }
1478
+ function getStepsMap(lang) {
1479
+ return Object.fromEntries(getStepDefinitions(lang).map((d) => [d.step, d.name]));
1480
+ }
1481
+ getStepDefinitions("ko");
1482
+ getStepsMap("ko");
1483
+
1484
+ // src/utils/context/progress.ts
1485
+ function resolveFeatureProgress(feature, stepDefinitions, lang) {
1486
+ const ordered = [...stepDefinitions].sort((a, b) => a.step - b.step);
1217
1487
  for (const definition of ordered) {
1218
1488
  if (!definition.current) continue;
1219
1489
  if (definition.current.when(feature)) {
@@ -1229,9 +1499,12 @@ function resolveFeatureProgress(feature) {
1229
1499
  return {
1230
1500
  currentStep: lastStep?.step ?? 10,
1231
1501
  actions: [
1232
- { type: "instruction", message: "npx lee-spec-kit context\uB85C \uC0C1\uD0DC\uB97C \uB2E4\uC2DC \uD655\uC778\uD558\uC138\uC694." }
1502
+ {
1503
+ type: "instruction",
1504
+ message: tr(lang, "messages", "fallbackRerunContext")
1505
+ }
1233
1506
  ],
1234
- nextAction: "npx lee-spec-kit context\uB85C \uC0C1\uD0DC\uB97C \uB2E4\uC2DC \uD655\uC778\uD558\uC138\uC694."
1507
+ nextAction: tr(lang, "messages", "fallbackRerunContext")
1235
1508
  };
1236
1509
  }
1237
1510
  function getCurrentBranch(cwd) {
@@ -1245,6 +1518,72 @@ function getCurrentBranch(cwd) {
1245
1518
  return "";
1246
1519
  }
1247
1520
  }
1521
+ function getGitStatusPorcelain(cwd, relativePaths) {
1522
+ try {
1523
+ const args = relativePaths.length > 0 ? ` -- ${relativePaths.map((p) => `"${p}"`).join(" ")}` : "";
1524
+ return execSync(`git status --porcelain=v1${args}`, {
1525
+ cwd,
1526
+ encoding: "utf-8",
1527
+ stdio: ["ignore", "pipe", "pipe"]
1528
+ });
1529
+ } catch {
1530
+ return void 0;
1531
+ }
1532
+ }
1533
+ function getGitTopLevel(cwd) {
1534
+ try {
1535
+ return execSync("git rev-parse --show-toplevel", {
1536
+ cwd,
1537
+ encoding: "utf-8",
1538
+ stdio: ["ignore", "pipe", "pipe"]
1539
+ }).trim();
1540
+ } catch {
1541
+ return null;
1542
+ }
1543
+ }
1544
+ function resolveProjectGitCwd(config, repo) {
1545
+ const docsRepo = config.docsRepo;
1546
+ if (docsRepo !== "standalone") {
1547
+ const topLevel = getGitTopLevel(process.cwd());
1548
+ return { cwd: topLevel || process.cwd() };
1549
+ }
1550
+ if (!config.projectRoot) {
1551
+ return {
1552
+ cwd: null,
1553
+ 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 ...)"
1554
+ };
1555
+ }
1556
+ if (config.projectType === "fullstack") {
1557
+ if (typeof config.projectRoot === "string") {
1558
+ return {
1559
+ cwd: null,
1560
+ warning: 'fullstack standalone \uBAA8\uB4DC\uC778\uB370 projectRoot \uD615\uD0DC\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (\uC608: { "fe": "...", "be": "..." })'
1561
+ };
1562
+ }
1563
+ const root = config.projectRoot[repo];
1564
+ if (!root) {
1565
+ return {
1566
+ cwd: null,
1567
+ warning: `projectRoot.${repo}\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ... --repo ${repo})`
1568
+ };
1569
+ }
1570
+ return { cwd: getGitTopLevel(root) || root };
1571
+ }
1572
+ if (typeof config.projectRoot !== "string") {
1573
+ return {
1574
+ cwd: null,
1575
+ warning: 'single standalone \uBAA8\uB4DC\uC778\uB370 projectRoot \uD615\uD0DC\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. (\uC608: "/path/to/project")'
1576
+ };
1577
+ }
1578
+ return { cwd: getGitTopLevel(config.projectRoot) || config.projectRoot };
1579
+ }
1580
+ function isExpectedFeatureBranch(branchName, issueNumber, slug, folderName) {
1581
+ if (!branchName || !issueNumber) return false;
1582
+ const match = branchName.match(new RegExp(`^feat\\/${issueNumber}-(.+)$`));
1583
+ if (!match) return false;
1584
+ const rest = match[1];
1585
+ return rest === slug || rest === folderName;
1586
+ }
1248
1587
  function escapeRegExp(value) {
1249
1588
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1250
1589
  }
@@ -1279,15 +1618,21 @@ function parseIssueNumber(value) {
1279
1618
  const match = value.match(/#?(\d+)/);
1280
1619
  return match ? match[1] : void 0;
1281
1620
  }
1621
+ function parsePrLink(value) {
1622
+ if (!value) return void 0;
1623
+ const trimmed = value.trim();
1624
+ if (!trimmed) return void 0;
1625
+ if (trimmed === "#" || trimmed === "-") return void 0;
1626
+ if (trimmed.includes("{") || trimmed.includes("}")) return void 0;
1627
+ return trimmed;
1628
+ }
1282
1629
  function parseTasks(content) {
1283
1630
  const summary = { total: 0, todo: 0, doing: 0, done: 0 };
1284
1631
  let activeTask;
1285
1632
  let nextTodoTask;
1286
1633
  const lines = content.split("\n");
1287
1634
  for (const line of lines) {
1288
- const match = line.match(
1289
- /^\s*-\s*\[([A-Z]+)\]((?:\[[^\]]+\])*)\s*(.+?)\s*$/
1290
- );
1635
+ const match = line.match(/^\s*-\s*\[([A-Z]+)\]((?:\[[^\]]+\])*)\s*(.+?)\s*$/);
1291
1636
  if (!match) continue;
1292
1637
  const status = match[1].toUpperCase();
1293
1638
  const title = match[3].trim();
@@ -1322,73 +1667,8 @@ function parseCompletionChecklist(content) {
1322
1667
  }
1323
1668
  return total > 0 ? { total, checked } : void 0;
1324
1669
  }
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) {
1670
+ async function parseFeature(featurePath, type, context, options) {
1671
+ const lang = options.lang;
1392
1672
  const folderName = path4.basename(featurePath);
1393
1673
  const match = folderName.match(/^(F\d+)-(.+)$/);
1394
1674
  const id = match?.[1];
@@ -1403,11 +1683,7 @@ async function parseFeature(featurePath, type, context) {
1403
1683
  const content = await fs6.readFile(specPath, "utf-8");
1404
1684
  const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
1405
1685
  specStatus = parseDocStatus(statusValue);
1406
- const issueValue = extractFirstSpecValue(content, [
1407
- "\uC774\uC288 \uBC88\uD638",
1408
- "Issue Number",
1409
- "Issue"
1410
- ]);
1686
+ const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
1411
1687
  issueNumber = parseIssueNumber(issueValue);
1412
1688
  }
1413
1689
  let planStatus;
@@ -1422,6 +1698,10 @@ async function parseFeature(featurePath, type, context) {
1422
1698
  let activeTask;
1423
1699
  let nextTodoTask;
1424
1700
  let completionChecklist;
1701
+ let prLink;
1702
+ let prStatus;
1703
+ let prFieldExists = false;
1704
+ let prStatusFieldExists = false;
1425
1705
  if (tasksExists) {
1426
1706
  const content = await fs6.readFile(tasksPath, "utf-8");
1427
1707
  const { summary, activeTask: active, nextTodoTask: nextTodo } = parseTasks(content);
@@ -1432,12 +1712,20 @@ async function parseFeature(featurePath, type, context) {
1432
1712
  activeTask = active;
1433
1713
  nextTodoTask = nextTodo;
1434
1714
  completionChecklist = parseCompletionChecklist(content);
1715
+ if (!issueNumber) {
1716
+ const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
1717
+ issueNumber = parseIssueNumber(issueValue);
1718
+ }
1719
+ const prValue = extractFirstSpecValue(content, ["PR", "Pull Request"]);
1720
+ prFieldExists = prValue !== void 0;
1721
+ prLink = parsePrLink(prValue);
1722
+ const prStatusValue = extractFirstSpecValue(content, ["PR \uC0C1\uD0DC", "PR Status"]);
1723
+ prStatusFieldExists = prStatusValue !== void 0;
1724
+ prStatus = parseDocStatus(prStatusValue);
1435
1725
  }
1436
1726
  const warnings = [];
1437
1727
  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
- );
1728
+ warnings.push(tr(lang, "warnings", "projectBranchUnavailable"));
1441
1729
  }
1442
1730
  const onExpectedBranch = isExpectedFeatureBranch(
1443
1731
  context.projectBranch,
@@ -1446,14 +1734,13 @@ async function parseFeature(featurePath, type, context) {
1446
1734
  folderName
1447
1735
  );
1448
1736
  const relativeFeaturePathFromDocs = path4.relative(context.docsDir, featurePath);
1449
- const docsStatus = getGitStatusPorcelain(context.docsGitCwd, [
1450
- relativeFeaturePathFromDocs
1451
- ]);
1737
+ const docsStatus = getGitStatusPorcelain(context.docsGitCwd, [relativeFeaturePathFromDocs]);
1452
1738
  const docsHasUncommittedChanges = docsStatus === void 0 ? true : docsStatus.trim().length > 0;
1453
1739
  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
- );
1740
+ warnings.push(tr(lang, "warnings", "docsGitUnavailable"));
1741
+ }
1742
+ if (tasksExists && (!prFieldExists || !prStatusFieldExists)) {
1743
+ warnings.push(tr(lang, "warnings", "legacyTasksPrFields"));
1457
1744
  }
1458
1745
  const featureState = {
1459
1746
  id,
@@ -1468,6 +1755,7 @@ async function parseFeature(featurePath, type, context) {
1468
1755
  activeTask,
1469
1756
  nextTodoTask,
1470
1757
  completionChecklist,
1758
+ pr: { link: prLink, status: prStatus },
1471
1759
  git: {
1472
1760
  docsBranch: context.docsBranch,
1473
1761
  projectBranch: context.projectBranch,
@@ -1481,21 +1769,22 @@ async function parseFeature(featurePath, type, context) {
1481
1769
  featurePathFromDocs: relativeFeaturePathFromDocs,
1482
1770
  specExists,
1483
1771
  planExists,
1484
- tasksExists
1772
+ tasksExists,
1773
+ prFieldExists,
1774
+ prStatusFieldExists
1485
1775
  }
1486
1776
  };
1487
- const { currentStep, actions, nextAction } = resolveFeatureProgress(featureState);
1488
- return {
1489
- ...featureState,
1490
- currentStep,
1491
- actions,
1492
- nextAction,
1493
- warnings
1494
- };
1777
+ const { currentStep, actions, nextAction } = resolveFeatureProgress(
1778
+ featureState,
1779
+ options.stepDefinitions,
1780
+ lang
1781
+ );
1782
+ return { ...featureState, currentStep, actions, nextAction, warnings };
1495
1783
  }
1496
1784
  async function scanFeatures(config) {
1497
1785
  const features = [];
1498
1786
  const warnings = [];
1787
+ const stepDefinitions = getStepDefinitions(config.lang);
1499
1788
  const docsBranch = getCurrentBranch(config.docsDir);
1500
1789
  const projectBranches = {
1501
1790
  single: "",
@@ -1526,51 +1815,60 @@ async function scanFeatures(config) {
1526
1815
  for (const dir of featureDirs) {
1527
1816
  if ((await fs6.stat(dir)).isDirectory()) {
1528
1817
  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
- })
1818
+ await parseFeature(
1819
+ dir,
1820
+ "single",
1821
+ {
1822
+ projectBranch: projectBranches.single,
1823
+ docsBranch,
1824
+ docsGitCwd: config.docsDir,
1825
+ projectGitCwd: singleProject?.cwd ?? void 0,
1826
+ docsDir: config.docsDir,
1827
+ projectBranchAvailable: Boolean(singleProject?.cwd)
1828
+ },
1829
+ { lang: config.lang, stepDefinitions }
1830
+ )
1537
1831
  );
1538
1832
  }
1539
1833
  }
1540
1834
  } 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
- });
1835
+ const feDirs = await glob("features/fe/*/", { cwd: config.docsDir, absolute: true });
1836
+ const beDirs = await glob("features/be/*/", { cwd: config.docsDir, absolute: true });
1549
1837
  for (const dir of feDirs) {
1550
1838
  if ((await fs6.stat(dir)).isDirectory()) {
1551
1839
  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
- })
1840
+ await parseFeature(
1841
+ dir,
1842
+ "fe",
1843
+ {
1844
+ projectBranch: projectBranches.fe,
1845
+ docsBranch,
1846
+ docsGitCwd: config.docsDir,
1847
+ projectGitCwd: feProject?.cwd ?? void 0,
1848
+ docsDir: config.docsDir,
1849
+ projectBranchAvailable: Boolean(feProject?.cwd)
1850
+ },
1851
+ { lang: config.lang, stepDefinitions }
1852
+ )
1560
1853
  );
1561
1854
  }
1562
1855
  }
1563
1856
  for (const dir of beDirs) {
1564
1857
  if ((await fs6.stat(dir)).isDirectory()) {
1565
1858
  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
- })
1859
+ await parseFeature(
1860
+ dir,
1861
+ "be",
1862
+ {
1863
+ projectBranch: projectBranches.be,
1864
+ docsBranch,
1865
+ docsGitCwd: config.docsDir,
1866
+ projectGitCwd: beProject?.cwd ?? void 0,
1867
+ docsDir: config.docsDir,
1868
+ projectBranchAvailable: Boolean(beProject?.cwd)
1869
+ },
1870
+ { lang: config.lang, stepDefinitions }
1871
+ )
1574
1872
  );
1575
1873
  }
1576
1874
  }
@@ -1579,9 +1877,7 @@ async function scanFeatures(config) {
1579
1877
  features,
1580
1878
  branches: {
1581
1879
  docs: docsBranch,
1582
- project: {
1583
- ...config.projectType === "single" ? { single: projectBranches.single } : { fe: projectBranches.fe, be: projectBranches.be }
1584
- }
1880
+ project: config.projectType === "single" ? { single: projectBranches.single } : { fe: projectBranches.fe, be: projectBranches.be }
1585
1881
  },
1586
1882
  warnings
1587
1883
  };
@@ -1628,9 +1924,12 @@ function detectFromBranch(branchName, features) {
1628
1924
  async function runContext(featureName, options) {
1629
1925
  const cwd = process.cwd();
1630
1926
  const config = await getConfig(cwd);
1927
+ const lang = config?.lang ?? "ko";
1631
1928
  if (!config) {
1632
1929
  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
1930
  }
1931
+ const stepDefinitions = getStepDefinitions(lang);
1932
+ const stepsMap = getStepsMap(lang);
1634
1933
  const { features, branches, warnings } = await scanFeatures(config);
1635
1934
  let targetFeatures = [];
1636
1935
  if (featureName) {
@@ -1728,7 +2027,7 @@ async function runContext(featureName, options) {
1728
2027
  );
1729
2028
  console.log();
1730
2029
  targetFeatures.forEach((f2) => {
1731
- const stepName2 = STEPS[f2.currentStep] || "Unknown";
2030
+ const stepName2 = stepsMap[f2.currentStep] || "Unknown";
1732
2031
  const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
1733
2032
  console.log(
1734
2033
  ` \u2022 ${chalk6.bold(f2.folderName)} ${typeStr} - ${chalk6.yellow(stepName2)}`
@@ -1743,7 +2042,8 @@ async function runContext(featureName, options) {
1743
2042
  return;
1744
2043
  }
1745
2044
  const f = targetFeatures[0];
1746
- const stepName = STEPS[f.currentStep] || "Unknown";
2045
+ const stepName = stepsMap[f.currentStep] || "Unknown";
2046
+ const okTag = (requiresUserOk) => requiresUserOk ? chalk6.yellow(lang === "ko" ? "[OK \uD544\uC694] " : "[OK required] ") : "";
1747
2047
  console.log(
1748
2048
  `\u{1F539} Feature: ${chalk6.bold(f.folderName)} ${config.projectType === "fullstack" ? chalk6.cyan(`(${f.type})`) : ""}`
1749
2049
  );
@@ -1762,12 +2062,12 @@ async function runContext(featureName, options) {
1762
2062
  console.log(
1763
2063
  ` \u2022 Active Task: ${chalk6.yellow(`[${f.activeTask.status}]`)} ${f.activeTask.title}`
1764
2064
  );
1765
- } else if (f.nextTodoTask && f.currentStep === 8) {
2065
+ } else if (f.nextTodoTask && f.currentStep === 10) {
1766
2066
  console.log(
1767
2067
  ` \u2022 Next TODO: ${chalk6.gray(`[${f.nextTodoTask.status}]`)} ${f.nextTodoTask.title}`
1768
2068
  );
1769
2069
  }
1770
- printChecklist(f);
2070
+ printChecklist(f, stepDefinitions);
1771
2071
  if (f.warnings.length > 0) {
1772
2072
  console.log();
1773
2073
  console.log(chalk6.yellow("\u26A0\uFE0F Feature Warnings:"));
@@ -1784,10 +2084,12 @@ async function runContext(featureName, options) {
1784
2084
  const action = f.actions[0];
1785
2085
  if (action.type === "command") {
1786
2086
  console.log(
1787
- `\u{1F449} Next Action (${chalk6.cyan(action.scope)}): ${chalk6.green(chalk6.bold(action.cmd))}`
2087
+ `\u{1F449} Next Action (${chalk6.cyan(action.scope)}): ${okTag(action.requiresUserOk)}${chalk6.green(chalk6.bold(action.cmd))}`
1788
2088
  );
1789
2089
  } else {
1790
- console.log(`\u{1F449} Next Action: ${chalk6.green(chalk6.bold(action.message))}`);
2090
+ console.log(
2091
+ `\u{1F449} Next Action: ${okTag(action.requiresUserOk)}${chalk6.green(chalk6.bold(action.message))}`
2092
+ );
1791
2093
  }
1792
2094
  console.log();
1793
2095
  return;
@@ -1795,15 +2097,15 @@ async function runContext(featureName, options) {
1795
2097
  console.log(chalk6.green(chalk6.bold("\u{1F449} Next Actions:")));
1796
2098
  f.actions.forEach((action) => {
1797
2099
  if (action.type === "command") {
1798
- console.log(` \u2022 (${action.scope}) ${action.cmd}`);
2100
+ console.log(` \u2022 (${action.scope}) ${okTag(action.requiresUserOk)}${action.cmd}`);
1799
2101
  } else {
1800
- console.log(` \u2022 ${action.message}`);
2102
+ console.log(` \u2022 ${okTag(action.requiresUserOk)}${action.message}`);
1801
2103
  }
1802
2104
  });
1803
2105
  console.log();
1804
2106
  }
1805
- function printChecklist(f) {
1806
- const checklistSteps = [...STEP_DEFINITIONS].sort((a, b) => a.step - b.step);
2107
+ function printChecklist(f, stepDefinitions) {
2108
+ const checklistSteps = [...stepDefinitions].sort((a, b) => a.step - b.step);
1807
2109
  checklistSteps.forEach((definition) => {
1808
2110
  const done = definition.checklist.done(f);
1809
2111
  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.1",
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