lee-spec-kit 0.6.7 → 0.6.8

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.
Files changed (48) hide show
  1. package/README.en.md +84 -51
  2. package/README.md +84 -57
  3. package/dist/index.js +1096 -305
  4. package/package.json +1 -1
  5. package/templates/en/{single → common}/README.md +10 -10
  6. package/templates/en/common/agents/agents.md +77 -0
  7. package/templates/en/common/agents/skills/create-feature.md +1 -0
  8. package/templates/en/common/agents/skills/create-pr.md +9 -9
  9. package/templates/en/common/agents/skills/execute-task.md +1 -0
  10. package/templates/en/{fullstack → common}/features/README.md +8 -16
  11. package/templates/en/{fullstack → common}/features/feature-base/decisions.md +3 -0
  12. package/templates/ko/{single → common}/README.md +9 -9
  13. package/templates/ko/common/agents/agents.md +78 -0
  14. package/templates/ko/common/agents/skills/create-feature.md +1 -0
  15. package/templates/ko/common/agents/skills/create-pr.md +9 -9
  16. package/templates/ko/common/agents/skills/execute-task.md +1 -0
  17. package/templates/ko/{fullstack → common}/features/README.md +7 -37
  18. package/templates/ko/{fullstack → common}/features/feature-base/decisions.md +3 -0
  19. package/templates/en/fullstack/README.md +0 -60
  20. package/templates/en/fullstack/agents/agents.md +0 -116
  21. package/templates/en/fullstack/features/be/README.md +0 -5
  22. package/templates/en/fullstack/features/fe/README.md +0 -5
  23. package/templates/en/fullstack/prd/README.md +0 -20
  24. package/templates/en/single/agents/agents.md +0 -103
  25. package/templates/en/single/features/README.md +0 -56
  26. package/templates/en/single/features/feature-base/decisions.md +0 -15
  27. package/templates/en/single/features/feature-base/plan.md +0 -48
  28. package/templates/en/single/features/feature-base/spec.md +0 -57
  29. package/templates/en/single/features/feature-base/tasks.md +0 -60
  30. package/templates/ko/fullstack/README.md +0 -60
  31. package/templates/ko/fullstack/agents/agents.md +0 -146
  32. package/templates/ko/fullstack/features/be/README.md +0 -5
  33. package/templates/ko/fullstack/features/fe/README.md +0 -5
  34. package/templates/ko/single/agents/agents.md +0 -116
  35. package/templates/ko/single/features/README.md +0 -56
  36. package/templates/ko/single/features/feature-base/decisions.md +0 -15
  37. package/templates/ko/single/features/feature-base/plan.md +0 -48
  38. package/templates/ko/single/features/feature-base/spec.md +0 -57
  39. package/templates/ko/single/features/feature-base/tasks.md +0 -60
  40. package/templates/ko/single/prd/README.md +0 -20
  41. /package/templates/en/{fullstack → common}/features/feature-base/plan.md +0 -0
  42. /package/templates/en/{fullstack → common}/features/feature-base/spec.md +0 -0
  43. /package/templates/en/{fullstack → common}/features/feature-base/tasks.md +0 -0
  44. /package/templates/en/{single → common}/prd/README.md +0 -0
  45. /package/templates/ko/{fullstack → common}/features/feature-base/plan.md +0 -0
  46. /package/templates/ko/{fullstack → common}/features/feature-base/spec.md +0 -0
  47. /package/templates/ko/{fullstack → common}/features/feature-base/tasks.md +0 -0
  48. /package/templates/ko/{fullstack → common}/prd/README.md +0 -0
package/dist/index.js CHANGED
@@ -2,19 +2,19 @@
2
2
  import path18 from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { program } from 'commander';
5
- import fs14 from 'fs-extra';
5
+ import fs15 from 'fs-extra';
6
6
  import prompts from 'prompts';
7
7
  import chalk6 from 'chalk';
8
8
  import { glob } from 'glob';
9
- import { spawn, execSync, spawnSync, execFileSync } from 'child_process';
9
+ import { spawn, execFileSync, execSync, spawnSync } from 'child_process';
10
10
  import os from 'os';
11
- import { createHash } from 'crypto';
11
+ import { createHash, randomUUID } from 'crypto';
12
12
 
13
13
  var getFilename = () => fileURLToPath(import.meta.url);
14
14
  var getDirname = () => path18.dirname(getFilename());
15
15
  var __dirname$1 = /* @__PURE__ */ getDirname();
16
16
  async function copyTemplates(src, dest) {
17
- await fs14.copy(src, dest, {
17
+ await fs15.copy(src, dest, {
18
18
  overwrite: true,
19
19
  errorOnExist: false
20
20
  });
@@ -30,15 +30,15 @@ function applyReplacements(content, replacements) {
30
30
  async function replaceInFiles(dir, replacements) {
31
31
  const files = await glob("**/*.md", { cwd: dir, absolute: true });
32
32
  for (const file of files) {
33
- let content = await fs14.readFile(file, "utf-8");
33
+ let content = await fs15.readFile(file, "utf-8");
34
34
  content = applyReplacements(content, replacements);
35
- await fs14.writeFile(file, content, "utf-8");
35
+ await fs15.writeFile(file, content, "utf-8");
36
36
  }
37
37
  const shFiles = await glob("**/*.sh", { cwd: dir, absolute: true });
38
38
  for (const file of shFiles) {
39
- let content = await fs14.readFile(file, "utf-8");
39
+ let content = await fs15.readFile(file, "utf-8");
40
40
  content = applyReplacements(content, replacements);
41
- await fs14.writeFile(file, content, "utf-8");
41
+ await fs15.writeFile(file, content, "utf-8");
42
42
  }
43
43
  }
44
44
  var __filename2 = fileURLToPath(import.meta.url);
@@ -83,7 +83,7 @@ var I18N = {
83
83
  "config.pathLabel": "\uACBD\uB85C",
84
84
  "config.projectRootStandaloneOnly": "\u26A0\uFE0F projectRoot\uB294 standalone \uBAA8\uB4DC\uC5D0\uC11C\uB9CC \uC124\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.",
85
85
  "config.selectRepoToUpdate": "\uC218\uC815\uD560 \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
86
- "config.fullstackRepoRequired": "Multi \uD504\uB85C\uC81D\uD2B8\uB294 --repo \uB610\uB294 --component\uB85C \uB300\uC0C1 \uCEF4\uD3EC\uB10C\uD2B8\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.",
86
+ "config.fullstackRepoRequired": "Multi \uD504\uB85C\uC81D\uD2B8\uB294 --component\uB85C \uB300\uC0C1 \uCEF4\uD3EC\uB10C\uD2B8\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.",
87
87
  "config.projectRootSet": "\u2705 {repo} projectRoot \uC124\uC815 \uC644\uB8CC: {path}",
88
88
  "config.projectRootSetSingle": "\u2705 projectRoot \uC124\uC815 \uC644\uB8CC: {path}",
89
89
  "update.start": "\u{1F4E6} \uD15C\uD50C\uB9BF \uC5C5\uB370\uC774\uD2B8\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4...",
@@ -132,10 +132,11 @@ var I18N = {
132
132
  "context.tipShowDone": "\uC644\uB8CC\uB9CC \uBCF4\uAE30",
133
133
  "context.checkRequired": "[\uD655\uC778 \uD544\uC694] ",
134
134
  "context.checkPolicyHint": "\u2139\uFE0F \uC0AC\uC6A9\uC790 \uD655\uC778 \uC815\uCC45\uC740 `npx lee-spec-kit docs get agents --json`\uC73C\uB85C \uBA3C\uC800 \uD655\uC778\uD558\uC138\uC694. (git push/merge/merge commit \uD3EC\uD568) [\uD655\uC778 \uD544\uC694]\uAC00 \uC788\uC73C\uBA74 \uC0AC\uC6A9\uC790\uC5D0\uAC8C `<\uB77C\uBCA8>` \uB610\uB294 `<\uB77C\uBCA8> OK` (\uC608: `A`, `A OK`) \uC751\uB2F5\uC744 \uBC1B\uC740 \uB4A4 \uC9C4\uD589 (config: approval\uB85C \uC870\uC815 \uAC00\uB2A5)",
135
- "context.actionOptionHint": "\uC2B9\uC778 \uC751\uB2F5 \uD615\uC2DD: `<\uB77C\uBCA8>` \uB610\uB294 `<\uB77C\uBCA8> OK` (\uC608: `A`, `A OK`)",
135
+ "context.actionOptionHint": "\uC2B9\uC778 \uC751\uB2F5 \uD615\uC2DD: \uB77C\uBCA8 \uD1A0\uD070 \uD3EC\uD568 (\uC608: `A`, `A OK`, `A \uC9C4\uD589\uD574`)",
136
136
  "context.actionExplainHint": "\uC2B9\uC778 \uC694\uCCAD \uC804, \uAC01 \uB77C\uBCA8\uC774 \uBB34\uC5C7\uC744 \uC2E4\uD589/\uBCC0\uACBD\uD558\uB294\uC9C0 \uD55C \uC904 \uC694\uC57D\uACFC \uD568\uAED8 \uC124\uBA85\uD558\uC138\uC694.",
137
137
  "context.finalLabelPrompt": "\uD604\uC7AC \uC120\uD0DD \uAC00\uB2A5\uD55C \uB77C\uBCA8: {labels}. \uB9C8\uC9C0\uB9C9 \uC751\uB2F5\uC740 `<\uB77C\uBCA8>` \uB610\uB294 `<\uB77C\uBCA8> OK` \uD615\uC2DD\uC73C\uB85C \uBC1B\uC73C\uC138\uC694. (\uC608: {example})",
138
- "context.finalLabelCommandHint": "\uB77C\uBCA8\uC744 \uBC1B\uC73C\uBA74 \uBC14\uB85C \uC2E4\uD589: {command}",
138
+ "context.finalLabelCommandHint": "\uB77C\uBCA8\uC744 \uBC1B\uC73C\uBA74 \uC2B9\uC778 \uC120\uD0DD \uC2E4\uD589: {command}",
139
+ "context.finalTicketCommandHint": "\uBA85\uB839 \uC2E4\uD589\uC740 \uC2B9\uC778 \uACB0\uACFC\uC758 \uD2F0\uCF13\uC73C\uB85C \uC2E4\uD589: {command}",
139
140
  "context.readBuiltinDocFirst": "\uBA3C\uC800 \uB0B4\uC7A5 \uBB38\uC11C\uB97C \uD655\uC778\uD558\uC138\uC694: {command}",
140
141
  "context.tipDocsCommitRules": "\uCEE4\uBC0B \uBA54\uC2DC\uC9C0 \uADDC\uCE59\uC740 `npx lee-spec-kit docs get git-workflow --json`\uC73C\uB85C \uD655\uC778\uD558\uC138\uC694.",
141
142
  "context.list.docsCommitNeeded": "\uBB38\uC11C \uCEE4\uBC0B \uD544\uC694",
@@ -171,6 +172,7 @@ var I18N = {
171
172
  "init.choice.docsRepo.standalone.desc": "push \uC5EC\uBD80\uB97C \uBCC4\uB3C4\uB85C \uC124\uC815\uD569\uB2C8\uB2E4",
172
173
  "init.prompt.feRepoPath": "Frontend \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
173
174
  "init.prompt.beRepoPath": "Backend \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
175
+ "init.prompt.componentRepoPath": "{component} \uCEF4\uD3EC\uB10C\uD2B8 \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
174
176
  "init.prompt.projectRepoPath": "\uD504\uB85C\uC81D\uD2B8 \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uACBD\uB85C\uB97C \uC785\uB825\uD558\uC138\uC694:",
175
177
  "init.validation.enterPath": "\uACBD\uB85C\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694",
176
178
  "init.prompt.pushMode": "Docs push \uBC29\uC2DD\uC744 \uC120\uD0DD\uD558\uC138\uC694:",
@@ -199,11 +201,10 @@ var I18N = {
199
201
  "init.log.gitInitialCommitDone": "\u2705 Git \uCD08\uAE30 \uCEE4\uBC0B \uC644\uB8CC!",
200
202
  "init.warn.skipGitInit": "\u26A0\uFE0F Git \uCD08\uAE30\uD654\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4 (\uC218\uB3D9\uC73C\uB85C \uCEE4\uBC0B\uD574\uC8FC\uC138\uC694)",
201
203
  "init.error.templateNotFound": "\uD15C\uD50C\uB9BF\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: {path}",
202
- "github.cmdGithubDescription": "GitHub \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uB3C4\uC6B0\uBBF8 (issue/pr \uCD08\uC548, \uAC80\uC99D, merge \uC7AC\uC2DC\uB3C4)",
204
+ "github.cmdGithubDescription": "GitHub \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uB3C4\uC6B0\uBBF8 (issue/pr \uBCF8\uBB38 \uD15C\uD50C\uB9BF \uC0DD\uC131, \uAC80\uC99D, merge \uC7AC\uC2DC\uB3C4)",
203
205
  "github.cmdIssueDescription": "feature \uBB38\uC11C \uAE30\uBC18 GitHub issue \uBCF8\uBB38 \uC0DD\uC131/\uC0DD\uC131",
204
206
  "github.cmdPrDescription": "GitHub PR \uBCF8\uBB38 \uC0DD\uC131/\uC0DD\uC131 + tasks \uB3D9\uAE30\uD654 + merge \uC7AC\uC2DC\uB3C4",
205
207
  "github.optJson": "\uC5D0\uC774\uC804\uD2B8\uC6A9 JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825",
206
- "github.optRepo": "\uBA40\uD2F0 \uD504\uB85C\uC81D\uD2B8 \uCEF4\uD3EC\uB10C\uD2B8 \uC774\uB984",
207
208
  "github.optComponent": "\uBA40\uD2F0 \uD504\uB85C\uC81D\uD2B8 \uCEF4\uD3EC\uB10C\uD2B8 \uC774\uB984",
208
209
  "github.optIssueTitle": "Issue \uC81C\uBAA9",
209
210
  "github.optLabels": "\uC27C\uD45C \uAD6C\uBD84 \uB77C\uBCA8 \uBAA9\uB85D (\uAE30\uBCF8: enhancement)",
@@ -224,7 +225,6 @@ var I18N = {
224
225
  "github.optPrMermaid": "PR Mermaid \uC139\uC158 \uBAA8\uB4DC (auto|on|off, \uAE30\uBCF8: auto)",
225
226
  "github.optPrNoSyncTasks": "tasks.md PR URL/PR \uC0C1\uD0DC \uB3D9\uAE30\uD654\uB97C \uAC74\uB108\uB700",
226
227
  "github.optPrCommitSync": "tasks.md \uB3D9\uAE30\uD654 \uBCC0\uACBD\uC744 \uC790\uB3D9 commit/push",
227
- "github.invalidRepoComponentMismatch": "`--repo`\uC640 `--component`\uB97C \uD568\uAED8 \uC4F8 \uB54C\uB294 \uAC19\uC740 \uAC12\uC744 \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.",
228
228
  "github.labelsRequired": "\uCD5C\uC18C 1\uAC1C \uB77C\uBCA8\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. `--labels enhancement`\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.",
229
229
  "github.approvalRequired": "{operation}\uC740(\uB294) \uC0AC\uC6A9\uC790 \uBA85\uC2DC \uC2B9\uC778 \uD6C4\uC5D0\uB9CC \uC2E4\uD589\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uACC4\uD68D \uACF5\uC720 \uD6C4 `--confirm OK`\uB85C \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
230
230
  "github.ghCommandFailed": "GitHub CLI \uBA85\uB839 \uC2E4\uD589\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4",
@@ -275,10 +275,10 @@ var I18N = {
275
275
  "github.labelLabels": "\uB77C\uBCA8",
276
276
  "github.labelPr": "PR",
277
277
  "github.issueCreated": "\u2705 \uC0DD\uC131 \uC644\uB8CC: {url}",
278
- "github.issueTemplateGenerated": "\uCD08\uC548\uC744 \uC0DD\uC131\uD588\uC2B5\uB2C8\uB2E4. \uC790\uB3D9 \uC0DD\uC131\uD558\uB824\uBA74 `--create`\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.",
278
+ "github.issueTemplateGenerated": "\uBCF8\uBB38 \uD15C\uD50C\uB9BF\uC744 \uC0DD\uC131\uD588\uC2B5\uB2C8\uB2E4. \uC6D0\uACA9\uC73C\uB85C \uC774\uC288\uB97C \uC0DD\uC131\uD558\uB824\uBA74 `--create`\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.",
279
279
  "github.prTasksSynced": "\u2705 tasks.md PR \uBA54\uD0C0\uB370\uC774\uD130\uB97C \uB3D9\uAE30\uD654\uD588\uC2B5\uB2C8\uB2E4.",
280
280
  "github.prMerged": "\u2705 PR merge \uC644\uB8CC (\uC2DC\uB3C4 \uD69F\uC218: {attempts})",
281
- "github.prTemplateGenerated": "\uCD08\uC548\uC744 \uC0DD\uC131\uD588\uC2B5\uB2C8\uB2E4. \uC790\uB3D9 \uC0DD\uC131\uD558\uB824\uBA74 `--create`\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.",
281
+ "github.prTemplateGenerated": "\uBCF8\uBB38 \uD15C\uD50C\uB9BF\uC744 \uC0DD\uC131\uD588\uC2B5\uB2C8\uB2E4. \uC6D0\uACA9\uC73C\uB85C PR\uC744 \uC0DD\uC131\uD558\uB824\uBA74 `--create`\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.",
282
282
  "github.syncCommitWithIssue": "docs(#{issue}): {folder} PR \uBA54\uD0C0\uB370\uC774\uD130 \uB3D9\uAE30\uD654",
283
283
  "github.syncCommitNoIssue": "docs: {folder} PR \uBA54\uD0C0\uB370\uC774\uD130 \uB3D9\uAE30\uD654",
284
284
  "github.kindIssue": "Issue",
@@ -292,6 +292,22 @@ var I18N = {
292
292
  "docs.nextDocs": "\uB2E4\uC74C \uBB38\uC11C",
293
293
  "docs.sourceLabel": "source",
294
294
  "docs.hashLabel": "hash",
295
+ "detect.cmdDescription": "\uD604\uC7AC \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uAC00 lee-spec-kit \uD504\uB85C\uC81D\uD2B8\uC778\uC9C0 \uAC10\uC9C0\uD569\uB2C8\uB2E4",
296
+ "detect.optDir": "\uAC10\uC9C0 \uAE30\uC900 \uACBD\uB85C (\uAE30\uBCF8: \uD604\uC7AC \uACBD\uB85C)",
297
+ "detect.optJson": "\uC5D0\uC774\uC804\uD2B8\uC6A9 JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825",
298
+ "detect.header": "\u{1F50E} Project Detection",
299
+ "detect.labelTarget": "Target",
300
+ "detect.resultDetected": "lee-spec-kit \uD504\uB85C\uC81D\uD2B8\uB97C \uAC10\uC9C0\uD588\uC2B5\uB2C8\uB2E4",
301
+ "detect.resultNotDetected": "lee-spec-kit \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4",
302
+ "detect.notDetectedHint": "`npx lee-spec-kit init`\uC73C\uB85C \uCD08\uAE30\uD654\uD558\uAC70\uB098 `--dir`\uB85C \uC62C\uBC14\uB978 \uACBD\uB85C\uB97C \uC9C0\uC815\uD558\uC138\uC694.",
303
+ "detect.labelDocsDir": "Docs",
304
+ "detect.labelConfigPath": "Config",
305
+ "detect.labelSource": "Source",
306
+ "detect.labelProjectType": "Project Type",
307
+ "detect.labelLang": "Lang",
308
+ "detect.labelProjectName": "Project",
309
+ "detect.sourceConfig": "config (.lee-spec-kit.json)",
310
+ "detect.sourceHeuristic": "heuristic (agents/features folder)",
295
311
  "cliError.headerNextOptionsError": "\u{1F449} \uB2E4\uC74C \uC635\uC158 (\uC624\uB958):",
296
312
  "cliError.promptBlocked.retryWithoutNonInteractive": "--non-interactive \uC5C6\uC774 \uAC19\uC740 \uBA85\uB839\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
297
313
  "cliError.promptBlocked.passRequiredFlags": "\uD544\uC218 \uD50C\uB798\uADF8\uB97C \uBAA8\uB450 \uBA85\uC2DC\uD558\uAC70\uB098(`--force` \uD3EC\uD568) \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
@@ -321,7 +337,7 @@ var I18N = {
321
337
  "cliError.approvalRequired.githubConfirmOk": "github \uC6D0\uACA9 \uC0DD\uC131/\uBA38\uC9C0\uBA74 --confirm OK\uB97C \uD568\uAED8 \uC804\uB2EC\uD558\uC138\uC694.",
322
338
  "cliError.approvalRequired.shareAndGetApproval": "\uC2E4\uD589 \uC804\uC5D0 \uC81C\uBAA9/\uBCF8\uBB38/\uB77C\uBCA8(\uB610\uB294 \uBA38\uC9C0 \uACC4\uD68D)\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uBA85\uC2DC\uC801 \uC2B9\uC778\uC744 \uBC1B\uC73C\uC138\uC694.",
323
339
  "cliError.contextSelection.specifySelector": "\uB2E8\uC77C Feature selector\uB97C \uBA85\uC2DC\uD558\uC138\uC694.",
324
- "cliError.contextSelection.narrowByComponent": "multi \uBAA8\uB4DC\uC5D0\uC11C\uB294 --repo(\uB610\uB294 --component)\uB85C \uBC94\uC704\uB97C \uC881\uD788\uC138\uC694.",
340
+ "cliError.contextSelection.narrowByComponent": "multi \uBAA8\uB4DC\uC5D0\uC11C\uB294 --component\uB85C \uBC94\uC704\uB97C \uC881\uD788\uC138\uC694.",
325
341
  "cliError.contextSelection.inspectAllCandidates": "\uBA3C\uC800 \uC804\uCCB4 \uD6C4\uBCF4\uB97C \uD655\uC778\uD558\uC138\uC694.",
326
342
  "cliError.noActionOptions.refreshContext": "\uD604\uC7AC \uC0C1\uD0DC\uB97C \uBCF4\uAE30 \uC704\uD574 context\uB97C \uC0C8\uB85C \uC870\uD68C\uD558\uC138\uC694.",
327
343
  "cliError.noActionOptions.completeChecklist": "Feature \uBB38\uC11C\uB97C \uC5F4\uC5B4 \uB204\uB77D\uB41C \uCCB4\uD06C \uD56D\uBAA9\uC744 \uC644\uB8CC\uD558\uC138\uC694.",
@@ -393,7 +409,7 @@ var I18N = {
393
409
  tasksImprove: "tasks.md\uB97C \uBCF4\uC644\uD558\uACE0 \uBB38\uC11C \uC0C1\uD0DC\uB97C Review\uB85C \uBCC0\uACBD\uD558\uC138\uC694.",
394
410
  tasksApproval: "tasks.md \uB0B4\uC6A9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uC9C4\uD589 \uC2B9\uC778(`A` \uB610\uB294 `A OK` \uD615\uC2DD)\uC744 \uBC1B\uC73C\uC138\uC694. (\uC2B9\uC778 \uD6C4 \uBB38\uC11C \uC0C1\uD0DC\uB97C Approved\uB85C \uBCC0\uACBD)",
395
411
  docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} \uAE30\uD68D \uBB38\uC11C"',
396
- issueCreateAndWrite: "`npx lee-spec-kit docs get create-issue --json`\uC73C\uB85C \uC808\uCC28\uB97C \uD655\uC778\uD55C \uB4A4, `npx lee-spec-kit github issue {featureRef} --json`\uC73C\uB85C \uCD08\uC548\uC744 \uC0DD\uC131\uD558\uC138\uC694. \uBAA9\uD45C/\uC644\uB8CC \uAE30\uC900\uC744 \uAC80\uD1A0\xB7\uBCF4\uC644\uD558\uACE0 \uC0AC\uC6A9\uC790 \uC2B9\uC778(OK) \uD6C4 `--create --confirm OK`\uB85C \uC0DD\uC131\uD55C \uB2E4\uC74C, spec.md/tasks.md\uC758 \uC774\uC288 \uBC88\uD638\uB97C \uCC44\uC6B0\uACE0 \uBB38\uC11C \uCEE4\uBC0B\uC744 \uC900\uBE44\uD558\uC138\uC694.",
412
+ issueCreateAndWrite: "`npx lee-spec-kit docs get create-issue --json`\uC73C\uB85C \uC808\uCC28\uB97C \uD655\uC778\uD55C \uB4A4, `npx lee-spec-kit github issue {featureRef} --json`\uC73C\uB85C \uBCF8\uBB38 \uD15C\uD50C\uB9BF\uC744 \uC0DD\uC131\uD558\uC138\uC694. \uBAA9\uD45C/\uC644\uB8CC \uAE30\uC900\uC744 \uAC80\uD1A0\xB7\uBCF4\uC644\uD558\uACE0 \uC0AC\uC6A9\uC790 \uC2B9\uC778(OK) \uD6C4 `--create --confirm OK`\uB85C \uC0DD\uC131\uD55C \uB2E4\uC74C, spec.md/tasks.md\uC758 \uC774\uC288 \uBC88\uD638\uB97C \uCC44\uC6B0\uACE0 \uBB38\uC11C \uCEE4\uBC0B\uC744 \uC900\uBE44\uD558\uC138\uC694.",
397
413
  docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
398
414
  docsCommitUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs: {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
399
415
  projectCommitIssueUpdate: 'cd "{projectGitCwd}" && (git diff --cached --quiet && echo "\uC2A4\uD14C\uC774\uC9D5\uB41C \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uC774\uBC88 \uD0DC\uC2A4\uD06C\uC5D0\uC11C \uC218\uC815\uD55C \uD30C\uC77C\uB9CC \uC120\uD0DD\uD574 git add [files] \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694." && exit 1 || git commit -m "feat(#{issueNumber}): {commitTopic}")',
@@ -405,12 +421,18 @@ var I18N = {
405
421
  finishDoingTask: '\uD604\uC7AC DOING/REVIEW \uC911\uC778 \uD0DC\uC2A4\uD06C\uB97C \uC644\uB8CC\uD558\uC138\uC694: "{title}" ({done}/{total}) (\uC644\uB8CC \uC804 `npx lee-spec-kit docs get execute-task --json`\uC73C\uB85C \uC808\uCC28 \uD655\uC778 \u2192 \uACB0\uACFC/\uAC80\uC99D \uACF5\uC720 + \uC2B9\uC778(`A` \uB610\uB294 `A OK` \uD615\uC2DD) \uD6C4 DONE \uCC98\uB9AC)',
406
422
  startNextTodoTask: '\uB2E4\uC74C TODO \uD0DC\uC2A4\uD06C\uB97C \uC2DC\uC791\uD558\uC138\uC694: "{title}" ({done}/{total}) (\uC2DC\uC791 \uC804 `npx lee-spec-kit docs get execute-task --json`\uC73C\uB85C \uC808\uCC28 \uD655\uC778 \u2192 \uC81C\uBAA9 \uACF5\uC720 + \uC2B9\uC778(`A` \uB610\uB294 `A OK` \uD615\uC2DD) \uD6C4 DOING \uCC98\uB9AC)',
407
423
  checkTaskStatuses: "\uD0DC\uC2A4\uD06C \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694. ({done}/{total}) (`npx lee-spec-kit docs get execute-task --json` \uC808\uCC28\uB97C \uAE30\uC900\uC73C\uB85C \uC810\uAC80)",
424
+ taskCommitGateStrictBlock: "\uB2E4\uC74C TODO \uD0DC\uC2A4\uD06C\uB85C \uB118\uC5B4\uAC00\uAE30 \uC804\uC5D0 `1 \uD0DC\uC2A4\uD06C = 1 \uCEE4\uBC0B` \uADDC\uCE59\uC744 \uCDA9\uC871\uD574\uC57C \uD569\uB2C8\uB2E4. \uC810\uAC80 \uACB0\uACFC: {reason}. `npx lee-spec-kit docs get execute-task --json` \uC808\uCC28\uB97C \uAE30\uC900\uC73C\uB85C \uD0DC\uC2A4\uD06C \uCEE4\uBC0B \uB2E8\uC704\uB97C \uC815\uB9AC\uD55C \uB4A4 \uB2E4\uC2DC \uC9C4\uD589\uD558\uC138\uC694.",
425
+ taskCommitGateWarnProceed: "\u26A0\uFE0F \uD0DC\uC2A4\uD06C \uCEE4\uBC0B \uB2E8\uC704 \uC810\uAC80 \uACBD\uACE0: {reason}. \uD604\uC7AC\uB294 \uC9C4\uD589 \uAC00\uB2A5\uD558\uC9C0\uB9CC `1 \uD0DC\uC2A4\uD06C = 1 \uCEE4\uBC0B`\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4.",
426
+ taskCommitGateReasonNoTasksCommit: "\uCD5C\uADFC tasks.md \uCEE4\uBC0B\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
427
+ taskCommitGateReasonTasksFileUnavailable: "\uCD5C\uADFC \uCEE4\uBC0B\uC5D0\uC11C tasks.md \uC774\uB825\uC744 \uD310\uB3C5\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
428
+ taskCommitGateReasonDoneCount: "\uCD5C\uADFC tasks.md \uCEE4\uBC0B\uC758 DONE \uC804\uD658 \uC218\uAC00 {count}\uAC1C\uC785\uB2C8\uB2E4 (\uC815\uC0C1: 1\uAC1C)",
429
+ taskCommitGateReasonMismatchLastDone: "\uCD5C\uADFC tasks.md \uCEE4\uBC0B\uC774 \uC9C1\uC804 \uC644\uB8CC \uD0DC\uC2A4\uD06C\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4",
408
430
  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? (\uD655\uC778 \uD544\uC694)",
409
431
  prePrReviewFieldMissing: "tasks.md\uC5D0 `PR \uC804 \uB9AC\uBDF0` \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. `- **PR \uC804 \uB9AC\uBDF0**: Pending | Done` \uD56D\uBAA9\uC744 \uCD94\uAC00\uD558\uACE0 \uB2E4\uC2DC context\uB97C \uC2E4\uD589\uD558\uC138\uC694. (\uD655\uC778 \uD544\uC694)",
410
432
  prePrReviewRun: "PR \uC0DD\uC131 \uC804 \uC0AC\uC804 \uCF54\uB4DC\uB9AC\uBDF0\uB97C \uC9C4\uD589\uD558\uC138\uC694. \uC6B0\uC120\uC21C\uC704 \uC2A4\uD0AC: {skills} (\uC124\uCE58\uB41C \uB354 \uC801\uD569\uD55C \uC2A4\uD0AC\uC774 \uC788\uB2E4\uBA74 \uBA3C\uC800 \uC81C\uC548 \uD6C4 \uC0AC\uC6A9). \uC2A4\uD0AC\uC744 \uC4F8 \uC218 \uC5C6\uC73C\uBA74 `{fallback}` \uC815\uCC45\uC73C\uB85C \uC9C4\uD589\uD558\uACE0 `PR \uC804 \uB9AC\uBDF0`\uB97C Done\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694. Findings \uC815\uCC45: {findingsPolicy}",
411
433
  prePrReviewFindingsBlock: "\uC911\uC694 \uC774\uC288\uB294 \uC218\uC815/\uD569\uC758 \uD6C4\uC5D0\uB9CC PR \uC0DD\uC131",
412
434
  prePrReviewFindingsWarn: "\uB9AC\uC2A4\uD06C\uB97C \uACF5\uC720\uD558\uBA74 PR \uC0DD\uC131 \uC9C4\uD589 \uAC00\uB2A5",
413
- prCreate: "`npx lee-spec-kit docs get create-pr --json`\uC73C\uB85C \uC808\uCC28\uB97C \uD655\uC778\uD55C \uB4A4, `npx lee-spec-kit github pr {featureRef} --json`\uC73C\uB85C \uCD08\uC548\uC744 \uC0DD\uC131\uD558\uC138\uC694. \uBCC0\uACBD \uC0AC\uD56D/\uD14C\uC2A4\uD2B8 \uC139\uC158\uC744 \uAC80\uD1A0\xB7\uBCF4\uC644\uD558\uACE0 \uC0AC\uC6A9\uC790 \uC2B9\uC778(OK) \uD6C4 `--create --confirm OK`\uB85C \uC0DD\uC131\uD55C \uB2E4\uC74C tasks.md\uC5D0 PR \uB9C1\uD06C\uB97C \uAE30\uB85D\uD558\uC138\uC694.",
435
+ prCreate: "`npx lee-spec-kit docs get create-pr --json`\uC73C\uB85C \uC808\uCC28\uB97C \uD655\uC778\uD55C \uB4A4, `npx lee-spec-kit github pr {featureRef} --json`\uC73C\uB85C \uBCF8\uBB38 \uD15C\uD50C\uB9BF\uC744 \uC0DD\uC131\uD558\uC138\uC694. \uBCC0\uACBD \uC0AC\uD56D/\uD14C\uC2A4\uD2B8 \uC139\uC158\uC744 \uAC80\uD1A0\xB7\uBCF4\uC644\uD558\uACE0 \uC0AC\uC6A9\uC790 \uC2B9\uC778(OK) \uD6C4 `--create --confirm OK`\uB85C \uC0DD\uC131\uD55C \uB2E4\uC74C tasks.md\uC5D0 PR \uB9C1\uD06C\uB97C \uAE30\uB85D\uD558\uC138\uC694.",
414
436
  prFillStatus: "tasks.md\uC758 PR \uC0C1\uD0DC\uB97C Review/Approved \uC911 \uD558\uB098\uB85C \uC124\uC815\uD558\uC138\uC694. (merge \uD6C4 Approved\uB85C \uC5C5\uB370\uC774\uD2B8)",
415
437
  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)",
416
438
  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.",
@@ -459,7 +481,7 @@ var I18N = {
459
481
  "config.pathLabel": "Path",
460
482
  "config.projectRootStandaloneOnly": "\u26A0\uFE0F projectRoot can only be set in standalone mode.",
461
483
  "config.selectRepoToUpdate": "Select a repository to update:",
462
- "config.fullstackRepoRequired": "For multi projects, specify a target component via `--repo` or `--component`.",
484
+ "config.fullstackRepoRequired": "For multi projects, specify a target component via `--component`.",
463
485
  "config.projectRootSet": "\u2705 {repo} projectRoot set: {path}",
464
486
  "config.projectRootSetSingle": "\u2705 projectRoot set: {path}",
465
487
  "update.start": "\u{1F4E6} Starting template update...",
@@ -508,10 +530,11 @@ var I18N = {
508
530
  "context.tipShowDone": "Show done only",
509
531
  "context.checkRequired": "[CHECK required] ",
510
532
  "context.checkPolicyHint": "\u2139\uFE0F Check user-approval policy first with `npx lee-spec-kit docs get agents --json` (includes git push/merge and merge commits). If you see [CHECK required], wait for `<label>` or `<label> OK` (e.g. `A`, `A OK`) before proceeding (config: approval can override)",
511
- "context.actionOptionHint": "Approval reply format: `<label>` or `<label> OK` (e.g. `A`, `A OK`)",
533
+ "context.actionOptionHint": "Approval reply format: include a label token (e.g. `A`, `A OK`, `A proceed`)",
512
534
  "context.actionExplainHint": "Before requesting approval, explain what each label will run/change with a one-line summary.",
513
535
  "context.finalLabelPrompt": "Available labels now: {labels}. End with a label request in `<label>` or `<label> OK` format. (e.g. {example})",
514
- "context.finalLabelCommandHint": "When a label is provided, run immediately: {command}",
536
+ "context.finalLabelCommandHint": "When a label is provided, run approval selection: {command}",
537
+ "context.finalTicketCommandHint": "Execute commands using the ticket from approval result: {command}",
515
538
  "context.readBuiltinDocFirst": "Read built-in docs first: {command}",
516
539
  "context.tipDocsCommitRules": "Check commit message rules with `npx lee-spec-kit docs get git-workflow --json`.",
517
540
  "context.list.docsCommitNeeded": "Commit docs changes",
@@ -547,6 +570,7 @@ var I18N = {
547
570
  "init.choice.docsRepo.standalone.desc": "Configure push settings separately",
548
571
  "init.prompt.feRepoPath": "Enter frontend repository path:",
549
572
  "init.prompt.beRepoPath": "Enter backend repository path:",
573
+ "init.prompt.componentRepoPath": 'Enter repository path for component "{component}":',
550
574
  "init.prompt.projectRepoPath": "Enter project repository path:",
551
575
  "init.validation.enterPath": "Please enter a path",
552
576
  "init.prompt.pushMode": "Select docs push mode:",
@@ -579,7 +603,6 @@ var I18N = {
579
603
  "github.cmdIssueDescription": "Generate/create GitHub issue body from feature docs with validation",
580
604
  "github.cmdPrDescription": "Generate/create GitHub PR body with validation, tasks PR sync, and merge retry",
581
605
  "github.optJson": "Output in JSON format for agents",
582
- "github.optRepo": "Component name for multi projects",
583
606
  "github.optComponent": "Component name for multi projects",
584
607
  "github.optIssueTitle": "Issue title",
585
608
  "github.optLabels": "Comma-separated labels (default: enhancement)",
@@ -600,7 +623,6 @@ var I18N = {
600
623
  "github.optPrMermaid": "PR Mermaid section mode (auto|on|off, default: auto)",
601
624
  "github.optPrNoSyncTasks": "Do not sync PR URL/PR status into tasks.md",
602
625
  "github.optPrCommitSync": "Commit and push tasks.md metadata sync automatically",
603
- "github.invalidRepoComponentMismatch": "`--repo` and `--component` must reference the same value when both are provided.",
604
626
  "github.labelsRequired": "At least one label is required. Use `--labels enhancement`.",
605
627
  "github.approvalRequired": "{operation} requires explicit user approval. Re-run with `--confirm OK` after sharing the plan with the user.",
606
628
  "github.ghCommandFailed": "GitHub CLI command failed",
@@ -668,6 +690,22 @@ var I18N = {
668
690
  "docs.nextDocs": "Next docs",
669
691
  "docs.sourceLabel": "source",
670
692
  "docs.hashLabel": "hash",
693
+ "detect.cmdDescription": "Detect whether the current workspace is a lee-spec-kit project",
694
+ "detect.optDir": "Target directory to probe (default: current directory)",
695
+ "detect.optJson": "Output in JSON format for agents",
696
+ "detect.header": "\u{1F50E} Project Detection",
697
+ "detect.labelTarget": "Target",
698
+ "detect.resultDetected": "Detected a lee-spec-kit project",
699
+ "detect.resultNotDetected": "No lee-spec-kit project detected",
700
+ "detect.notDetectedHint": "Run `npx lee-spec-kit init` or pass `--dir` to the correct path.",
701
+ "detect.labelDocsDir": "Docs",
702
+ "detect.labelConfigPath": "Config",
703
+ "detect.labelSource": "Source",
704
+ "detect.labelProjectType": "Project Type",
705
+ "detect.labelLang": "Lang",
706
+ "detect.labelProjectName": "Project",
707
+ "detect.sourceConfig": "config (.lee-spec-kit.json)",
708
+ "detect.sourceHeuristic": "heuristic (agents/features folder)",
671
709
  "cliError.headerNextOptionsError": "\u{1F449} Next Options (Error):",
672
710
  "cliError.promptBlocked.retryWithoutNonInteractive": "Run the same command without --non-interactive.",
673
711
  "cliError.promptBlocked.passRequiredFlags": "Pass all required flags (including `--force` when needed), then run again.",
@@ -769,7 +807,7 @@ var I18N = {
769
807
  tasksImprove: "Improve tasks.md and change Doc Status to Review.",
770
808
  tasksApproval: "Share tasks.md with the user and get progress approval (`A` or `A OK` format). (Then set Doc Status to Approved)",
771
809
  docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} planning docs"',
772
- issueCreateAndWrite: "Review procedure with `npx lee-spec-kit docs get create-issue --json`, then generate a draft via `npx lee-spec-kit github issue {featureRef} --json`. Refine goals/completion criteria, get explicit user OK, run `--create --confirm OK`, then update issue number in spec.md/tasks.md and prepare a docs commit.",
810
+ issueCreateAndWrite: "Review procedure with `npx lee-spec-kit docs get create-issue --json`, then generate a body template via `npx lee-spec-kit github issue {featureRef} --json`. Refine goals/completion criteria, get explicit user OK, run `--create --confirm OK`, then update issue number in spec.md/tasks.md and prepare a docs commit.",
773
811
  docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} docs update"',
774
812
  docsCommitUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs: {folderName} docs update"',
775
813
  projectCommitIssueUpdate: 'cd "{projectGitCwd}" && (git diff --cached --quiet && echo "No staged files. Stage only files changed in this task with git add [files], then run again." && exit 1 || git commit -m "feat(#{issueNumber}): {commitTopic}")',
@@ -781,12 +819,18 @@ var I18N = {
781
819
  finishDoingTask: 'Finish the current DOING/REVIEW task: "{title}" ({done}/{total}) (Before finishing, read `npx lee-spec-kit docs get execute-task --json`, then share outcome/verification + get OK before marking DONE)',
782
820
  startNextTodoTask: 'Start the next TODO task: "{title}" ({done}/{total}) (Before starting, read `npx lee-spec-kit docs get execute-task --json`, then share title + get OK before marking DOING)',
783
821
  checkTaskStatuses: "Check task statuses. ({done}/{total}) (Use `npx lee-spec-kit docs get execute-task --json` as the procedure baseline)",
822
+ taskCommitGateStrictBlock: "Before moving to the next TODO task, you must satisfy the `1 task = 1 commit` rule. Check result: {reason}. Re-align task commit boundaries using `npx lee-spec-kit docs get execute-task --json`, then continue.",
823
+ taskCommitGateWarnProceed: "\u26A0\uFE0F Task commit boundary warning: {reason}. You may continue, but `1 task = 1 commit` is recommended.",
824
+ taskCommitGateReasonNoTasksCommit: "No recent tasks.md commit was found",
825
+ taskCommitGateReasonTasksFileUnavailable: "Cannot read tasks.md history from the latest commit",
826
+ taskCommitGateReasonDoneCount: "DONE transitions in the latest tasks.md commit: {count} (expected: 1)",
827
+ taskCommitGateReasonMismatchLastDone: "The latest tasks.md commit does not match the last completed task",
784
828
  prLegacyAsk: "tasks.md is missing PR/PR Status fields. Update to the latest template format? (CHECK required)",
785
829
  prePrReviewFieldMissing: "tasks.md is missing the `Pre-PR Review` field. Add `- **Pre-PR Review**: Pending | Done` and run context again. (CHECK required)",
786
830
  prePrReviewRun: "Run a pre-PR code review before creating the PR. Preferred skills: {skills} (if a better installed skill fits this change, propose it first). If no skill can run, use `{fallback}` and set `Pre-PR Review` to Done in tasks.md. Findings policy: {findingsPolicy}",
787
831
  prePrReviewFindingsBlock: "major findings must be fixed/aligned before PR creation",
788
832
  prePrReviewFindingsWarn: "you may proceed after sharing the risks",
789
- prCreate: "Review procedure with `npx lee-spec-kit docs get create-pr --json`, then generate a draft via `npx lee-spec-kit github pr {featureRef} --json`. Refine changes/tests sections, get explicit user OK, run `--create --confirm OK`, then record the PR link in tasks.md.",
833
+ prCreate: "Review procedure with `npx lee-spec-kit docs get create-pr --json`, then generate a body template via `npx lee-spec-kit github pr {featureRef} --json`. Refine changes/tests sections, get explicit user OK, run `--create --confirm OK`, then record the PR link in tasks.md.",
790
834
  prFillStatus: "Set PR Status in tasks.md to Review/Approved. (After merge, update it to Approved.)",
791
835
  prResolveReview: "Resolve review comments and update PR Status. (PR Status: Review \u2192 Approved)",
792
836
  prRequestReview: "Request review and update PR Status to Review.",
@@ -943,7 +987,7 @@ var SUGGESTION_MAP = {
943
987
  },
944
988
  {
945
989
  titleKey: "contextSelection.narrowByComponent",
946
- command: "npx lee-spec-kit context --repo <component>"
990
+ command: "npx lee-spec-kit context --component <component>"
947
991
  },
948
992
  {
949
993
  titleKey: "contextSelection.inspectAllCandidates",
@@ -1092,9 +1136,6 @@ function normalizeProjectType(input) {
1092
1136
  if (input === "multi") return "multi";
1093
1137
  return "single";
1094
1138
  }
1095
- function toTemplateProjectType(projectType) {
1096
- return projectType === "multi" ? "fullstack" : "single";
1097
- }
1098
1139
 
1099
1140
  // src/utils/validation.ts
1100
1141
  var VALID_PROJECT_TYPES = ["single", "multi", "fullstack"];
@@ -1234,7 +1275,7 @@ function getProjectExecutionLockPath(cwd) {
1234
1275
  }
1235
1276
  async function isStaleLock(lockPath, staleMs) {
1236
1277
  try {
1237
- const stat = await fs14.stat(lockPath);
1278
+ const stat = await fs15.stat(lockPath);
1238
1279
  if (Date.now() - stat.mtimeMs <= staleMs) {
1239
1280
  return false;
1240
1281
  }
@@ -1249,7 +1290,7 @@ async function isStaleLock(lockPath, staleMs) {
1249
1290
  }
1250
1291
  async function readLockPayload(lockPath) {
1251
1292
  try {
1252
- const raw = await fs14.readFile(lockPath, "utf8");
1293
+ const raw = await fs15.readFile(lockPath, "utf8");
1253
1294
  const parsed = JSON.parse(raw);
1254
1295
  if (!parsed || typeof parsed !== "object") return null;
1255
1296
  return parsed;
@@ -1271,17 +1312,17 @@ function isProcessAlive(pid) {
1271
1312
  }
1272
1313
  }
1273
1314
  async function tryAcquire(lockPath, owner) {
1274
- await fs14.ensureDir(path18.dirname(lockPath));
1315
+ await fs15.ensureDir(path18.dirname(lockPath));
1275
1316
  try {
1276
- const fd = await fs14.open(lockPath, "wx");
1317
+ const fd = await fs15.open(lockPath, "wx");
1277
1318
  const payload = JSON.stringify(
1278
1319
  { pid: process.pid, owner: owner ?? "unknown", createdAt: (/* @__PURE__ */ new Date()).toISOString() },
1279
1320
  null,
1280
1321
  2
1281
1322
  );
1282
- await fs14.writeFile(fd, `${payload}
1323
+ await fs15.writeFile(fd, `${payload}
1283
1324
  `, { encoding: "utf8" });
1284
- await fs14.close(fd);
1325
+ await fs15.close(fd);
1285
1326
  return true;
1286
1327
  } catch (error) {
1287
1328
  if (error.code === "EEXIST") {
@@ -1295,9 +1336,9 @@ async function waitForLockRelease(lockPath, options = {}) {
1295
1336
  const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
1296
1337
  const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
1297
1338
  const startedAt = Date.now();
1298
- while (await fs14.pathExists(lockPath)) {
1339
+ while (await fs15.pathExists(lockPath)) {
1299
1340
  if (await isStaleLock(lockPath, staleMs)) {
1300
- await fs14.remove(lockPath);
1341
+ await fs15.remove(lockPath);
1301
1342
  break;
1302
1343
  }
1303
1344
  if (Date.now() - startedAt > timeoutMs) {
@@ -1315,7 +1356,7 @@ async function withFileLock(lockPath, task, options = {}) {
1315
1356
  const acquired = await tryAcquire(lockPath, options.owner);
1316
1357
  if (acquired) break;
1317
1358
  if (await isStaleLock(lockPath, staleMs)) {
1318
- await fs14.remove(lockPath);
1359
+ await fs15.remove(lockPath);
1319
1360
  continue;
1320
1361
  }
1321
1362
  if (Date.now() - startedAt > timeoutMs) {
@@ -1329,7 +1370,7 @@ async function withFileLock(lockPath, task, options = {}) {
1329
1370
  try {
1330
1371
  return await task();
1331
1372
  } finally {
1332
- await fs14.remove(lockPath).catch(() => {
1373
+ await fs15.remove(lockPath).catch(() => {
1333
1374
  });
1334
1375
  }
1335
1376
  }
@@ -1356,21 +1397,21 @@ async function pruneEngineManagedDocs(docsDir) {
1356
1397
  const removed = [];
1357
1398
  for (const file of ENGINE_MANAGED_AGENT_FILES) {
1358
1399
  const target = path18.join(docsDir, "agents", file);
1359
- if (await fs14.pathExists(target)) {
1360
- await fs14.remove(target);
1400
+ if (await fs15.pathExists(target)) {
1401
+ await fs15.remove(target);
1361
1402
  removed.push(path18.relative(docsDir, target));
1362
1403
  }
1363
1404
  }
1364
1405
  for (const dir of ENGINE_MANAGED_AGENT_DIRS) {
1365
1406
  const target = path18.join(docsDir, "agents", dir);
1366
- if (await fs14.pathExists(target)) {
1367
- await fs14.remove(target);
1407
+ if (await fs15.pathExists(target)) {
1408
+ await fs15.remove(target);
1368
1409
  removed.push(path18.relative(docsDir, target));
1369
1410
  }
1370
1411
  }
1371
1412
  const featureBasePath = path18.join(docsDir, ENGINE_MANAGED_FEATURE_PATH);
1372
- if (await fs14.pathExists(featureBasePath)) {
1373
- await fs14.remove(featureBasePath);
1413
+ if (await fs15.pathExists(featureBasePath)) {
1414
+ await fs15.remove(featureBasePath);
1374
1415
  removed.push(path18.relative(docsDir, featureBasePath));
1375
1416
  }
1376
1417
  return removed;
@@ -1388,16 +1429,110 @@ function checkGitRepo(cwd) {
1388
1429
  return false;
1389
1430
  }
1390
1431
  }
1432
+ function parseStandaloneMultiProjectRootJson(raw) {
1433
+ let parsed;
1434
+ try {
1435
+ parsed = JSON.parse(raw);
1436
+ } catch {
1437
+ throw createCliError(
1438
+ "INVALID_ARGUMENT",
1439
+ '`--project-root` for standalone multi must be a JSON object. Example: {"fe":"/path/fe","be":"/path/be"}'
1440
+ );
1441
+ }
1442
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1443
+ throw createCliError(
1444
+ "INVALID_ARGUMENT",
1445
+ "`--project-root` for standalone multi must be a JSON object."
1446
+ );
1447
+ }
1448
+ const out = {};
1449
+ for (const [key, value] of Object.entries(parsed)) {
1450
+ const component = key.trim().toLowerCase();
1451
+ const root = typeof value === "string" ? value.trim() : "";
1452
+ if (!component || !root) {
1453
+ throw createCliError(
1454
+ "INVALID_ARGUMENT",
1455
+ "`--project-root` JSON entries must be non-empty component/path pairs."
1456
+ );
1457
+ }
1458
+ assertValidComponentId(component);
1459
+ out[component] = root;
1460
+ }
1461
+ if (Object.keys(out).length === 0) {
1462
+ throw createCliError(
1463
+ "INVALID_ARGUMENT",
1464
+ "`--project-root` JSON object must include at least one component path."
1465
+ );
1466
+ }
1467
+ return out;
1468
+ }
1469
+ function parseComponentProjectRootsOption(raw) {
1470
+ if (!raw) return {};
1471
+ const out = {};
1472
+ const entries = raw.split(",").map((entry) => entry.trim()).filter(Boolean);
1473
+ if (entries.length === 0) {
1474
+ throw createCliError(
1475
+ "INVALID_ARGUMENT",
1476
+ "`--component-project-roots` must include at least one `component=/path` pair."
1477
+ );
1478
+ }
1479
+ for (const entry of entries) {
1480
+ const index = entry.indexOf("=");
1481
+ if (index <= 0 || index === entry.length - 1) {
1482
+ throw createCliError(
1483
+ "INVALID_ARGUMENT",
1484
+ `Invalid --component-project-roots entry "${entry}". Use \`component=/path\`.`
1485
+ );
1486
+ }
1487
+ const component = entry.slice(0, index).trim().toLowerCase();
1488
+ const root = entry.slice(index + 1).trim();
1489
+ assertValidComponentId(component);
1490
+ if (!root) {
1491
+ throw createCliError(
1492
+ "INVALID_ARGUMENT",
1493
+ `Invalid --component-project-roots entry "${entry}". Path must not be empty.`
1494
+ );
1495
+ }
1496
+ out[component] = root;
1497
+ }
1498
+ return out;
1499
+ }
1500
+ function getComponentFeaturesReadme(lang, component) {
1501
+ if (lang === "ko") {
1502
+ return [
1503
+ `# ${component.toUpperCase()} Features`,
1504
+ "",
1505
+ `\uC774 \uD3F4\uB354\uC5D0 ${component} Feature\uB4E4\uC774 \uC0DD\uC131\uB429\uB2C8\uB2E4.`,
1506
+ "",
1507
+ `\`npx lee-spec-kit feature --component ${component} <name>\` \uBA85\uB839\uC5B4\uB85C \uC0C8 Feature\uB97C \uC0DD\uC131\uD558\uC138\uC694.`,
1508
+ ""
1509
+ ].join("\n");
1510
+ }
1511
+ return [
1512
+ `# ${component.toUpperCase()} Features`,
1513
+ "",
1514
+ `Features for component \`${component}\` are created in this folder.`,
1515
+ "",
1516
+ `Use \`npx lee-spec-kit feature --component ${component} <name>\` to create a new Feature.`,
1517
+ ""
1518
+ ].join("\n");
1519
+ }
1391
1520
  function initCommand(program2) {
1392
1521
  program2.command("init").description("Initialize project documentation structure").option("-n, --name <name>", "Project name (default: current folder name)").option("-t, --type <type>", "Project type: single | multi (fullstack alias)").option(
1393
1522
  "--components <list>",
1394
1523
  "Component list for multi (comma-separated, e.g. fe,be,worker)"
1395
- ).option("-l, --lang <lang>", "Language: ko | en (default: en)").option("--workflow <mode>", "Workflow mode: github | local").option("-d, --dir <dir>", "Target directory (default: ./docs)", "./docs").option("--docs-repo <mode>", "Docs repository mode: embedded | standalone").option("--project-root <path>", "Project repository path (standalone single)").option(
1524
+ ).option("-l, --lang <lang>", "Language: ko | en (default: en)").option("--workflow <mode>", "Workflow mode: github | local").option("-d, --dir <dir>", "Target directory (default: ./docs)", "./docs").option("--docs-repo <mode>", "Docs repository mode: embedded | standalone").option(
1525
+ "--project-root <path>",
1526
+ "Project root path (standalone single) or JSON map for standalone multi"
1527
+ ).option(
1396
1528
  "--fe-project-root <path>",
1397
1529
  "Frontend repository path (standalone multi)"
1398
1530
  ).option(
1399
1531
  "--be-project-root <path>",
1400
1532
  "Backend repository path (standalone multi)"
1533
+ ).option(
1534
+ "--component-project-roots <pairs>",
1535
+ "Component roots for standalone multi (comma-separated, e.g. fe=/path/fe,be=/path/be,worker=/path/worker)"
1401
1536
  ).option("--push-docs", "Push standalone docs to remote").option("--docs-remote <url>", "Remote URL for standalone docs repository").option("-y, --yes", "Skip prompts and use defaults").option("-f, --force", "Overwrite target directory if not empty").option("--non-interactive", "Fail instead of prompting for input").action(async (options) => {
1402
1537
  try {
1403
1538
  await runInit(options);
@@ -1434,6 +1569,9 @@ async function runInit(options) {
1434
1569
  let pushDocs = typeof options.pushDocs === "boolean" ? options.pushDocs : void 0;
1435
1570
  let docsRemote = options.docsRemote;
1436
1571
  let projectRoot;
1572
+ const componentProjectRoots = parseComponentProjectRootsOption(
1573
+ options.componentProjectRoots
1574
+ );
1437
1575
  const targetDir = path18.resolve(cwd, options.dir || "./docs");
1438
1576
  const skipPrompts = !!options.yes || !!options.nonInteractive;
1439
1577
  if (options.docsRepo && !["embedded", "standalone"].includes(options.docsRepo)) {
@@ -1445,12 +1583,7 @@ async function runInit(options) {
1445
1583
  if (docsRemote && typeof pushDocs === "undefined") {
1446
1584
  pushDocs = true;
1447
1585
  }
1448
- if (options.feProjectRoot || options.beProjectRoot) {
1449
- projectRoot = {
1450
- fe: options.feProjectRoot || "",
1451
- be: options.beProjectRoot || ""
1452
- };
1453
- } else if (options.projectRoot) {
1586
+ if (options.projectRoot) {
1454
1587
  projectRoot = options.projectRoot;
1455
1588
  }
1456
1589
  const isInsideGitRepo = checkGitRepo(cwd);
@@ -1572,32 +1705,46 @@ async function runInit(options) {
1572
1705
  String(projectType || response.projectType || "single")
1573
1706
  );
1574
1707
  if (resolvedType === "multi") {
1575
- const projectRootResponse = await prompts(
1576
- [
1577
- {
1578
- type: options.feProjectRoot ? null : "text",
1579
- name: "feRoot",
1580
- message: tr(lang, "cli", "init.prompt.feRepoPath"),
1581
- validate: (value) => value.trim() ? true : tr(lang, "cli", "init.validation.enterPath")
1582
- },
1708
+ const promptComponents = components.length > 0 ? components : ["fe", "be"];
1709
+ const rootMap = {};
1710
+ if (typeof projectRoot === "string" && projectRoot.trim()) {
1711
+ Object.assign(rootMap, parseStandaloneMultiProjectRootJson(projectRoot));
1712
+ } else if (projectRoot && typeof projectRoot === "object") {
1713
+ for (const [component, root] of Object.entries(projectRoot)) {
1714
+ const normalized = component.trim().toLowerCase();
1715
+ const normalizedRoot = String(root || "").trim();
1716
+ if (!normalized || !normalizedRoot) continue;
1717
+ rootMap[normalized] = normalizedRoot;
1718
+ }
1719
+ }
1720
+ if (options.feProjectRoot?.trim()) rootMap.fe = options.feProjectRoot.trim();
1721
+ if (options.beProjectRoot?.trim()) rootMap.be = options.beProjectRoot.trim();
1722
+ Object.assign(rootMap, componentProjectRoots);
1723
+ for (const component of promptComponents) {
1724
+ const seeded = (rootMap[component] || "").trim();
1725
+ if (seeded) {
1726
+ rootMap[component] = seeded;
1727
+ continue;
1728
+ }
1729
+ const message = component === "fe" ? tr(lang, "cli", "init.prompt.feRepoPath") : component === "be" ? tr(lang, "cli", "init.prompt.beRepoPath") : tr(lang, "cli", "init.prompt.componentRepoPath", { component });
1730
+ const response2 = await prompts(
1731
+ [
1732
+ {
1733
+ type: "text",
1734
+ name: "componentRoot",
1735
+ message,
1736
+ validate: (value) => value.trim() ? true : tr(lang, "cli", "init.validation.enterPath")
1737
+ }
1738
+ ],
1583
1739
  {
1584
- type: options.beProjectRoot ? null : "text",
1585
- name: "beRoot",
1586
- message: tr(lang, "cli", "init.prompt.beRepoPath"),
1587
- validate: (value) => value.trim() ? true : tr(lang, "cli", "init.validation.enterPath")
1588
- }
1589
- ],
1590
- {
1591
- onCancel: () => {
1592
- throw new Error("canceled");
1740
+ onCancel: () => {
1741
+ throw new Error("canceled");
1742
+ }
1593
1743
  }
1594
- }
1595
- );
1596
- const currentRoot = typeof projectRoot === "string" ? { fe: "", be: "" } : projectRoot;
1597
- projectRoot = {
1598
- fe: projectRootResponse.feRoot || currentRoot?.fe || "",
1599
- be: projectRootResponse.beRoot || currentRoot?.be || ""
1600
- };
1744
+ );
1745
+ rootMap[component] = (response2.componentRoot || "").trim();
1746
+ }
1747
+ projectRoot = rootMap;
1601
1748
  } else {
1602
1749
  const projectRootResponse = await prompts(
1603
1750
  [
@@ -1700,7 +1847,7 @@ async function runInit(options) {
1700
1847
  components.forEach(assertValidComponentId);
1701
1848
  }
1702
1849
  if (docsRepo !== "standalone") {
1703
- if (options.projectRoot || options.feProjectRoot || options.beProjectRoot || typeof options.pushDocs === "boolean" || options.docsRemote) {
1850
+ if (options.projectRoot || options.feProjectRoot || options.beProjectRoot || options.componentProjectRoots || typeof options.pushDocs === "boolean" || options.docsRemote) {
1704
1851
  throw createCliError(
1705
1852
  "INVALID_ARGUMENT",
1706
1853
  "Standalone-only options require `--docs-repo standalone`."
@@ -1711,21 +1858,54 @@ async function runInit(options) {
1711
1858
  docsRemote = void 0;
1712
1859
  } else {
1713
1860
  if (projectType === "multi") {
1714
- if (!isDefaultFullstackComponents(components)) {
1861
+ const multiRoot = {};
1862
+ if (typeof projectRoot === "string" && projectRoot.trim()) {
1863
+ Object.assign(multiRoot, parseStandaloneMultiProjectRootJson(projectRoot));
1864
+ } else if (projectRoot && typeof projectRoot === "object") {
1865
+ for (const [component, root] of Object.entries(projectRoot)) {
1866
+ const normalized = component.trim().toLowerCase();
1867
+ const normalizedRoot = String(root || "").trim();
1868
+ if (!normalized || !normalizedRoot) continue;
1869
+ multiRoot[normalized] = normalizedRoot;
1870
+ }
1871
+ }
1872
+ if (options.feProjectRoot?.trim()) multiRoot.fe = options.feProjectRoot.trim();
1873
+ if (options.beProjectRoot?.trim()) multiRoot.be = options.beProjectRoot.trim();
1874
+ Object.assign(multiRoot, componentProjectRoots);
1875
+ const unknownComponents = Object.keys(multiRoot).filter(
1876
+ (component) => !components.includes(component)
1877
+ );
1878
+ if (unknownComponents.length > 0) {
1715
1879
  throw createCliError(
1716
1880
  "INVALID_ARGUMENT",
1717
- "Standalone multi init currently supports default components only: fe,be."
1881
+ `Standalone multi project roots contain unknown components: ${unknownComponents.join(", ")}. Allowed: ${components.join(", ")}`
1718
1882
  );
1719
1883
  }
1720
- const fullstackRoot = typeof projectRoot === "object" ? projectRoot : { fe: "", be: "" };
1721
- if (!fullstackRoot.fe.trim() || !fullstackRoot.be.trim()) {
1884
+ const missingComponents = components.filter(
1885
+ (component) => !(multiRoot[component] || "").trim()
1886
+ );
1887
+ if (missingComponents.length > 0) {
1722
1888
  throw createCliError(
1723
1889
  "PROMPT_BLOCKED",
1724
- "Standalone multi mode requires both `--fe-project-root` and `--be-project-root`."
1890
+ `Standalone multi mode requires project roots for all components: ${missingComponents.join(", ")}. Use \`--component-project-roots <component=/path,...>\` or \`--project-root '{"component":"/path"}'\`.`
1725
1891
  );
1726
1892
  }
1727
- projectRoot = fullstackRoot;
1893
+ projectRoot = Object.fromEntries(
1894
+ components.map((component) => [component, multiRoot[component].trim()])
1895
+ );
1728
1896
  } else {
1897
+ if (options.componentProjectRoots) {
1898
+ throw createCliError(
1899
+ "INVALID_ARGUMENT",
1900
+ "`--component-project-roots` can only be used when `--type multi`."
1901
+ );
1902
+ }
1903
+ if (options.feProjectRoot || options.beProjectRoot) {
1904
+ throw createCliError(
1905
+ "INVALID_ARGUMENT",
1906
+ "`--fe-project-root` and `--be-project-root` can only be used when `--type multi`."
1907
+ );
1908
+ }
1729
1909
  const singleRoot = typeof projectRoot === "string" ? projectRoot.trim() : "";
1730
1910
  if (!singleRoot) {
1731
1911
  throw createCliError(
@@ -1752,8 +1932,8 @@ async function runInit(options) {
1752
1932
  await withFileLock(
1753
1933
  initLockPath,
1754
1934
  async () => {
1755
- if (await fs14.pathExists(targetDir)) {
1756
- const files = await fs14.readdir(targetDir);
1935
+ if (await fs15.pathExists(targetDir)) {
1936
+ const files = await fs15.readdir(targetDir);
1757
1937
  if (files.length > 0) {
1758
1938
  if (options.force) {
1759
1939
  } else if (options.nonInteractive) {
@@ -1790,32 +1970,22 @@ async function runInit(options) {
1790
1970
  console.log();
1791
1971
  const templatesDir = getTemplatesDir();
1792
1972
  const commonPath = path18.join(templatesDir, lang, "common");
1793
- const templateProjectType = toTemplateProjectType(projectType);
1794
- const typePath = path18.join(templatesDir, lang, templateProjectType);
1795
- if (await fs14.pathExists(commonPath)) {
1796
- await copyTemplates(commonPath, targetDir);
1797
- }
1798
- if (!await fs14.pathExists(typePath)) {
1973
+ if (!await fs15.pathExists(commonPath)) {
1799
1974
  throw new Error(
1800
- tr(lang, "cli", "init.error.templateNotFound", { path: typePath })
1975
+ tr(lang, "cli", "init.error.templateNotFound", { path: commonPath })
1801
1976
  );
1802
1977
  }
1803
- await copyTemplates(typePath, targetDir);
1804
- if (projectType === "multi" && !isDefaultFullstackComponents(components)) {
1978
+ await copyTemplates(commonPath, targetDir);
1979
+ if (projectType === "multi") {
1805
1980
  const featuresRoot = path18.join(targetDir, "features");
1806
- await fs14.remove(path18.join(featuresRoot, "fe"));
1807
- await fs14.remove(path18.join(featuresRoot, "be"));
1808
1981
  for (const component of components) {
1809
1982
  const componentDir = path18.join(featuresRoot, component);
1810
- await fs14.ensureDir(componentDir);
1983
+ await fs15.ensureDir(componentDir);
1811
1984
  const readmePath = path18.join(componentDir, "README.md");
1812
- if (!await fs14.pathExists(readmePath)) {
1813
- await fs14.writeFile(
1985
+ if (!await fs15.pathExists(readmePath)) {
1986
+ await fs15.writeFile(
1814
1987
  readmePath,
1815
- `# ${component.toUpperCase()} Features
1816
-
1817
- Store ${component} feature specs here.
1818
- `,
1988
+ getComponentFeaturesReadme(lang, component),
1819
1989
  "utf-8"
1820
1990
  );
1821
1991
  }
@@ -1824,6 +1994,7 @@ Store ${component} feature specs here.
1824
1994
  const featurePath = projectType === "multi" ? isDefaultFullstackComponents(components) ? "docs/features/{be|fe}" : "docs/features/{component}" : "docs/features";
1825
1995
  const replacements = {
1826
1996
  "{{projectName}}": projectName,
1997
+ "{{projectType}}": projectType,
1827
1998
  "{{date}}": getLocalDateString(),
1828
1999
  "{{featurePath}}": featurePath
1829
2000
  };
@@ -1839,6 +2010,7 @@ Store ${component} feature specs here.
1839
2010
  workflow: {
1840
2011
  mode: workflowMode,
1841
2012
  codeDirtyScope: "auto",
2013
+ taskCommitGate: "strict",
1842
2014
  prePrReview: { skills: ["code-review-excellence"] }
1843
2015
  },
1844
2016
  pr: {
@@ -1860,7 +2032,7 @@ Store ${component} feature specs here.
1860
2032
  }
1861
2033
  }
1862
2034
  const configPath = path18.join(targetDir, ".lee-spec-kit.json");
1863
- await fs14.writeJson(configPath, config, { spaces: 2 });
2035
+ await fs15.writeJson(configPath, config, { spaces: 2 });
1864
2036
  console.log(chalk6.green(tr(lang, "cli", "init.log.docsCreated")));
1865
2037
  console.log();
1866
2038
  await initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote);
@@ -1906,7 +2078,7 @@ async function initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote) {
1906
2078
  }
1907
2079
  };
1908
2080
  const toGitPath = (input) => input.replace(/\\/g, "/").replace(/^\.\//, "");
1909
- const toRepoRelativePath = (workdir, relativePath2) => {
2081
+ const toRepoRelativePath2 = (workdir, relativePath2) => {
1910
2082
  if (relativePath2 === ".") return ".";
1911
2083
  try {
1912
2084
  const prefix = execFileSync("git", ["rev-parse", "--show-prefix"], {
@@ -1941,7 +2113,7 @@ async function initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote) {
1941
2113
  return;
1942
2114
  }
1943
2115
  if (relativePath !== "." && isPathIgnored(cwd, relativePath)) {
1944
- const repoRelativePath = toRepoRelativePath(cwd, relativePath);
2116
+ const repoRelativePath = toRepoRelativePath2(cwd, relativePath);
1945
2117
  console.log(
1946
2118
  chalk6.yellow(
1947
2119
  tr(lang, "cli", "init.warn.docsPathIgnoredSkipCommit", {
@@ -1995,7 +2167,7 @@ function getAncestorDirs(startDir) {
1995
2167
  return dirs;
1996
2168
  }
1997
2169
  function hasWorkspaceBoundary(dir) {
1998
- return fs14.existsSync(path18.join(dir, "package.json")) || fs14.existsSync(path18.join(dir, ".git"));
2170
+ return fs15.existsSync(path18.join(dir, "package.json")) || fs15.existsSync(path18.join(dir, ".git"));
1999
2171
  }
2000
2172
  function getSearchBaseDirs(cwd) {
2001
2173
  const ancestors = getAncestorDirs(cwd);
@@ -2023,9 +2195,9 @@ async function getConfig(cwd) {
2023
2195
  if (visitedDocsDirs.has(resolvedDocsDir)) continue;
2024
2196
  visitedDocsDirs.add(resolvedDocsDir);
2025
2197
  const configPath = path18.join(resolvedDocsDir, ".lee-spec-kit.json");
2026
- if (await fs14.pathExists(configPath)) {
2198
+ if (await fs15.pathExists(configPath)) {
2027
2199
  try {
2028
- const configFile = await fs14.readJson(configPath);
2200
+ const configFile = await fs15.readJson(configPath);
2029
2201
  const projectType = normalizeProjectType(configFile.projectType);
2030
2202
  const components = resolveProjectComponents(
2031
2203
  projectType,
@@ -2050,10 +2222,10 @@ async function getConfig(cwd) {
2050
2222
  }
2051
2223
  const agentsPath = path18.join(resolvedDocsDir, "agents");
2052
2224
  const featuresPath = path18.join(resolvedDocsDir, "features");
2053
- if (await fs14.pathExists(agentsPath) && await fs14.pathExists(featuresPath)) {
2225
+ if (await fs15.pathExists(agentsPath) && await fs15.pathExists(featuresPath)) {
2054
2226
  const bePath = path18.join(featuresPath, "be");
2055
2227
  const fePath = path18.join(featuresPath, "fe");
2056
- const projectType = await fs14.pathExists(bePath) || await fs14.pathExists(fePath) ? "multi" : "single";
2228
+ const projectType = await fs15.pathExists(bePath) || await fs15.pathExists(fePath) ? "multi" : "single";
2057
2229
  const components = projectType === "multi" ? resolveProjectComponents("multi", ["fe", "be"]) : void 0;
2058
2230
  const langProbeCandidates = [
2059
2231
  path18.join(agentsPath, "custom.md"),
@@ -2062,8 +2234,8 @@ async function getConfig(cwd) {
2062
2234
  ];
2063
2235
  let lang = "en";
2064
2236
  for (const candidate of langProbeCandidates) {
2065
- if (!await fs14.pathExists(candidate)) continue;
2066
- const content = await fs14.readFile(candidate, "utf-8");
2237
+ if (!await fs15.pathExists(candidate)) continue;
2238
+ const content = await fs15.readFile(candidate, "utf-8");
2067
2239
  if (/[가-힣]/.test(content)) {
2068
2240
  lang = "ko";
2069
2241
  break;
@@ -2109,9 +2281,9 @@ function sanitizeTasksForLocal(content, lang) {
2109
2281
  return normalizeTrailingBlankLines(next);
2110
2282
  }
2111
2283
  async function patchMarkdownIfExists(filePath, transform) {
2112
- if (!await fs14.pathExists(filePath)) return;
2113
- const content = await fs14.readFile(filePath, "utf-8");
2114
- await fs14.writeFile(filePath, transform(content), "utf-8");
2284
+ if (!await fs15.pathExists(filePath)) return;
2285
+ const content = await fs15.readFile(filePath, "utf-8");
2286
+ await fs15.writeFile(filePath, transform(content), "utf-8");
2115
2287
  }
2116
2288
  async function applyLocalWorkflowTemplateToFeatureDir(featureDir, lang) {
2117
2289
  await patchMarkdownIfExists(path18.join(featureDir, "spec.md"), sanitizeSpecForLocal);
@@ -2123,7 +2295,7 @@ async function applyLocalWorkflowTemplateToFeatureDir(featureDir, lang) {
2123
2295
 
2124
2296
  // src/commands/feature.ts
2125
2297
  function featureCommand(program2) {
2126
- program2.command("feature <name>").description("Create a new feature folder").option("-r, --repo <repo>", "Component name (multi only)").option("--component <component>", "Component name (multi only)").option("--id <id>", "Feature ID (default: auto)").option("-d, --desc <description>", "Feature description for spec.md").option("--non-interactive", "Fail instead of prompting for input").option("--json", "Output in JSON format for agents").action(async (name, options) => {
2298
+ program2.command("feature <name>").description("Create a new feature folder").option("--component <component>", "Component name (multi only)").option("--id <id>", "Feature ID (default: auto)").option("-d, --desc <description>", "Feature description for spec.md").option("--non-interactive", "Fail instead of prompting for input").option("--json", "Output in JSON format for agents").action(async (name, options) => {
2127
2299
  try {
2128
2300
  const result = await runFeature(name, options);
2129
2301
  if (options.json) {
@@ -2207,25 +2379,19 @@ async function runFeature(name, options) {
2207
2379
  tr(lang, "cli", "validation.context.featureName"),
2208
2380
  lang
2209
2381
  );
2210
- if (options.repo && options.component && options.repo.trim().toLowerCase() !== options.component.trim().toLowerCase()) {
2211
- throw createCliError(
2212
- "INVALID_ARGUMENT",
2213
- "`--repo` and `--component` must reference the same value when both are provided."
2214
- );
2215
- }
2216
- let component = (options.component || options.repo || "").trim().toLowerCase();
2382
+ let component = (options.component || "").trim().toLowerCase();
2217
2383
  if (!component) component = "";
2218
2384
  if (projectType === "single" && component) {
2219
2385
  throw createCliError(
2220
2386
  "INVALID_ARGUMENT",
2221
- "`--repo`/`--component` can only be used in multi mode."
2387
+ "`--component` can only be used in multi mode."
2222
2388
  );
2223
2389
  }
2224
2390
  if (projectType === "multi" && !component) {
2225
2391
  if (options.nonInteractive) {
2226
2392
  throw createCliError(
2227
2393
  "PROMPT_BLOCKED",
2228
- "`--component` (or `--repo`) is required in multi mode when using `--non-interactive`."
2394
+ "`--component` is required in multi mode when using `--non-interactive`."
2229
2395
  );
2230
2396
  }
2231
2397
  const response = await prompts(
@@ -2272,7 +2438,7 @@ async function runFeature(name, options) {
2272
2438
  }
2273
2439
  const featureFolderName = `${featureId}-${name}`;
2274
2440
  const featureDir = path18.join(featuresDir, featureFolderName);
2275
- if (await fs14.pathExists(featureDir)) {
2441
+ if (await fs15.pathExists(featureDir)) {
2276
2442
  throw createCliError(
2277
2443
  "INVALID_ARGUMENT",
2278
2444
  tr(lang, "cli", "feature.folderExists", { path: featureDir })
@@ -2281,14 +2447,14 @@ async function runFeature(name, options) {
2281
2447
  const featureBasePath = path18.join(
2282
2448
  getTemplatesDir(),
2283
2449
  lang,
2284
- toTemplateProjectType(projectType),
2450
+ "common",
2285
2451
  "features",
2286
2452
  "feature-base"
2287
2453
  );
2288
- if (!await fs14.pathExists(featureBasePath)) {
2454
+ if (!await fs15.pathExists(featureBasePath)) {
2289
2455
  throw createCliError("DOCS_NOT_FOUND", tr(lang, "cli", "feature.baseNotFound"));
2290
2456
  }
2291
- await fs14.copy(featureBasePath, featureDir);
2457
+ await fs15.copy(featureBasePath, featureDir);
2292
2458
  const idNumber = featureId.replace("F", "");
2293
2459
  const repoName = projectType === "multi" ? `{{projectName}}-${component}` : "{{projectName}}";
2294
2460
  const replacements = {
@@ -2360,7 +2526,7 @@ async function waitForConfigAfterInit(cwd, timeoutMs = 8e3) {
2360
2526
  const initLockPath = getInitLockPath(dir);
2361
2527
  const docsLockPath = getDocsLockPath(dir);
2362
2528
  for (const lockPath of [initLockPath, docsLockPath]) {
2363
- if (await fs14.pathExists(lockPath)) {
2529
+ if (await fs15.pathExists(lockPath)) {
2364
2530
  sawLock = true;
2365
2531
  await waitForLockRelease(lockPath, {
2366
2532
  timeoutMs: Math.max(200, endAt - Date.now()),
@@ -2385,8 +2551,8 @@ async function getNextFeatureId(docsDir, projectType, components) {
2385
2551
  scanDirs.push(featuresDir);
2386
2552
  }
2387
2553
  for (const dir of scanDirs) {
2388
- if (!await fs14.pathExists(dir)) continue;
2389
- const entries = await fs14.readdir(dir, { withFileTypes: true });
2554
+ if (!await fs15.pathExists(dir)) continue;
2555
+ const entries = await fs15.readdir(dir, { withFileTypes: true });
2390
2556
  for (const entry of entries) {
2391
2557
  if (!entry.isDirectory()) continue;
2392
2558
  const match = entry.name.match(/^F(\d+)-/);
@@ -2449,6 +2615,11 @@ function resolveCodeDirtyScopePolicy(workflow, projectType) {
2449
2615
  }
2450
2616
  return projectType === "multi" ? "component" : "repo";
2451
2617
  }
2618
+ function resolveTaskCommitGatePolicy(workflow) {
2619
+ const raw = workflow?.taskCommitGate;
2620
+ if (raw === "off" || raw === "warn" || raw === "strict") return raw;
2621
+ return "warn";
2622
+ }
2452
2623
  function normalizeSkillList(input) {
2453
2624
  if (!Array.isArray(input)) return [];
2454
2625
  const deduped = /* @__PURE__ */ new Set();
@@ -2509,9 +2680,105 @@ function resolveProjectCommitTopic(feature) {
2509
2680
  const topic = withoutTaskId || normalizeCommitTopicText(feature.folderName);
2510
2681
  return toShellSafeCommitTopic(topic);
2511
2682
  }
2683
+ function normalizeGitRelativePath(value) {
2684
+ return value.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
2685
+ }
2686
+ function toRepoRelativePath(cwd, relativePathFromCwd) {
2687
+ const prefix = (readGitText(cwd, ["rev-parse", "--show-prefix"]) || "").trim().replace(/\/+$/, "");
2688
+ if (!prefix) return normalizeGitRelativePath(relativePathFromCwd);
2689
+ return normalizeGitRelativePath(`${prefix}/${relativePathFromCwd}`);
2690
+ }
2691
+ function readGitText(cwd, args) {
2692
+ try {
2693
+ return execFileSync("git", args, {
2694
+ cwd,
2695
+ encoding: "utf-8",
2696
+ stdio: ["ignore", "pipe", "pipe"]
2697
+ });
2698
+ } catch {
2699
+ return void 0;
2700
+ }
2701
+ }
2702
+ function normalizeTaskTopic(value) {
2703
+ return normalizeCommitTopicText(value).replace(/^T-[A-Za-z0-9-]+\s+/, "");
2704
+ }
2705
+ function parseDoneTaskTopicCounts(content) {
2706
+ const counts = /* @__PURE__ */ new Map();
2707
+ const lines = content.split("\n");
2708
+ let inCodeBlock = false;
2709
+ for (const line of lines) {
2710
+ if (/^\s*(```|~~~)/.test(line)) {
2711
+ inCodeBlock = !inCodeBlock;
2712
+ continue;
2713
+ }
2714
+ if (inCodeBlock) continue;
2715
+ const match = line.match(/^\s*-\s*\[([A-Z]+)\]((?:\[[^\]]+\])*)\s*(.+?)\s*$/);
2716
+ if (!match) continue;
2717
+ if (match[1].toUpperCase() !== "DONE") continue;
2718
+ const topic = normalizeTaskTopic(match[3] || "");
2719
+ if (!topic) continue;
2720
+ counts.set(topic, (counts.get(topic) || 0) + 1);
2721
+ }
2722
+ return counts;
2723
+ }
2724
+ function checkTaskCommitGate(feature) {
2725
+ const tasksPath = normalizeGitRelativePath(`${feature.docs.featurePathFromDocs}/tasks.md`);
2726
+ const docsGitCwd = feature.git.docsGitCwd;
2727
+ const repoTasksPath = toRepoRelativePath(docsGitCwd, tasksPath);
2728
+ const latestTasksCommit = (readGitText(docsGitCwd, ["rev-list", "-n", "1", "HEAD", "--", tasksPath]) || "").trim();
2729
+ if (!latestTasksCommit) {
2730
+ return { pass: false, reason: "NO_TASKS_COMMIT" };
2731
+ }
2732
+ const currentContent = readGitText(docsGitCwd, ["show", `${latestTasksCommit}:${repoTasksPath}`]);
2733
+ if (currentContent === void 0) {
2734
+ return { pass: false, reason: "TASKS_FILE_UNAVAILABLE" };
2735
+ }
2736
+ const previousContent = readGitText(docsGitCwd, ["show", `${latestTasksCommit}^:${repoTasksPath}`]) || "";
2737
+ const currentDone = parseDoneTaskTopicCounts(currentContent);
2738
+ const previousDone = parseDoneTaskTopicCounts(previousContent);
2739
+ let newDoneCount = 0;
2740
+ for (const [topic, currentCount] of currentDone.entries()) {
2741
+ const previousCount = previousDone.get(topic) || 0;
2742
+ if (currentCount > previousCount) {
2743
+ newDoneCount += currentCount - previousCount;
2744
+ }
2745
+ }
2746
+ if (newDoneCount !== 1) {
2747
+ return {
2748
+ pass: false,
2749
+ reason: "MULTIPLE_DONE_TRANSITIONS",
2750
+ newDoneCount
2751
+ };
2752
+ }
2753
+ const lastDoneTopic = normalizeTaskTopic(feature.lastDoneTask?.title || "");
2754
+ if (lastDoneTopic) {
2755
+ const previousCount = previousDone.get(lastDoneTopic) || 0;
2756
+ const currentCount = currentDone.get(lastDoneTopic) || 0;
2757
+ if (currentCount <= previousCount) {
2758
+ return { pass: false, reason: "MISMATCH_LAST_DONE", newDoneCount };
2759
+ }
2760
+ }
2761
+ return { pass: true, reason: "MULTIPLE_DONE_TRANSITIONS", newDoneCount };
2762
+ }
2763
+ function getTaskCommitGateReasonText(lang, check) {
2764
+ switch (check.reason) {
2765
+ case "NO_TASKS_COMMIT":
2766
+ return tr(lang, "messages", "taskCommitGateReasonNoTasksCommit");
2767
+ case "TASKS_FILE_UNAVAILABLE":
2768
+ return tr(lang, "messages", "taskCommitGateReasonTasksFileUnavailable");
2769
+ case "MISMATCH_LAST_DONE":
2770
+ return tr(lang, "messages", "taskCommitGateReasonMismatchLastDone");
2771
+ case "MULTIPLE_DONE_TRANSITIONS":
2772
+ default:
2773
+ return tr(lang, "messages", "taskCommitGateReasonDoneCount", {
2774
+ count: check.newDoneCount ?? 0
2775
+ });
2776
+ }
2777
+ }
2512
2778
  function getStepDefinitions(lang, workflow) {
2513
2779
  const workflowPolicy = resolveWorkflowPolicy(workflow);
2514
2780
  const prePrReviewPolicy = resolvePrePrReviewPolicy(workflow);
2781
+ const taskCommitGatePolicy = resolveTaskCommitGatePolicy(workflow);
2515
2782
  return [
2516
2783
  {
2517
2784
  step: 1,
@@ -2842,6 +3109,39 @@ function getStepDefinitions(lang, workflow) {
2842
3109
  }
2843
3110
  ];
2844
3111
  }
3112
+ if (taskCommitGatePolicy !== "off" && f.lastDoneTask) {
3113
+ const commitGate = checkTaskCommitGate(f);
3114
+ if (!commitGate.pass) {
3115
+ const reasonText = getTaskCommitGateReasonText(lang, commitGate);
3116
+ if (taskCommitGatePolicy === "strict") {
3117
+ return [
3118
+ {
3119
+ type: "instruction",
3120
+ category: "task_execute",
3121
+ requiresUserCheck: true,
3122
+ message: tr(lang, "messages", "taskCommitGateStrictBlock", {
3123
+ reason: reasonText
3124
+ })
3125
+ }
3126
+ ];
3127
+ }
3128
+ return [
3129
+ {
3130
+ type: "instruction",
3131
+ category: "task_execute",
3132
+ requiresUserCheck: true,
3133
+ message: `${tr(lang, "messages", "startNextTodoTask", {
3134
+ title: f.nextTodoTask.title,
3135
+ done: f.tasks.done,
3136
+ total: f.tasks.total
3137
+ })}
3138
+ ${tr(lang, "messages", "taskCommitGateWarnProceed", {
3139
+ reason: reasonText
3140
+ })}`
3141
+ }
3142
+ ];
3143
+ }
3144
+ }
2845
3145
  return [
2846
3146
  {
2847
3147
  type: "instruction",
@@ -3429,7 +3729,7 @@ async function resolveComponentStatusPaths(projectGitCwd, component, workflow) {
3429
3729
  if (cached) return [...cached];
3430
3730
  const existing = [];
3431
3731
  for (const candidate of normalizedCandidates) {
3432
- if (await fs14.pathExists(path18.join(projectGitCwd, candidate))) {
3732
+ if (await fs15.pathExists(path18.join(projectGitCwd, candidate))) {
3433
3733
  existing.push(candidate);
3434
3734
  }
3435
3735
  }
@@ -3453,18 +3753,20 @@ function parseTasks(content) {
3453
3753
  if (!match) continue;
3454
3754
  const status = match[1].toUpperCase();
3455
3755
  const title = match[3].trim();
3756
+ const taskIdMatch = title.match(/\b(T-[A-Za-z0-9-]+)\b/);
3757
+ const taskId = taskIdMatch ? taskIdMatch[1] : void 0;
3456
3758
  summary.total++;
3457
3759
  if (status === "DONE") summary.done++;
3458
3760
  else if (status === "DOING" || status === "REVIEW") summary.doing++;
3459
3761
  else if (status === "TODO") summary.todo++;
3460
3762
  if (!activeTask && (status === "DOING" || status === "REVIEW")) {
3461
- activeTask = { status, title };
3763
+ activeTask = { id: taskId, status, title };
3462
3764
  }
3463
3765
  if (status === "DONE") {
3464
- lastDoneTask = { status: "DONE", title };
3766
+ lastDoneTask = { id: taskId, status: "DONE", title };
3465
3767
  }
3466
3768
  if (!nextTodoTask && status === "TODO") {
3467
- nextTodoTask = { status: "TODO", title };
3769
+ nextTodoTask = { id: taskId, status: "TODO", title };
3468
3770
  }
3469
3771
  }
3470
3772
  return { summary, activeTask, lastDoneTask, nextTodoTask };
@@ -3506,22 +3808,22 @@ async function parseFeature(featurePath, type, context, options) {
3506
3808
  const tasksPath = path18.join(featurePath, "tasks.md");
3507
3809
  let specStatus;
3508
3810
  let issueNumber;
3509
- const specExists = await fs14.pathExists(specPath);
3811
+ const specExists = await fs15.pathExists(specPath);
3510
3812
  if (specExists) {
3511
- const content = await fs14.readFile(specPath, "utf-8");
3813
+ const content = await fs15.readFile(specPath, "utf-8");
3512
3814
  const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
3513
3815
  specStatus = parseDocStatus(statusValue);
3514
3816
  const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
3515
3817
  issueNumber = parseIssueNumber(issueValue);
3516
3818
  }
3517
3819
  let planStatus;
3518
- const planExists = await fs14.pathExists(planPath);
3820
+ const planExists = await fs15.pathExists(planPath);
3519
3821
  if (planExists) {
3520
- const content = await fs14.readFile(planPath, "utf-8");
3822
+ const content = await fs15.readFile(planPath, "utf-8");
3521
3823
  const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
3522
3824
  planStatus = parseDocStatus(statusValue);
3523
3825
  }
3524
- const tasksExists = await fs14.pathExists(tasksPath);
3826
+ const tasksExists = await fs15.pathExists(tasksPath);
3525
3827
  const tasksSummary = { total: 0, todo: 0, doing: 0, done: 0 };
3526
3828
  let activeTask;
3527
3829
  let lastDoneTask;
@@ -3536,7 +3838,7 @@ async function parseFeature(featurePath, type, context, options) {
3536
3838
  let prStatusFieldExists = false;
3537
3839
  let prePrReviewFieldExists = false;
3538
3840
  if (tasksExists) {
3539
- const content = await fs14.readFile(tasksPath, "utf-8");
3841
+ const content = await fs15.readFile(tasksPath, "utf-8");
3540
3842
  const {
3541
3843
  summary,
3542
3844
  activeTask: active,
@@ -4076,7 +4378,7 @@ async function runStatus(options) {
4076
4378
  ),
4077
4379
  ""
4078
4380
  ].join("\n");
4079
- await fs14.writeFile(outputPath, content, "utf-8");
4381
+ await fs15.writeFile(outputPath, content, "utf-8");
4080
4382
  console.log(
4081
4383
  chalk6.green(
4082
4384
  tr(lang, "cli", "status.wrote", { path: outputPath })
@@ -4090,8 +4392,8 @@ function escapeRegExp2(value) {
4090
4392
  async function getFeatureNameFromSpec(featureDir, fallbackSlug, fallbackFolderName) {
4091
4393
  try {
4092
4394
  const specPath = path18.join(featureDir, "spec.md");
4093
- if (!await fs14.pathExists(specPath)) return fallbackSlug;
4094
- const content = await fs14.readFile(specPath, "utf-8");
4395
+ if (!await fs15.pathExists(specPath)) return fallbackSlug;
4396
+ const content = await fs15.readFile(specPath, "utf-8");
4095
4397
  const keys = ["\uAE30\uB2A5\uBA85", "Feature Name"];
4096
4398
  for (const key of keys) {
4097
4399
  const regex = new RegExp(
@@ -4148,6 +4450,7 @@ async function runUpdate(options) {
4148
4450
  const templatesDir = getTemplatesDir();
4149
4451
  const docsLockPath = getDocsLockPath(docsDir);
4150
4452
  const forceOverwrite = !!options.force || await isDocsWorktreeCleanOrThrow(docsDir, lang, [docsLockPath]);
4453
+ const configBackfill = await backfillMissingConfigDefaults(docsDir);
4151
4454
  const hasExplicitSelection = !!(options.agents || options.skills || options.templates);
4152
4455
  const updateAgents = options.agents || options.skills || !hasExplicitSelection;
4153
4456
  const updateTemplates = options.templates || !hasExplicitSelection;
@@ -4171,15 +4474,8 @@ async function runUpdate(options) {
4171
4474
  }
4172
4475
  if (agentsMode === "all") {
4173
4476
  const commonAgentsBase = path18.join(templatesDir, lang, "common", "agents");
4174
- const typeAgentsBase = path18.join(
4175
- templatesDir,
4176
- lang,
4177
- toTemplateProjectType(projectType),
4178
- "agents"
4179
- );
4180
4477
  const targetAgentsBase = path18.join(docsDir, "agents");
4181
4478
  const commonAgents = agentsMode === "skills" ? path18.join(commonAgentsBase, "skills") : commonAgentsBase;
4182
- const typeAgents = agentsMode === "skills" ? path18.join(typeAgentsBase, "skills") : typeAgentsBase;
4183
4479
  const targetAgents = agentsMode === "skills" ? path18.join(targetAgentsBase, "skills") : targetAgentsBase;
4184
4480
  const featurePath = projectType === "multi" ? isDefaultFullstackComponents(config.components || []) ? "docs/features/{be|fe}" : "docs/features/{component}" : "docs/features";
4185
4481
  const projectName = config.projectName ?? "{{projectName}}";
@@ -4187,10 +4483,7 @@ async function runUpdate(options) {
4187
4483
  "{{projectName}}": projectName,
4188
4484
  "{{featurePath}}": featurePath
4189
4485
  };
4190
- const typeReplacements = {
4191
- "{{projectName}}": projectName
4192
- };
4193
- if (await fs14.pathExists(commonAgents)) {
4486
+ if (await fs15.pathExists(commonAgents)) {
4194
4487
  const count = await updateFolder(
4195
4488
  commonAgents,
4196
4489
  targetAgents,
@@ -4208,24 +4501,6 @@ async function runUpdate(options) {
4208
4501
  );
4209
4502
  updatedCount += count;
4210
4503
  }
4211
- if (await fs14.pathExists(typeAgents)) {
4212
- const count = await updateFolder(
4213
- typeAgents,
4214
- targetAgents,
4215
- forceOverwrite,
4216
- typeReplacements,
4217
- lang,
4218
- {
4219
- protectedFiles: /* @__PURE__ */ new Set([
4220
- "custom.md",
4221
- "constitution.md",
4222
- ...ENGINE_MANAGED_AGENT_FILES
4223
- ]),
4224
- skipDirectories: new Set(ENGINE_MANAGED_AGENT_DIRS)
4225
- }
4226
- );
4227
- updatedCount += count;
4228
- }
4229
4504
  console.log(
4230
4505
  chalk6.green(
4231
4506
  ` \u2705 ${agentsMode === "skills" ? tr(lang, "cli", "update.skillsUpdated") : tr(lang, "cli", "update.agentsUpdated")}`
@@ -4248,6 +4523,18 @@ async function runUpdate(options) {
4248
4523
  );
4249
4524
  }
4250
4525
  console.log();
4526
+ if (configBackfill.changed) {
4527
+ console.log(
4528
+ chalk6.gray(
4529
+ ` - ${tr(lang, "cli", "update.fileUpdated", { file: ".lee-spec-kit.json" })}`
4530
+ )
4531
+ );
4532
+ console.log(
4533
+ chalk6.gray(
4534
+ ` (${configBackfill.changedPaths.join(", ")})`
4535
+ )
4536
+ );
4537
+ }
4251
4538
  console.log(
4252
4539
  chalk6.green(`\u2705 ${tr(lang, "cli", "update.updatedTotal", { count: updatedCount })}`)
4253
4540
  );
@@ -4255,27 +4542,106 @@ async function runUpdate(options) {
4255
4542
  { owner: "update" }
4256
4543
  );
4257
4544
  }
4545
+ function isPlainObject(value) {
4546
+ return !!value && typeof value === "object" && !Array.isArray(value);
4547
+ }
4548
+ function normalizeSkillList2(raw) {
4549
+ if (!Array.isArray(raw)) return [];
4550
+ const deduped = /* @__PURE__ */ new Set();
4551
+ for (const item of raw) {
4552
+ const value = String(item || "").trim();
4553
+ if (!value) continue;
4554
+ deduped.add(value);
4555
+ }
4556
+ return [...deduped];
4557
+ }
4558
+ async function backfillMissingConfigDefaults(docsDir) {
4559
+ const configPath = path18.join(docsDir, ".lee-spec-kit.json");
4560
+ if (!await fs15.pathExists(configPath)) {
4561
+ return { changed: false, changedPaths: [] };
4562
+ }
4563
+ const raw = await fs15.readJson(configPath);
4564
+ if (!isPlainObject(raw)) {
4565
+ return { changed: false, changedPaths: [] };
4566
+ }
4567
+ const changedPaths = [];
4568
+ const setIfMissing = (parent, key, nextValue, pathLabel) => {
4569
+ if (parent[key] !== void 0) return;
4570
+ parent[key] = nextValue;
4571
+ changedPaths.push(pathLabel);
4572
+ };
4573
+ if (!isPlainObject(raw.workflow)) {
4574
+ raw.workflow = {};
4575
+ changedPaths.push("workflow");
4576
+ }
4577
+ const workflow = raw.workflow;
4578
+ setIfMissing(workflow, "mode", "github", "workflow.mode");
4579
+ setIfMissing(workflow, "codeDirtyScope", "auto", "workflow.codeDirtyScope");
4580
+ setIfMissing(workflow, "taskCommitGate", "strict", "workflow.taskCommitGate");
4581
+ if (!isPlainObject(workflow.prePrReview)) {
4582
+ workflow.prePrReview = {};
4583
+ changedPaths.push("workflow.prePrReview");
4584
+ }
4585
+ const prePrReview = workflow.prePrReview;
4586
+ if (prePrReview.skills === void 0) {
4587
+ prePrReview.skills = ["code-review-excellence"];
4588
+ changedPaths.push("workflow.prePrReview.skills");
4589
+ } else {
4590
+ const normalizedSkills = normalizeSkillList2(prePrReview.skills);
4591
+ if (normalizedSkills.length === 0) {
4592
+ prePrReview.skills = ["code-review-excellence"];
4593
+ changedPaths.push("workflow.prePrReview.skills");
4594
+ } else if (JSON.stringify(normalizedSkills) !== JSON.stringify(prePrReview.skills)) {
4595
+ prePrReview.skills = normalizedSkills;
4596
+ changedPaths.push("workflow.prePrReview.skills");
4597
+ }
4598
+ }
4599
+ setIfMissing(prePrReview, "fallback", "builtin-checklist", "workflow.prePrReview.fallback");
4600
+ setIfMissing(prePrReview, "blockOnFindings", true, "workflow.prePrReview.blockOnFindings");
4601
+ if (!isPlainObject(raw.pr)) {
4602
+ raw.pr = {};
4603
+ changedPaths.push("pr");
4604
+ }
4605
+ const pr = raw.pr;
4606
+ if (!isPlainObject(pr.screenshots)) {
4607
+ pr.screenshots = {};
4608
+ changedPaths.push("pr.screenshots");
4609
+ }
4610
+ const screenshots = pr.screenshots;
4611
+ setIfMissing(screenshots, "upload", false, "pr.screenshots.upload");
4612
+ if (!isPlainObject(raw.approval)) {
4613
+ raw.approval = {};
4614
+ changedPaths.push("approval");
4615
+ }
4616
+ const approval = raw.approval;
4617
+ setIfMissing(approval, "mode", "builtin", "approval.mode");
4618
+ if (changedPaths.length === 0) {
4619
+ return { changed: false, changedPaths: [] };
4620
+ }
4621
+ await fs15.writeJson(configPath, raw, { spaces: 2 });
4622
+ return { changed: true, changedPaths };
4623
+ }
4258
4624
  async function updateFolder(sourceDir, targetDir, force, replacements, lang = DEFAULT_LANG, options = {}) {
4259
4625
  const protectedFiles = options.protectedFiles ?? /* @__PURE__ */ new Set(["custom.md", "constitution.md"]);
4260
4626
  const skipDirectories = options.skipDirectories ?? /* @__PURE__ */ new Set();
4261
- await fs14.ensureDir(targetDir);
4262
- const files = await fs14.readdir(sourceDir);
4627
+ await fs15.ensureDir(targetDir);
4628
+ const files = await fs15.readdir(sourceDir);
4263
4629
  let updatedCount = 0;
4264
4630
  for (const file of files) {
4265
4631
  const sourcePath = path18.join(sourceDir, file);
4266
4632
  const targetPath = path18.join(targetDir, file);
4267
- const stat = await fs14.stat(sourcePath);
4633
+ const stat = await fs15.stat(sourcePath);
4268
4634
  if (stat.isFile()) {
4269
4635
  if (protectedFiles.has(file)) {
4270
4636
  continue;
4271
4637
  }
4272
- let sourceContent = await fs14.readFile(sourcePath, "utf-8");
4638
+ let sourceContent = await fs15.readFile(sourcePath, "utf-8");
4273
4639
  if (replacements) {
4274
4640
  sourceContent = applyReplacements(sourceContent, replacements);
4275
4641
  }
4276
4642
  let shouldUpdate = true;
4277
- if (await fs14.pathExists(targetPath)) {
4278
- const targetContent = await fs14.readFile(targetPath, "utf-8");
4643
+ if (await fs15.pathExists(targetPath)) {
4644
+ const targetContent = await fs15.readFile(targetPath, "utf-8");
4279
4645
  if (sourceContent === targetContent) {
4280
4646
  continue;
4281
4647
  }
@@ -4289,7 +4655,7 @@ async function updateFolder(sourceDir, targetDir, force, replacements, lang = DE
4289
4655
  }
4290
4656
  }
4291
4657
  if (shouldUpdate) {
4292
- await fs14.writeFile(targetPath, sourceContent);
4658
+ await fs15.writeFile(targetPath, sourceContent);
4293
4659
  console.log(
4294
4660
  chalk6.gray(` \u{1F4C4} ${tr(lang, "cli", "update.fileUpdated", { file })}`)
4295
4661
  );
@@ -4389,7 +4755,7 @@ async function isDocsWorktreeCleanOrThrow(docsDir, lang, ignoredAbsPaths = []) {
4389
4755
  return true;
4390
4756
  }
4391
4757
  function configCommand(program2) {
4392
- program2.command("config").description("View or modify project configuration").option("--dir <dir>", "Docs directory or project path to target").option("--project-root <path>", "Set project root path").option("--repo <repo>", "Component name for multi projects").option("--component <component>", "Component name for multi projects").option("--non-interactive", "Fail instead of prompting for input").action(async (options) => {
4758
+ program2.command("config").description("View or modify project configuration").option("--dir <dir>", "Docs directory or project path to target").option("--project-root <path>", "Set project root path").option("--component <component>", "Component name for multi projects").option("--non-interactive", "Fail instead of prompting for input").action(async (options) => {
4393
4759
  try {
4394
4760
  await runConfig(options);
4395
4761
  } catch (error) {
@@ -4434,7 +4800,7 @@ async function runConfig(options) {
4434
4800
  )
4435
4801
  );
4436
4802
  console.log();
4437
- const configFile = await fs14.readJson(configPath);
4803
+ const configFile = await fs15.readJson(configPath);
4438
4804
  console.log(JSON.stringify(configFile, null, 2));
4439
4805
  console.log();
4440
4806
  return;
@@ -4442,7 +4808,7 @@ async function runConfig(options) {
4442
4808
  await withFileLock(
4443
4809
  getDocsLockPath(config.docsDir),
4444
4810
  async () => {
4445
- const configFile = await fs14.readJson(configPath);
4811
+ const configFile = await fs15.readJson(configPath);
4446
4812
  if (configFile.docsRepo !== "standalone") {
4447
4813
  console.log(
4448
4814
  chalk6.yellow(tr(config.lang, "cli", "config.projectRootStandaloneOnly"))
@@ -4450,13 +4816,7 @@ async function runConfig(options) {
4450
4816
  return;
4451
4817
  }
4452
4818
  const projectType = normalizeProjectType(String(configFile.projectType || "single"));
4453
- const targetFromOptions = options.component?.trim().toLowerCase() || options.repo?.trim().toLowerCase();
4454
- if (options.component && options.repo && options.component.trim().toLowerCase() !== options.repo.trim().toLowerCase()) {
4455
- throw createCliError(
4456
- "INVALID_ARGUMENT",
4457
- "`--repo` and `--component` must reference the same value when both are provided."
4458
- );
4459
- }
4819
+ const targetFromOptions = options.component?.trim().toLowerCase();
4460
4820
  if (projectType === "multi") {
4461
4821
  const components = resolveProjectComponents(projectType, configFile.components);
4462
4822
  let targetComponent = targetFromOptions;
@@ -4464,7 +4824,7 @@ async function runConfig(options) {
4464
4824
  if (options.nonInteractive) {
4465
4825
  throw createCliError(
4466
4826
  "PROMPT_BLOCKED",
4467
- "`--component` (or `--repo`) is required for multi projectRoot update when using `--non-interactive`."
4827
+ "`--component` is required for multi projectRoot update when using `--non-interactive`."
4468
4828
  );
4469
4829
  }
4470
4830
  const response = await prompts(
@@ -4509,7 +4869,7 @@ async function runConfig(options) {
4509
4869
  if (targetFromOptions) {
4510
4870
  throw createCliError(
4511
4871
  "INVALID_ARGUMENT",
4512
- "`--repo`/`--component` is only valid for multi projectRoot updates."
4872
+ "`--component` is only valid for multi projectRoot updates."
4513
4873
  );
4514
4874
  }
4515
4875
  configFile.projectRoot = options.projectRoot;
@@ -4521,7 +4881,7 @@ async function runConfig(options) {
4521
4881
  )
4522
4882
  );
4523
4883
  }
4524
- await fs14.writeJson(configPath, configFile, { spaces: 2 });
4884
+ await fs15.writeJson(configPath, configFile, { spaces: 2 });
4525
4885
  console.log();
4526
4886
  },
4527
4887
  { owner: "config" }
@@ -4540,7 +4900,7 @@ var LOCAL_ACTION_CATEGORIES = /* @__PURE__ */ new Set([
4540
4900
  ]);
4541
4901
  var REMOTE_COMMAND_PATTERN = /\b(?:git\s+push|git\s+merge|gh\s+(?:issue|pr)\b)/i;
4542
4902
  function resolveComponentOption(options) {
4543
- const component = (options.component || options.repo || "").trim().toLowerCase();
4903
+ const component = (options.component || "").trim().toLowerCase();
4544
4904
  return component || void 0;
4545
4905
  }
4546
4906
  function getActionLabel(index) {
@@ -4611,7 +4971,7 @@ function toActionOptions(actions) {
4611
4971
  label,
4612
4972
  summary,
4613
4973
  detail,
4614
- approvalPrompt: `${label}: ${summary}`,
4974
+ approvalPrompt: `${label}: ${detail}`,
4615
4975
  action
4616
4976
  };
4617
4977
  });
@@ -4775,7 +5135,7 @@ var BUILTIN_DOC_DEFINITIONS = [
4775
5135
  {
4776
5136
  id: "agents",
4777
5137
  title: { ko: "\uC5D0\uC774\uC804\uD2B8 \uC6B4\uC601 \uADDC\uCE59", en: "Agent Operating Rules" },
4778
- relativePath: (projectType, lang) => path18.join(lang, toTemplateProjectType(projectType), "agents", "agents.md")
5138
+ relativePath: (_, lang) => path18.join(lang, "common", "agents", "agents.md")
4779
5139
  },
4780
5140
  {
4781
5141
  id: "git-workflow",
@@ -4903,7 +5263,7 @@ async function getBuiltinDoc(docId, projectType, lang) {
4903
5263
  if (!entry) {
4904
5264
  throw new Error(`Unknown builtin doc: ${docId}`);
4905
5265
  }
4906
- const content = await fs14.readFile(entry.absolutePath, "utf-8");
5266
+ const content = await fs15.readFile(entry.absolutePath, "utf-8");
4907
5267
  const hash = createHash("sha256").update(content).digest("hex").slice(0, 12);
4908
5268
  return {
4909
5269
  entry,
@@ -4914,6 +5274,8 @@ async function getBuiltinDoc(docId, projectType, lang) {
4914
5274
  }
4915
5275
 
4916
5276
  // src/commands/context.ts
5277
+ var APPROVAL_TICKET_FILENAME = ".lee-spec-kit.approval-tickets.json";
5278
+ var APPROVAL_TICKET_TTL_MS = 5 * 60 * 1e3;
4917
5279
  async function resolveContextState(config, featureName, options) {
4918
5280
  if (!config) {
4919
5281
  throw createCliError(
@@ -4923,10 +5285,176 @@ async function resolveContextState(config, featureName, options) {
4923
5285
  }
4924
5286
  return resolveContextSelection(config, featureName, options);
4925
5287
  }
4926
- function parseApprovalLabel(input) {
4927
- const match = input.trim().match(/^([A-Z]+)(?:\s+OK)?$/i);
4928
- if (!match) return null;
4929
- return match[1].toUpperCase();
5288
+ function parseApprovalLabel(input, validLabels) {
5289
+ const normalized = input.trim().toUpperCase();
5290
+ if (!normalized) return null;
5291
+ const validSet = new Set(
5292
+ validLabels.map((label) => label.trim().toUpperCase()).filter(Boolean)
5293
+ );
5294
+ if (validSet.size === 0) return null;
5295
+ const leading = normalized.match(/^[`"'([{<\s]*([A-Z]+)\b/);
5296
+ if (leading && validSet.has(leading[1])) return leading[1];
5297
+ const tokens = normalized.match(/[A-Z]+/g) || [];
5298
+ for (const token of tokens) {
5299
+ if (validSet.has(token)) return token;
5300
+ }
5301
+ return null;
5302
+ }
5303
+ function getApprovalTicketPath(config) {
5304
+ return path18.join(config.docsDir, APPROVAL_TICKET_FILENAME);
5305
+ }
5306
+ function getApprovalSessionId() {
5307
+ const explicit = (process.env.LEE_SPEC_KIT_SESSION_ID || "").trim();
5308
+ if (explicit) return explicit;
5309
+ const terminalSession = (process.env.TERM_SESSION_ID || process.env.WT_SESSION || process.env.TMUX_PANE || "").trim();
5310
+ if (terminalSession) return terminalSession;
5311
+ return `ppid:${process.ppid}`;
5312
+ }
5313
+ function toActionHash(option) {
5314
+ const payload = JSON.stringify({
5315
+ label: option.label,
5316
+ action: option.action
5317
+ });
5318
+ return createHash("sha256").update(payload).digest("hex").slice(0, 24);
5319
+ }
5320
+ async function loadApprovalTicketStore(storePath) {
5321
+ if (!await fs15.pathExists(storePath)) return { tickets: [] };
5322
+ try {
5323
+ const parsed = await fs15.readJson(storePath);
5324
+ if (!parsed || !Array.isArray(parsed.tickets)) return { tickets: [] };
5325
+ return { tickets: parsed.tickets };
5326
+ } catch {
5327
+ return { tickets: [] };
5328
+ }
5329
+ }
5330
+ function pruneApprovalTickets(tickets, nowMs) {
5331
+ return tickets.filter((ticket) => {
5332
+ if (ticket.usedAt) return false;
5333
+ const expiresAtMs = Date.parse(ticket.expiresAt || "");
5334
+ if (!Number.isFinite(expiresAtMs)) return false;
5335
+ return expiresAtMs > nowMs;
5336
+ });
5337
+ }
5338
+ async function issueApprovalTicket(config, payload) {
5339
+ const sessionId = getApprovalSessionId();
5340
+ const nowMs = Date.now();
5341
+ const record = {
5342
+ token: randomUUID().replace(/-/g, ""),
5343
+ sessionId,
5344
+ contextVersion: payload.contextVersion,
5345
+ actionHash: payload.actionHash,
5346
+ label: payload.label,
5347
+ featureRef: payload.featureRef,
5348
+ createdAt: new Date(nowMs).toISOString(),
5349
+ expiresAt: new Date(nowMs + APPROVAL_TICKET_TTL_MS).toISOString()
5350
+ };
5351
+ const storePath = getApprovalTicketPath(config);
5352
+ const lockPath = getDocsLockPath(config.docsDir);
5353
+ return withFileLock(
5354
+ lockPath,
5355
+ async () => {
5356
+ const store = await loadApprovalTicketStore(storePath);
5357
+ const nextTickets = pruneApprovalTickets(store.tickets, nowMs);
5358
+ nextTickets.push(record);
5359
+ await fs15.writeJson(
5360
+ storePath,
5361
+ {
5362
+ tickets: nextTickets,
5363
+ updatedAt: new Date(nowMs).toISOString()
5364
+ },
5365
+ { spaces: 2 }
5366
+ );
5367
+ return record;
5368
+ },
5369
+ { owner: "context-approval-ticket:issue" }
5370
+ );
5371
+ }
5372
+ async function consumeApprovalTicket(config, token, expected) {
5373
+ const normalizedToken = token.trim();
5374
+ if (!normalizedToken) {
5375
+ throw createCliError(
5376
+ "APPROVAL_REQUIRED",
5377
+ "Execution requires an approval ticket. Run `context --approve <reply> --json` first and pass `--ticket <token>`."
5378
+ );
5379
+ }
5380
+ const storePath = getApprovalTicketPath(config);
5381
+ const lockPath = getDocsLockPath(config.docsDir);
5382
+ const sessionId = getApprovalSessionId();
5383
+ const nowMs = Date.now();
5384
+ return withFileLock(
5385
+ lockPath,
5386
+ async () => {
5387
+ const store = await loadApprovalTicketStore(storePath);
5388
+ const cleaned = pruneApprovalTickets(store.tickets, nowMs);
5389
+ const index = cleaned.findIndex((entry) => entry.token === normalizedToken);
5390
+ if (index < 0) {
5391
+ await fs15.writeJson(
5392
+ storePath,
5393
+ { tickets: cleaned, updatedAt: new Date(nowMs).toISOString() },
5394
+ { spaces: 2 }
5395
+ );
5396
+ throw createCliError(
5397
+ "INVALID_APPROVAL",
5398
+ "Unknown or expired approval ticket. Re-run `context` and approve again."
5399
+ );
5400
+ }
5401
+ const record = cleaned[index];
5402
+ const expiresAtMs = Date.parse(record.expiresAt || "");
5403
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs) {
5404
+ cleaned.splice(index, 1);
5405
+ await fs15.writeJson(
5406
+ storePath,
5407
+ { tickets: cleaned, updatedAt: new Date(nowMs).toISOString() },
5408
+ { spaces: 2 }
5409
+ );
5410
+ throw createCliError(
5411
+ "CONTEXT_STALE",
5412
+ "Approval ticket expired. Run `context` again and re-approve."
5413
+ );
5414
+ }
5415
+ if (record.sessionId !== sessionId) {
5416
+ throw createCliError(
5417
+ "INVALID_APPROVAL",
5418
+ "Approval ticket session mismatch. Re-run `context` in the current session and approve again."
5419
+ );
5420
+ }
5421
+ if (record.label !== expected.label) {
5422
+ throw createCliError(
5423
+ "INVALID_APPROVAL",
5424
+ `Approval ticket label mismatch. Ticket=${record.label}, expected=${expected.label}.`
5425
+ );
5426
+ }
5427
+ if (record.contextVersion !== expected.contextVersion) {
5428
+ throw createCliError(
5429
+ "CONTEXT_STALE",
5430
+ "Context changed after approval. Run `context` again and re-approve."
5431
+ );
5432
+ }
5433
+ if (record.actionHash !== expected.actionHash) {
5434
+ throw createCliError(
5435
+ "CONTEXT_STALE",
5436
+ "Selected action changed after approval. Run `context` again and re-approve."
5437
+ );
5438
+ }
5439
+ if (record.featureRef !== expected.featureRef) {
5440
+ throw createCliError(
5441
+ "INVALID_APPROVAL",
5442
+ "Approval ticket feature mismatch. Re-run `context` for this feature and approve again."
5443
+ );
5444
+ }
5445
+ cleaned.splice(index, 1);
5446
+ await fs15.writeJson(
5447
+ storePath,
5448
+ {
5449
+ tickets: cleaned,
5450
+ updatedAt: new Date(nowMs).toISOString()
5451
+ },
5452
+ { spaces: 2 }
5453
+ );
5454
+ return record;
5455
+ },
5456
+ { owner: "context-approval-ticket:consume" }
5457
+ );
4930
5458
  }
4931
5459
  function listLabels(actionOptions) {
4932
5460
  if (actionOptions.length === 0) return "-";
@@ -4939,8 +5467,10 @@ function resolveFeatureRefForApproval(state, featureName) {
4939
5467
  function buildApprovalCommand(state, featureName, selectedComponent, execute) {
4940
5468
  const featureRef = resolveFeatureRefForApproval(state, featureName);
4941
5469
  const componentArg = selectedComponent ? ` --component ${selectedComponent}` : "";
4942
- const executeArg = execute ? " --execute" : "";
4943
- return `npx lee-spec-kit context ${featureRef}${componentArg} --approve <LABEL>${executeArg}`;
5470
+ if (execute) {
5471
+ return `npx lee-spec-kit context ${featureRef}${componentArg} --approve <LABEL> --execute [--ticket <TICKET>]`;
5472
+ }
5473
+ return `npx lee-spec-kit context ${featureRef}${componentArg} --approve <LABEL>`;
4944
5474
  }
4945
5475
  function buildFinalApprovalPrompt(lang, actionOptions) {
4946
5476
  if (actionOptions.length === 0) return "";
@@ -4991,7 +5521,16 @@ function getCommandExecutionLockPath(action, config) {
4991
5521
  return getProjectExecutionLockPath(action.cwd);
4992
5522
  }
4993
5523
  function contextCommand(program2) {
4994
- program2.command("context [feature-name]").description("Show current feature context and next actions").option("--json", "Output in JSON format for agents").option("--repo <repo>", "Component name for multi projects").option("--component <component>", "Component name for multi projects").option("--all", "Include completed features when auto-detecting").option("--done", "Show completed (workflow-done) features only").option("--approve <reply>", "Approve one labeled option: A or A OK").option("--execute", "Execute approved option when it is a command").option(
5524
+ program2.command("context [feature-name]").description("Show current feature context and next actions").option("--json", "Output in JSON format for agents").option("--component <component>", "Component name for multi projects").option("--all", "Include completed features when auto-detecting").option("--done", "Show completed (workflow-done) features only").option(
5525
+ "--approve <reply>",
5526
+ "Approve one labeled option (examples: A, A OK, A proceed, A \uC9C4\uD589\uD574)"
5527
+ ).option(
5528
+ "--ticket <token>",
5529
+ "Approval ticket issued by `--approve` (required only when selected option requires user check)"
5530
+ ).option(
5531
+ "--execute",
5532
+ "Execute approved option when it is a command (ticket required only for check-required options)"
5533
+ ).option(
4995
5534
  "--execute-strict",
4996
5535
  "Fail when approved option is instruction-only (use with --execute)"
4997
5536
  ).action(
@@ -5079,6 +5618,7 @@ async function runContext(featureName, options) {
5079
5618
  const lang = config?.lang ?? "en";
5080
5619
  const workflowPolicy = resolveWorkflowPolicy(config?.workflow);
5081
5620
  const prePrReviewPolicy = resolvePrePrReviewPolicy(config?.workflow);
5621
+ const taskCommitGatePolicy = resolveTaskCommitGatePolicy(config?.workflow);
5082
5622
  if (!config) {
5083
5623
  throw createCliError(
5084
5624
  "CONFIG_NOT_FOUND",
@@ -5088,22 +5628,22 @@ async function runContext(featureName, options) {
5088
5628
  if (options.execute && !options.approve) {
5089
5629
  throw createCliError(
5090
5630
  "APPROVAL_REQUIRED",
5091
- "`--execute` requires `--approve <label>`."
5631
+ "`--execute` requires `--approve <reply>`."
5092
5632
  );
5093
5633
  }
5094
- if (options.executeStrict && !options.execute) {
5634
+ if (!options.execute && options.ticket) {
5095
5635
  throw createCliError(
5096
5636
  "INVALID_ARGUMENT",
5097
- "`--execute-strict` requires `--execute`."
5637
+ "`--ticket` is only valid with `--execute`."
5098
5638
  );
5099
5639
  }
5100
- if (options.repo && options.component && options.repo.trim().toLowerCase() !== options.component.trim().toLowerCase()) {
5640
+ if (options.executeStrict && !options.execute) {
5101
5641
  throw createCliError(
5102
5642
  "INVALID_ARGUMENT",
5103
- "`--repo` and `--component` must reference the same value when both are provided."
5643
+ "`--execute-strict` requires `--execute`."
5104
5644
  );
5105
5645
  }
5106
- const selectedComponent = (options.component || options.repo || "").trim().toLowerCase();
5646
+ const selectedComponent = (options.component || "").trim().toLowerCase();
5107
5647
  const stepDefinitions = getStepDefinitions(lang, config.workflow);
5108
5648
  const stepsMap = getStepsMap(lang, config.workflow);
5109
5649
  const selectionOptions = {
@@ -5113,7 +5653,7 @@ async function runContext(featureName, options) {
5113
5653
  };
5114
5654
  const state = await resolveContextState(config, featureName, selectionOptions);
5115
5655
  const requiredDocs = buildRequiredDocHints(state.actionOptions);
5116
- if (options.approve) {
5656
+ if (options.approve || options.execute) {
5117
5657
  await runApprovedOption(
5118
5658
  state,
5119
5659
  config,
@@ -5160,34 +5700,47 @@ async function runContext(featureName, options) {
5160
5700
  primaryActionCategory: primaryAction?.action.category ?? null,
5161
5701
  primaryActionOperationType: primaryAction?.action.operationType ?? null,
5162
5702
  workflowPolicy,
5703
+ taskCommitGatePolicy,
5163
5704
  prePrReviewPolicy,
5164
5705
  checkPolicy: {
5165
5706
  docPath: "builtin://agents/policy",
5166
5707
  hint: tr(lang, "cli", "context.checkPolicyHint"),
5167
5708
  policyOnly: true,
5168
5709
  token: "<LABEL>",
5169
- acceptedTokens: ["<LABEL>", "<LABEL> OK"],
5170
- tokenPattern: "^([A-Z]+)(?:\\s+OK)?$",
5710
+ acceptedTokens: ["<LABEL>", "<LABEL> OK", "<LABEL> ...", "... <LABEL> ..."],
5711
+ tokenPattern: "^.*\\b([A-Z]+)\\b.*$",
5171
5712
  validLabels: state.actionOptions.map((o) => o.label),
5172
5713
  requireExplanationBeforeApproval: true,
5173
- requiredExplanationFields: ["actionOptions[].summary", "actionOptions[].approvalPrompt"],
5174
- recommendation: "Before asking for approval, explain each label with summary and then ask for `<LABEL>` or `<LABEL> OK`.",
5714
+ requiredExplanationFields: [
5715
+ "actionOptions[].label",
5716
+ "actionOptions[].detail",
5717
+ "actionOptions[].approvalPrompt"
5718
+ ],
5719
+ recommendation: "Before asking for approval, present each label with exact CLI detail first (`A: <detail>`). Do not paraphrase command options. User replies should include the label token (e.g. `A`, `A OK`, `A proceed`, `A \uC9C4\uD589\uD574`). For command execution, require one-time `approvalTicket` only when the selected action has `requiresUserCheck=true`.",
5175
5720
  oneApprovalPerAction: true,
5176
5721
  requireFreshContext: true,
5177
5722
  contextVersion: state.contextVersion,
5178
5723
  config: config.approval ?? { mode: "builtin" }
5179
5724
  },
5180
5725
  approvalRequest: {
5181
- guidance: "Present each label with summary (e.g. `A: <summary>`) before asking for approval.",
5726
+ guidance: "Present each label with exact CLI detail (e.g. `A: <detail>`). For command options, include the raw `cmd` unchanged, then ask for `<LABEL>` or `<LABEL> OK`.",
5182
5727
  finalPrompt: finalApprovalPrompt,
5183
5728
  labels: state.actionOptions.map((o) => o.label),
5184
5729
  approveCommand,
5185
5730
  executeCommand,
5731
+ executeRequiresTicket: !!state.actionOptions[0]?.action?.requiresUserCheck,
5186
5732
  options: state.actionOptions.map((o) => ({
5187
5733
  label: o.label,
5188
5734
  summary: o.summary,
5735
+ detail: o.detail,
5189
5736
  approvalPrompt: o.approvalPrompt,
5737
+ actionType: o.action.type,
5738
+ scope: o.action.type === "command" ? o.action.scope : void 0,
5739
+ cwd: o.action.type === "command" ? o.action.cwd : void 0,
5740
+ cmd: o.action.type === "command" ? o.action.cmd : void 0,
5741
+ message: o.action.type === "instruction" ? o.action.message : void 0,
5190
5742
  requiresUserCheck: !!o.action.requiresUserCheck,
5743
+ executeRequiresTicket: !!o.action.requiresUserCheck,
5191
5744
  operationType: o.action.operationType
5192
5745
  }))
5193
5746
  },
@@ -5448,6 +6001,12 @@ async function runContext(featureName, options) {
5448
6001
  }
5449
6002
  if (actionOptions.length > 0) {
5450
6003
  const finalApprovalPrompt = buildFinalApprovalPrompt(lang, actionOptions);
6004
+ const approveCommand = buildApprovalCommand(
6005
+ state,
6006
+ featureName,
6007
+ selectedComponent,
6008
+ false
6009
+ );
5451
6010
  const executeCommand = buildApprovalCommand(
5452
6011
  state,
5453
6012
  featureName,
@@ -5458,6 +6017,13 @@ async function runContext(featureName, options) {
5458
6017
  console.log(
5459
6018
  chalk6.gray(
5460
6019
  ` \u21B3 ${tr(lang, "cli", "context.finalLabelCommandHint", {
6020
+ command: approveCommand
6021
+ })}`
6022
+ )
6023
+ );
6024
+ console.log(
6025
+ chalk6.gray(
6026
+ ` \u21B3 ${tr(lang, "cli", "context.finalTicketCommandHint", {
5461
6027
  command: executeCommand
5462
6028
  })}`
5463
6029
  )
@@ -5467,13 +6033,8 @@ async function runContext(featureName, options) {
5467
6033
  }
5468
6034
  async function runApprovedOption(state, config, lang, featureName, selectionOptions, options) {
5469
6035
  const approval = options.approve || "";
5470
- const parsedLabel = parseApprovalLabel(approval);
5471
- if (!parsedLabel) {
5472
- throw createCliError(
5473
- "INVALID_APPROVAL",
5474
- "Invalid approval reply. Use `<label>` or `<label> OK` (e.g. `A`, `A OK`)."
5475
- );
5476
- }
6036
+ const ticketToken = (options.ticket || "").trim();
6037
+ let parsedLabel = null;
5477
6038
  if (state.status !== "single_matched" || !state.matchedFeature) {
5478
6039
  throw createCliError(
5479
6040
  "CONTEXT_SELECTION_REQUIRED",
@@ -5483,6 +6044,16 @@ async function runApprovedOption(state, config, lang, featureName, selectionOpti
5483
6044
  if (state.actionOptions.length === 0) {
5484
6045
  throw createCliError("NO_ACTION_OPTIONS", "No action options to approve.");
5485
6046
  }
6047
+ parsedLabel = parseApprovalLabel(
6048
+ approval,
6049
+ state.actionOptions.map((o) => o.label)
6050
+ );
6051
+ if (!parsedLabel) {
6052
+ throw createCliError(
6053
+ "INVALID_APPROVAL",
6054
+ "Invalid approval reply. Include a valid label token (e.g. `A`, `A OK`, `A proceed`, `A \uC9C4\uD589\uD574`)."
6055
+ );
6056
+ }
5486
6057
  const selected = state.actionOptions.find((o) => o.label === parsedLabel);
5487
6058
  if (!selected) {
5488
6059
  throw createCliError(
@@ -5506,8 +6077,23 @@ async function runApprovedOption(state, config, lang, featureName, selectionOpti
5506
6077
  `Approved label "${parsedLabel}" is no longer available. Run \`context\` again.`
5507
6078
  );
5508
6079
  }
6080
+ if (!freshState.matchedFeature || !freshState.contextVersion) {
6081
+ throw createCliError(
6082
+ "CONTEXT_STALE",
6083
+ "Context changed since approval was requested. Run `context` again and re-approve."
6084
+ );
6085
+ }
5509
6086
  const selectedAction = freshSelected.action;
6087
+ const executeRequiresTicket = !!selectedAction.requiresUserCheck;
6088
+ const actionHash = toActionHash(freshSelected);
6089
+ const featureRef = freshState.matchedFeature.folderName;
5510
6090
  if (!options.execute) {
6091
+ const ticket = executeRequiresTicket ? await issueApprovalTicket(config, {
6092
+ contextVersion: freshState.contextVersion,
6093
+ actionHash,
6094
+ label: parsedLabel,
6095
+ featureRef
6096
+ }) : null;
5511
6097
  if (options.json) {
5512
6098
  console.log(
5513
6099
  JSON.stringify(
@@ -5519,7 +6105,17 @@ async function runApprovedOption(state, config, lang, featureName, selectionOpti
5519
6105
  action: selectedAction,
5520
6106
  contextVersion: freshState.contextVersion,
5521
6107
  executable: selectedAction.type === "command",
5522
- oneApprovalPerAction: true
6108
+ executeRequiresTicket,
6109
+ oneApprovalPerAction: true,
6110
+ approvalTicket: ticket ? {
6111
+ token: ticket.token,
6112
+ sessionId: ticket.sessionId,
6113
+ label: ticket.label,
6114
+ contextVersion: ticket.contextVersion,
6115
+ actionHash: ticket.actionHash,
6116
+ expiresAt: ticket.expiresAt,
6117
+ oneTime: true
6118
+ } : void 0
5523
6119
  },
5524
6120
  null,
5525
6121
  2
@@ -5531,13 +6127,45 @@ async function runApprovedOption(state, config, lang, featureName, selectionOpti
5531
6127
  console.log(chalk6.green(`\u2705 Approved option: ${parsedLabel}`));
5532
6128
  console.log(chalk6.gray(` - Action: ${formatActionSummary2(selectedAction)}`));
5533
6129
  if (selectedAction.type === "command") {
5534
- console.log(chalk6.gray(" - Run with: --execute"));
6130
+ const selectedComponent = selectionOptions.component || "";
6131
+ let executeCommand = buildApprovalCommand(
6132
+ freshState,
6133
+ featureName,
6134
+ selectedComponent,
6135
+ true
6136
+ ).replace("<LABEL>", parsedLabel);
6137
+ if (ticket) {
6138
+ executeCommand = executeCommand.replace(
6139
+ "[--ticket <TICKET>]",
6140
+ `--ticket ${ticket.token}`
6141
+ );
6142
+ console.log(chalk6.gray(` - Ticket: ${ticket.token} (expires: ${ticket.expiresAt})`));
6143
+ } else {
6144
+ executeCommand = executeCommand.replace(" [--ticket <TICKET>]", "");
6145
+ }
6146
+ console.log(chalk6.gray(` - Run with: ${executeCommand}`));
5535
6147
  } else {
5536
6148
  console.log(chalk6.gray(" - Instruction-only action (no command execution)."));
5537
6149
  }
5538
6150
  console.log();
5539
6151
  return;
5540
6152
  }
6153
+ if (!ticketToken) {
6154
+ if (executeRequiresTicket) {
6155
+ throw createCliError(
6156
+ "APPROVAL_REQUIRED",
6157
+ "`--execute` requires `--ticket <token>` for check-required options. Run `context --approve <reply>` first."
6158
+ );
6159
+ }
6160
+ }
6161
+ if (executeRequiresTicket) {
6162
+ await consumeApprovalTicket(config, ticketToken, {
6163
+ contextVersion: freshState.contextVersion,
6164
+ actionHash,
6165
+ label: parsedLabel,
6166
+ featureRef
6167
+ });
6168
+ }
5541
6169
  if (selectedAction.type !== "command") {
5542
6170
  if (options.executeStrict) {
5543
6171
  throw createCliError(
@@ -5790,8 +6418,8 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
5790
6418
  ];
5791
6419
  for (const file of files) {
5792
6420
  const fullPath = path18.join(f.path, file);
5793
- if (!await fs14.pathExists(fullPath)) continue;
5794
- const original = await fs14.readFile(fullPath, "utf-8");
6421
+ if (!await fs15.pathExists(fullPath)) continue;
6422
+ const original = await fs15.readFile(fullPath, "utf-8");
5795
6423
  let next = original;
5796
6424
  const changes = [];
5797
6425
  const placeholderFix = applyPlaceholderFixes(next, placeholderContext, config.lang);
@@ -5815,7 +6443,7 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
5815
6443
  }
5816
6444
  if (next === original) continue;
5817
6445
  if (!dryRun) {
5818
- await fs14.writeFile(fullPath, next, "utf-8");
6446
+ await fs15.writeFile(fullPath, next, "utf-8");
5819
6447
  }
5820
6448
  entries.push({
5821
6449
  path: formatPath(cwd, fullPath),
@@ -5835,7 +6463,7 @@ async function checkDocsStructure(config, cwd) {
5835
6463
  const requiredDirs = ["agents", "features", "prd", "designs", "ideas"];
5836
6464
  for (const dir of requiredDirs) {
5837
6465
  const p = path18.join(config.docsDir, dir);
5838
- if (!await fs14.pathExists(p)) {
6466
+ if (!await fs15.pathExists(p)) {
5839
6467
  issues.push({
5840
6468
  level: "error",
5841
6469
  code: "missing_dir",
@@ -5845,7 +6473,7 @@ async function checkDocsStructure(config, cwd) {
5845
6473
  }
5846
6474
  }
5847
6475
  const configPath = path18.join(config.docsDir, ".lee-spec-kit.json");
5848
- if (!await fs14.pathExists(configPath)) {
6476
+ if (!await fs15.pathExists(configPath)) {
5849
6477
  issues.push({
5850
6478
  level: "warn",
5851
6479
  code: "missing_config",
@@ -5876,8 +6504,8 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
5876
6504
  const featureDocs = ["spec.md", "plan.md", "tasks.md"];
5877
6505
  for (const file of featureDocs) {
5878
6506
  const p = path18.join(f.path, file);
5879
- if (!await fs14.pathExists(p)) continue;
5880
- const content = await fs14.readFile(p, "utf-8");
6507
+ if (!await fs15.pathExists(p)) continue;
6508
+ const content = await fs15.readFile(p, "utf-8");
5881
6509
  const placeholders = detectPlaceholders(content);
5882
6510
  if (placeholders.length === 0) continue;
5883
6511
  issues.push({
@@ -5891,8 +6519,8 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
5891
6519
  }
5892
6520
  if (decisionsPlaceholderMode !== "off") {
5893
6521
  const decisionsPath = path18.join(f.path, "decisions.md");
5894
- if (await fs14.pathExists(decisionsPath)) {
5895
- const content = await fs14.readFile(decisionsPath, "utf-8");
6522
+ if (await fs15.pathExists(decisionsPath)) {
6523
+ const content = await fs15.readFile(decisionsPath, "utf-8");
5896
6524
  const placeholders = detectPlaceholders(content);
5897
6525
  if (placeholders.length > 0) {
5898
6526
  issues.push({
@@ -6198,17 +6826,11 @@ function doctorCommand(program2) {
6198
6826
  });
6199
6827
  }
6200
6828
  function resolveComponentOption2(options) {
6201
- if (options.repo && options.component && options.repo.trim().toLowerCase() !== options.component.trim().toLowerCase()) {
6202
- throw createCliError(
6203
- "INVALID_ARGUMENT",
6204
- "`--repo` and `--component` must reference the same value when both are provided."
6205
- );
6206
- }
6207
- const component = (options.component || options.repo || "").trim().toLowerCase();
6829
+ const component = (options.component || "").trim().toLowerCase();
6208
6830
  return component || void 0;
6209
6831
  }
6210
6832
  function viewCommand(program2) {
6211
- program2.command("view [feature-name]").description("Show workflow dashboard for features").option("--json", "Output in JSON format for agents").option("--repo <repo>", "Component name for multi projects").option("--component <component>", "Component name for multi projects").option("--all", "Include completed features when auto-detecting").option("--done", "Show completed (workflow-done) features only").action(async (featureName, options) => {
6833
+ program2.command("view [feature-name]").description("Show workflow dashboard for features").option("--json", "Output in JSON format for agents").option("--component <component>", "Component name for multi projects").option("--all", "Include completed features when auto-detecting").option("--done", "Show completed (workflow-done) features only").action(async (featureName, options) => {
6212
6834
  try {
6213
6835
  await runView(featureName, options);
6214
6836
  } catch (error) {
@@ -6348,13 +6970,7 @@ async function runView(featureName, options) {
6348
6970
  console.log();
6349
6971
  }
6350
6972
  function resolveComponentOption3(options) {
6351
- if (options.repo && options.component && options.repo.trim().toLowerCase() !== options.component.trim().toLowerCase()) {
6352
- throw createCliError(
6353
- "INVALID_ARGUMENT",
6354
- "`--repo` and `--component` must reference the same value when both are provided."
6355
- );
6356
- }
6357
- const component = (options.component || options.repo || "").trim().toLowerCase();
6973
+ const component = (options.component || "").trim().toLowerCase();
6358
6974
  return component || void 0;
6359
6975
  }
6360
6976
  function runSelfCli(args) {
@@ -6397,14 +7013,17 @@ function runSelfCliJson(args, allowFailure = false) {
6397
7013
  function buildSelectionArgs(featureName, options) {
6398
7014
  const args = [];
6399
7015
  if (featureName) args.push(featureName);
6400
- const component = (options.component || options.repo || "").trim();
7016
+ const component = (options.component || "").trim();
6401
7017
  if (component) args.push("--component", component);
6402
7018
  if (options.all) args.push("--all");
6403
7019
  if (options.done) args.push("--done");
6404
7020
  return args;
6405
7021
  }
6406
7022
  function flowCommand(program2) {
6407
- program2.command("flow [feature-name]").description("Run combined workflow checks (context + status + doctor)").option("--json", "Output in JSON format for agents").option("--repo <repo>", "Component name for multi projects").option("--component <component>", "Component name for multi projects").option("--all", "Include completed features when auto-detecting").option("--done", "Show completed (workflow-done) features only").option("--approve <reply>", "Approve one labeled context option").option("--execute", "Execute approved option when it is a command").option(
7023
+ program2.command("flow [feature-name]").description("Run combined workflow checks (context + status + doctor)").option("--json", "Output in JSON format for agents").option("--component <component>", "Component name for multi projects").option("--all", "Include completed features when auto-detecting").option("--done", "Show completed (workflow-done) features only").option(
7024
+ "--approve <reply>",
7025
+ "Approve one labeled context option (examples: A, A OK, A proceed, A \uC9C4\uD589\uD574)"
7026
+ ).option("--execute", "Execute approved option when it is a command").option(
6408
7027
  "--execute-strict",
6409
7028
  "With --execute, fail if approved option is instruction-only"
6410
7029
  ).option("--strict", "Also run status --strict and doctor --strict").action(async (featureName, options) => {
@@ -6449,6 +7068,12 @@ async function runFlow(featureName, options) {
6449
7068
  "`--execute-strict` requires `--execute`."
6450
7069
  );
6451
7070
  }
7071
+ if (options.execute && !options.approve) {
7072
+ throw createCliError(
7073
+ "APPROVAL_REQUIRED",
7074
+ "`--execute` requires `--approve <reply>`."
7075
+ );
7076
+ }
6452
7077
  const selectedComponent = resolveComponentOption3(options);
6453
7078
  const selectionOptions = {
6454
7079
  component: selectedComponent,
@@ -6461,9 +7086,29 @@ async function runFlow(featureName, options) {
6461
7086
  let approvalResult = null;
6462
7087
  if (options.approve) {
6463
7088
  const approveArgs = [...contextArgs, "--approve", options.approve];
6464
- if (options.execute) approveArgs.push("--execute");
6465
- if (options.executeStrict) approveArgs.push("--execute-strict");
6466
- approvalResult = runSelfCliJson(approveArgs, true);
7089
+ const selected = runSelfCliJson(approveArgs, true);
7090
+ if (options.execute) {
7091
+ const selectedPayload = selected;
7092
+ const executeRequiresTicket = selectedPayload?.executeRequiresTicket === true;
7093
+ const ticket = selectedPayload?.approvalTicket?.token;
7094
+ if (selectedPayload?.status === "approved_selected") {
7095
+ const executeArgs = [
7096
+ ...contextArgs,
7097
+ "--approve",
7098
+ options.approve,
7099
+ "--execute"
7100
+ ];
7101
+ if (executeRequiresTicket && ticket) {
7102
+ executeArgs.push("--ticket", ticket);
7103
+ }
7104
+ if (options.executeStrict) executeArgs.push("--execute-strict");
7105
+ approvalResult = runSelfCliJson(executeArgs, true);
7106
+ } else {
7107
+ approvalResult = selected;
7108
+ }
7109
+ } else {
7110
+ approvalResult = selected;
7111
+ }
6467
7112
  }
6468
7113
  const after = await resolveContextSelection(config, featureName, selectionOptions);
6469
7114
  const statusReport = runSelfCliJson(["status"]);
@@ -6645,21 +7290,21 @@ function detectGithubCliLangSync(cwd) {
6645
7290
  for (const base of scanOrder) {
6646
7291
  for (const docsDir of [path18.join(base, "docs"), base]) {
6647
7292
  const configPath = path18.join(docsDir, ".lee-spec-kit.json");
6648
- if (fs14.existsSync(configPath)) {
7293
+ if (fs15.existsSync(configPath)) {
6649
7294
  try {
6650
- const parsed = fs14.readJsonSync(configPath);
7295
+ const parsed = fs15.readJsonSync(configPath);
6651
7296
  if (parsed?.lang === "ko" || parsed?.lang === "en") return parsed.lang;
6652
7297
  } catch {
6653
7298
  }
6654
7299
  }
6655
7300
  const agentsPath = path18.join(docsDir, "agents");
6656
7301
  const featuresPath = path18.join(docsDir, "features");
6657
- if (!fs14.existsSync(agentsPath) || !fs14.existsSync(featuresPath)) continue;
7302
+ if (!fs15.existsSync(agentsPath) || !fs15.existsSync(featuresPath)) continue;
6658
7303
  for (const probe of ["custom.md", "constitution.md", "agents.md"]) {
6659
7304
  const file = path18.join(agentsPath, probe);
6660
- if (!fs14.existsSync(file)) continue;
7305
+ if (!fs15.existsSync(file)) continue;
6661
7306
  try {
6662
- const content = fs14.readFileSync(file, "utf-8");
7307
+ const content = fs15.readFileSync(file, "utf-8");
6663
7308
  if (/[가-힣]/.test(content)) return "ko";
6664
7309
  } catch {
6665
7310
  }
@@ -6669,14 +7314,8 @@ function detectGithubCliLangSync(cwd) {
6669
7314
  }
6670
7315
  return DEFAULT_LANG;
6671
7316
  }
6672
- function resolveComponentOption4(options, lang) {
6673
- if (options.repo && options.component && options.repo.trim().toLowerCase() !== options.component.trim().toLowerCase()) {
6674
- throw createCliError(
6675
- "INVALID_ARGUMENT",
6676
- tg(lang, "invalidRepoComponentMismatch")
6677
- );
6678
- }
6679
- const component = (options.component || options.repo || "").trim().toLowerCase();
7317
+ function resolveComponentOption4(options) {
7318
+ const component = (options.component || "").trim().toLowerCase();
6680
7319
  return component || void 0;
6681
7320
  }
6682
7321
  function parseLabels(raw, lang) {
@@ -6760,7 +7399,7 @@ function ensureSections(body, sections, kind, lang) {
6760
7399
  }
6761
7400
  function ensureDocsExist(docsDir, relativePaths, lang) {
6762
7401
  const missing = relativePaths.filter(
6763
- (relativePath) => !fs14.existsSync(path18.join(docsDir, relativePath))
7402
+ (relativePath) => !fs15.existsSync(path18.join(docsDir, relativePath))
6764
7403
  );
6765
7404
  if (missing.length > 0) {
6766
7405
  throw createCliError(
@@ -6853,6 +7492,22 @@ async function resolveFeatureOrThrow(featureName, options, lang) {
6853
7492
  }
6854
7493
  return { config, feature: state.matchedFeature };
6855
7494
  }
7495
+ function resolveGithubProjectCwd(config, feature) {
7496
+ const projectGitCwd = (feature.git.projectGitCwd || "").trim();
7497
+ if (projectGitCwd) return projectGitCwd;
7498
+ if (config.docsRepo === "standalone") {
7499
+ throw createCliError(
7500
+ "PRECONDITION_FAILED",
7501
+ tr(config.lang, "messages", "standaloneNeedsProjectRoot")
7502
+ );
7503
+ }
7504
+ return process.cwd();
7505
+ }
7506
+ function resolveGithubDocsCwd(config, feature) {
7507
+ const docsGitCwd = (feature.git.docsGitCwd || "").trim();
7508
+ if (docsGitCwd) return docsGitCwd;
7509
+ return config.docsDir;
7510
+ }
6856
7511
  function getFeatureDocPaths(feature) {
6857
7512
  const featurePathFromDocs = feature.docs.featurePathFromDocs;
6858
7513
  return {
@@ -6935,7 +7590,7 @@ function uniqItems(items) {
6935
7590
  return ordered;
6936
7591
  }
6937
7592
  function normalizeSemanticKey(value) {
6938
- return value.toLowerCase().replace(/[`'"(){}\[\].,:;!?/\\\-_|]/g, " ").replace(/\s+/g, " ").trim().replace(/\s/g, "");
7593
+ return value.toLowerCase().replace(/[`\]\u005B'"(){}.,:;!?/\\_|-]/g, " ").replace(/\s+/g, " ").trim().replace(/\s/g, "");
6939
7594
  }
6940
7595
  function uniqItemsByContainment(items) {
6941
7596
  const kept = [];
@@ -7503,10 +8158,10 @@ function insertFieldInGithubIssueSection(content, key, value) {
7503
8158
  return { content: lines.join("\n"), changed: true };
7504
8159
  }
7505
8160
  function syncTasksPrMetadata(tasksPath, prUrl, nextStatus, lang) {
7506
- if (!fs14.existsSync(tasksPath)) {
8161
+ if (!fs15.existsSync(tasksPath)) {
7507
8162
  throw createCliError("DOCS_NOT_FOUND", tg(lang, "tasksNotFound", { path: tasksPath }));
7508
8163
  }
7509
- const original = fs14.readFileSync(tasksPath, "utf-8");
8164
+ const original = fs15.readFileSync(tasksPath, "utf-8");
7510
8165
  let next = original;
7511
8166
  let changed = false;
7512
8167
  const prReplaced = replaceListField(next, ["PR", "Pull Request"], prUrl);
@@ -7530,7 +8185,7 @@ function syncTasksPrMetadata(tasksPath, prUrl, nextStatus, lang) {
7530
8185
  changed = changed || inserted.changed;
7531
8186
  }
7532
8187
  if (changed) {
7533
- fs14.writeFileSync(tasksPath, next, "utf-8");
8188
+ fs15.writeFileSync(tasksPath, next, "utf-8");
7534
8189
  }
7535
8190
  return { changed, path: tasksPath };
7536
8191
  }
@@ -7674,12 +8329,12 @@ function toRetryCount(raw, lang) {
7674
8329
  function githubCommand(program2) {
7675
8330
  const commandLang = detectGithubCliLangSync(process.cwd());
7676
8331
  const github = program2.command("github").description(tg(commandLang, "cmdGithubDescription"));
7677
- github.command("issue [feature-name]").description(tg(commandLang, "cmdIssueDescription")).option("--json", tg(commandLang, "optJson")).option("--repo <repo>", tg(commandLang, "optRepo")).option("--component <component>", tg(commandLang, "optComponent")).option("--title <title>", tg(commandLang, "optIssueTitle")).option("--labels <labels>", tg(commandLang, "optLabels")).option("--body-file <path>", tg(commandLang, "optIssueBodyFile")).option("--assignee <assignee>", tg(commandLang, "optIssueAssignee")).option("--create", tg(commandLang, "optIssueCreate")).option(
8332
+ github.command("issue [feature-name]").description(tg(commandLang, "cmdIssueDescription")).option("--json", tg(commandLang, "optJson")).option("--component <component>", tg(commandLang, "optComponent")).option("--title <title>", tg(commandLang, "optIssueTitle")).option("--labels <labels>", tg(commandLang, "optLabels")).option("--body-file <path>", tg(commandLang, "optIssueBodyFile")).option("--assignee <assignee>", tg(commandLang, "optIssueAssignee")).option("--create", tg(commandLang, "optIssueCreate")).option(
7678
8333
  "--confirm <reply>",
7679
8334
  tg(commandLang, "optIssueConfirm")
7680
8335
  ).action(async (featureName, options) => {
7681
8336
  try {
7682
- const selectedComponent = resolveComponentOption4(options, commandLang);
8337
+ const selectedComponent = resolveComponentOption4(options);
7683
8338
  const { config, feature } = await resolveFeatureOrThrow(featureName, {
7684
8339
  component: selectedComponent
7685
8340
  }, commandLang);
@@ -7690,9 +8345,9 @@ function githubCommand(program2) {
7690
8345
  [paths.specPath, paths.planPath, paths.tasksPath],
7691
8346
  config.lang
7692
8347
  );
7693
- const specContent = await fs14.readFile(path18.join(config.docsDir, paths.specPath), "utf-8");
7694
- const planContent = await fs14.readFile(path18.join(config.docsDir, paths.planPath), "utf-8");
7695
- const tasksContent = await fs14.readFile(path18.join(config.docsDir, paths.tasksPath), "utf-8");
8348
+ const specContent = await fs15.readFile(path18.join(config.docsDir, paths.specPath), "utf-8");
8349
+ const planContent = await fs15.readFile(path18.join(config.docsDir, paths.planPath), "utf-8");
8350
+ const tasksContent = await fs15.readFile(path18.join(config.docsDir, paths.tasksPath), "utf-8");
7696
8351
  const overview = resolveOverviewFromSpec(specContent, feature, config.lang);
7697
8352
  const title = options.title?.trim() || tg(config.lang, "issueDefaultTitle", {
7698
8353
  slug: feature.slug,
@@ -7721,8 +8376,8 @@ function githubCommand(program2) {
7721
8376
  );
7722
8377
  const explicitBodyFile = (options.bodyFile || "").trim();
7723
8378
  let body = generatedBody;
7724
- if (options.create && explicitBodyFile && await fs14.pathExists(bodyFile)) {
7725
- body = await fs14.readFile(bodyFile, "utf-8");
8379
+ if (options.create && explicitBodyFile && await fs15.pathExists(bodyFile)) {
8380
+ body = await fs15.readFile(bodyFile, "utf-8");
7726
8381
  ensureSections(
7727
8382
  body,
7728
8383
  getRequiredIssueSections(config.lang),
@@ -7730,11 +8385,12 @@ function githubCommand(program2) {
7730
8385
  config.lang
7731
8386
  );
7732
8387
  } else {
7733
- await fs14.ensureDir(path18.dirname(bodyFile));
7734
- await fs14.writeFile(bodyFile, generatedBody, "utf-8");
8388
+ await fs15.ensureDir(path18.dirname(bodyFile));
8389
+ await fs15.writeFile(bodyFile, generatedBody, "utf-8");
7735
8390
  }
7736
8391
  let issueUrl;
7737
8392
  if (options.create) {
8393
+ const projectGitCwd = resolveGithubProjectCwd(config, feature);
7738
8394
  ensureNoTodoPlaceholders(body, tg(config.lang, "kindIssue"), config.lang);
7739
8395
  assertRemoteApproval(
7740
8396
  options.confirm,
@@ -7757,7 +8413,7 @@ function githubCommand(program2) {
7757
8413
  const created = runProcessOrThrow(
7758
8414
  "gh",
7759
8415
  args,
7760
- process.cwd(),
8416
+ projectGitCwd,
7761
8417
  tg(config.lang, "createIssueFailed")
7762
8418
  );
7763
8419
  issueUrl = created.stdout.trim() || void 0;
@@ -7816,22 +8472,22 @@ function githubCommand(program2) {
7816
8472
  process.exit(1);
7817
8473
  }
7818
8474
  });
7819
- github.command("pr [feature-name]").description(tg(commandLang, "cmdPrDescription")).option("--json", tg(commandLang, "optJson")).option("--repo <repo>", tg(commandLang, "optRepo")).option("--component <component>", tg(commandLang, "optComponent")).option("--title <title>", tg(commandLang, "optPrTitle")).option("--labels <labels>", tg(commandLang, "optLabels")).option("--body-file <path>", tg(commandLang, "optPrBodyFile")).option("--assignee <assignee>", tg(commandLang, "optPrAssignee")).option("--base <branch>", tg(commandLang, "optPrBase"), "main").option("--create", tg(commandLang, "optPrCreate")).option("--pr <ref>", tg(commandLang, "optPrRef")).option("--merge", tg(commandLang, "optPrMerge")).option(
8475
+ github.command("pr [feature-name]").description(tg(commandLang, "cmdPrDescription")).option("--json", tg(commandLang, "optJson")).option("--component <component>", tg(commandLang, "optComponent")).option("--title <title>", tg(commandLang, "optPrTitle")).option("--labels <labels>", tg(commandLang, "optLabels")).option("--body-file <path>", tg(commandLang, "optPrBodyFile")).option("--assignee <assignee>", tg(commandLang, "optPrAssignee")).option("--base <branch>", tg(commandLang, "optPrBase"), "main").option("--create", tg(commandLang, "optPrCreate")).option("--pr <ref>", tg(commandLang, "optPrRef")).option("--merge", tg(commandLang, "optPrMerge")).option(
7820
8476
  "--confirm <reply>",
7821
8477
  tg(commandLang, "optPrConfirm")
7822
8478
  ).option("--retry <count>", tg(commandLang, "optPrRetry")).option("--screenshots <mode>", tg(commandLang, "optPrScreenshots"), "auto").option("--mermaid <mode>", tg(commandLang, "optPrMermaid"), "auto").option("--no-sync-tasks", tg(commandLang, "optPrNoSyncTasks")).option("--commit-sync", tg(commandLang, "optPrCommitSync")).action(async (featureName, options) => {
7823
8479
  try {
7824
- const selectedComponent = resolveComponentOption4(options, commandLang);
8480
+ const selectedComponent = resolveComponentOption4(options);
7825
8481
  const { config, feature } = await resolveFeatureOrThrow(featureName, {
7826
8482
  component: selectedComponent
7827
8483
  }, commandLang);
7828
8484
  const labels = parseLabels(options.labels, config.lang);
7829
8485
  const paths = getFeatureDocPaths(feature);
7830
8486
  ensureDocsExist(config.docsDir, [paths.specPath, paths.tasksPath], config.lang);
7831
- const specContent = await fs14.readFile(path18.join(config.docsDir, paths.specPath), "utf-8");
8487
+ const specContent = await fs15.readFile(path18.join(config.docsDir, paths.specPath), "utf-8");
7832
8488
  const planPath = path18.join(config.docsDir, paths.planPath);
7833
- const planContent = await fs14.pathExists(planPath) ? await fs14.readFile(planPath, "utf-8") : "";
7834
- const tasksContent = await fs14.readFile(path18.join(config.docsDir, paths.tasksPath), "utf-8");
8489
+ const planContent = await fs15.pathExists(planPath) ? await fs15.readFile(planPath, "utf-8") : "";
8490
+ const tasksContent = await fs15.readFile(path18.join(config.docsDir, paths.tasksPath), "utf-8");
7835
8491
  const overview = resolveOverviewFromSpec(specContent, feature, config.lang);
7836
8492
  const defaultTitle = feature.issueNumber ? tg(config.lang, "prDefaultTitleWithIssue", {
7837
8493
  issue: feature.issueNumber,
@@ -7865,8 +8521,8 @@ function githubCommand(program2) {
7865
8521
  );
7866
8522
  const explicitBodyFile = (options.bodyFile || "").trim();
7867
8523
  let body = generatedBody;
7868
- if (options.create && explicitBodyFile && await fs14.pathExists(bodyFile)) {
7869
- body = await fs14.readFile(bodyFile, "utf-8");
8524
+ if (options.create && explicitBodyFile && await fs15.pathExists(bodyFile)) {
8525
+ body = await fs15.readFile(bodyFile, "utf-8");
7870
8526
  ensureSections(
7871
8527
  body,
7872
8528
  getRequiredPrSections(config.lang),
@@ -7874,14 +8530,15 @@ function githubCommand(program2) {
7874
8530
  config.lang
7875
8531
  );
7876
8532
  } else {
7877
- await fs14.ensureDir(path18.dirname(bodyFile));
7878
- await fs14.writeFile(bodyFile, generatedBody, "utf-8");
8533
+ await fs15.ensureDir(path18.dirname(bodyFile));
8534
+ await fs15.writeFile(bodyFile, generatedBody, "utf-8");
7879
8535
  }
7880
8536
  const retryCount = toRetryCount(options.retry, config.lang);
7881
8537
  let prUrl = options.pr?.trim() || "";
7882
8538
  let mergedAttempts;
7883
8539
  let syncChanged = false;
7884
8540
  if (options.create) {
8541
+ const projectGitCwd = resolveGithubProjectCwd(config, feature);
7885
8542
  ensureNoTodoPlaceholders(body, tg(config.lang, "kindPr"), config.lang);
7886
8543
  ensurePrArtifacts(body, artifactPolicy, config.lang);
7887
8544
  assertRemoteApproval(
@@ -7907,7 +8564,7 @@ function githubCommand(program2) {
7907
8564
  const created = runProcessOrThrow(
7908
8565
  "gh",
7909
8566
  args,
7910
- process.cwd(),
8567
+ projectGitCwd,
7911
8568
  tg(config.lang, "createPrFailed")
7912
8569
  );
7913
8570
  prUrl = created.stdout.trim();
@@ -7935,6 +8592,7 @@ function githubCommand(program2) {
7935
8592
  syncChanged = synced.changed;
7936
8593
  const shouldCommitSync = !!options.commitSync || !!options.merge;
7937
8594
  if (syncChanged && shouldCommitSync) {
8595
+ const docsGitCwd = resolveGithubDocsCwd(config, feature);
7938
8596
  const message = feature.issueNumber ? tg(config.lang, "syncCommitWithIssue", {
7939
8597
  issue: feature.issueNumber,
7940
8598
  folder: feature.folderName
@@ -7942,7 +8600,7 @@ function githubCommand(program2) {
7942
8600
  folder: feature.folderName
7943
8601
  });
7944
8602
  commitAndPushPath(
7945
- process.cwd(),
8603
+ docsGitCwd,
7946
8604
  synced.path,
7947
8605
  message,
7948
8606
  config.lang
@@ -7950,19 +8608,25 @@ function githubCommand(program2) {
7950
8608
  }
7951
8609
  }
7952
8610
  if (options.merge) {
7953
- const merged = mergePrWithRetry(prUrl, process.cwd(), retryCount, config.lang);
8611
+ const projectGitCwd = resolveGithubProjectCwd(config, feature);
8612
+ const merged = mergePrWithRetry(
8613
+ prUrl,
8614
+ projectGitCwd,
8615
+ retryCount,
8616
+ config.lang
8617
+ );
7954
8618
  mergedAttempts = merged.attempts;
7955
8619
  const baseBranch = options.base || "main";
7956
8620
  runProcessOrThrow(
7957
8621
  "git",
7958
8622
  ["checkout", baseBranch],
7959
- process.cwd(),
8623
+ projectGitCwd,
7960
8624
  tg(config.lang, "checkoutBaseAfterMergeFailed", { base: baseBranch })
7961
8625
  );
7962
8626
  runProcessOrThrow(
7963
8627
  "git",
7964
8628
  ["pull", "--rebase", "origin", baseBranch],
7965
- process.cwd(),
8629
+ projectGitCwd,
7966
8630
  tg(config.lang, "pullBaseAfterMergeFailed", { base: baseBranch })
7967
8631
  );
7968
8632
  }
@@ -8195,6 +8859,132 @@ function docsCommand(program2) {
8195
8859
  }
8196
8860
  });
8197
8861
  }
8862
+ function detectCommand(program2) {
8863
+ program2.command("detect").description(tr(DEFAULT_LANG, "cli", "detect.cmdDescription")).option("--dir <dir>", tr(DEFAULT_LANG, "cli", "detect.optDir")).option("--json", tr(DEFAULT_LANG, "cli", "detect.optJson")).action(async (options) => {
8864
+ try {
8865
+ await runDetect(options);
8866
+ } catch (error) {
8867
+ const config = await getConfig(process.cwd());
8868
+ const lang = config?.lang ?? DEFAULT_LANG;
8869
+ const cliError = toCliError(error);
8870
+ const suggestions = getCliErrorSuggestions(cliError.code, lang);
8871
+ if (options.json) {
8872
+ console.log(
8873
+ JSON.stringify({
8874
+ status: "error",
8875
+ reasonCode: cliError.code,
8876
+ error: cliError.message,
8877
+ suggestions
8878
+ })
8879
+ );
8880
+ } else {
8881
+ console.error(
8882
+ chalk6.red(tr(lang, "cli", "common.errorLabel")),
8883
+ chalk6.red(`[${cliError.code}] ${cliError.message}`)
8884
+ );
8885
+ printCliErrorSuggestions(suggestions, lang);
8886
+ }
8887
+ process.exit(1);
8888
+ }
8889
+ });
8890
+ }
8891
+ async function runDetect(options) {
8892
+ const cwd = process.cwd();
8893
+ const targetCwd = options.dir ? path18.resolve(cwd, options.dir) : cwd;
8894
+ const config = await getConfig(targetCwd);
8895
+ const detected = !!config;
8896
+ const reasonCode = detected ? "PROJECT_DETECTED" : "PROJECT_NOT_DETECTED";
8897
+ if (options.json) {
8898
+ if (!config) {
8899
+ console.log(
8900
+ JSON.stringify(
8901
+ {
8902
+ status: "ok",
8903
+ reasonCode,
8904
+ isLeeSpecKitProject: false,
8905
+ targetCwd,
8906
+ docsDir: null,
8907
+ configPath: null,
8908
+ configFilePresent: false,
8909
+ detectionSource: null,
8910
+ projectType: null,
8911
+ lang: null,
8912
+ projectName: null
8913
+ },
8914
+ null,
8915
+ 2
8916
+ )
8917
+ );
8918
+ return;
8919
+ }
8920
+ const configPath2 = path18.join(config.docsDir, ".lee-spec-kit.json");
8921
+ const configFilePresent2 = await fs15.pathExists(configPath2);
8922
+ const detectionSource2 = configFilePresent2 ? "config" : "heuristic";
8923
+ console.log(
8924
+ JSON.stringify(
8925
+ {
8926
+ status: "ok",
8927
+ reasonCode,
8928
+ isLeeSpecKitProject: true,
8929
+ targetCwd,
8930
+ docsDir: config.docsDir,
8931
+ configPath: configFilePresent2 ? configPath2 : null,
8932
+ configFilePresent: configFilePresent2,
8933
+ detectionSource: detectionSource2,
8934
+ projectType: config.projectType,
8935
+ lang: config.lang,
8936
+ projectName: config.projectName ?? null
8937
+ },
8938
+ null,
8939
+ 2
8940
+ )
8941
+ );
8942
+ return;
8943
+ }
8944
+ const lang = config?.lang ?? DEFAULT_LANG;
8945
+ console.log();
8946
+ console.log(chalk6.blue(tr(lang, "cli", "detect.header")));
8947
+ console.log(chalk6.gray(`- ${tr(lang, "cli", "detect.labelTarget")}: ${targetCwd}`));
8948
+ if (!config) {
8949
+ console.log(chalk6.yellow(`- ${tr(lang, "cli", "detect.resultNotDetected")}`));
8950
+ console.log(chalk6.gray(`- ${tr(lang, "cli", "detect.notDetectedHint")}`));
8951
+ console.log();
8952
+ return;
8953
+ }
8954
+ const configPath = path18.join(config.docsDir, ".lee-spec-kit.json");
8955
+ const configFilePresent = await fs15.pathExists(configPath);
8956
+ const detectionSource = configFilePresent ? "config" : "heuristic";
8957
+ console.log(chalk6.green(`- ${tr(lang, "cli", "detect.resultDetected")}`));
8958
+ console.log(chalk6.gray(`- ${tr(lang, "cli", "detect.labelDocsDir")}: ${config.docsDir}`));
8959
+ console.log(
8960
+ chalk6.gray(
8961
+ `- ${tr(lang, "cli", "detect.labelConfigPath")}: ${configFilePresent ? configPath : "-"}`
8962
+ )
8963
+ );
8964
+ console.log(
8965
+ chalk6.gray(
8966
+ `- ${tr(lang, "cli", "detect.labelSource")}: ${tr(
8967
+ lang,
8968
+ "cli",
8969
+ detectionSource === "config" ? "detect.sourceConfig" : "detect.sourceHeuristic"
8970
+ )}`
8971
+ )
8972
+ );
8973
+ console.log(
8974
+ chalk6.gray(
8975
+ `- ${tr(lang, "cli", "detect.labelProjectType")}: ${config.projectType}`
8976
+ )
8977
+ );
8978
+ console.log(chalk6.gray(`- ${tr(lang, "cli", "detect.labelLang")}: ${config.lang}`));
8979
+ if (config.projectName) {
8980
+ console.log(
8981
+ chalk6.gray(
8982
+ `- ${tr(lang, "cli", "detect.labelProjectName")}: ${config.projectName}`
8983
+ )
8984
+ );
8985
+ }
8986
+ console.log();
8987
+ }
8198
8988
  function isBannerDisabled() {
8199
8989
  const v = (process.env.LEE_SPEC_KIT_NO_BANNER || "").trim();
8200
8990
  return v === "1";
@@ -8243,8 +9033,8 @@ var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
8243
9033
  function getCurrentVersion() {
8244
9034
  try {
8245
9035
  const packageJsonPath = path18.join(__dirname$1, "..", "package.json");
8246
- if (fs14.existsSync(packageJsonPath)) {
8247
- const pkg = fs14.readJsonSync(packageJsonPath);
9036
+ if (fs15.existsSync(packageJsonPath)) {
9037
+ const pkg = fs15.readJsonSync(packageJsonPath);
8248
9038
  return pkg.version;
8249
9039
  }
8250
9040
  } catch {
@@ -8253,8 +9043,8 @@ function getCurrentVersion() {
8253
9043
  }
8254
9044
  function readCache() {
8255
9045
  try {
8256
- if (fs14.existsSync(CACHE_FILE)) {
8257
- return fs14.readJsonSync(CACHE_FILE);
9046
+ if (fs15.existsSync(CACHE_FILE)) {
9047
+ return fs15.readJsonSync(CACHE_FILE);
8258
9048
  }
8259
9049
  } catch {
8260
9050
  }
@@ -8343,8 +9133,8 @@ if (shouldCheckForUpdates()) checkForUpdates();
8343
9133
  function getCliVersion() {
8344
9134
  try {
8345
9135
  const packageJsonPath = path18.join(__dirname$1, "..", "package.json");
8346
- if (fs14.existsSync(packageJsonPath)) {
8347
- const pkg = fs14.readJsonSync(packageJsonPath);
9136
+ if (fs15.existsSync(packageJsonPath)) {
9137
+ const pkg = fs15.readJsonSync(packageJsonPath);
8348
9138
  if (pkg?.version) return String(pkg.version);
8349
9139
  }
8350
9140
  } catch {
@@ -8369,4 +9159,5 @@ viewCommand(program);
8369
9159
  flowCommand(program);
8370
9160
  githubCommand(program);
8371
9161
  docsCommand(program);
9162
+ detectCommand(program);
8372
9163
  await program.parseAsync();