lee-spec-kit 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import path17 from 'path';
2
+ import path18 from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { program } from 'commander';
5
- import fs15 from 'fs-extra';
5
+ import fs14 from 'fs-extra';
6
6
  import prompts from 'prompts';
7
7
  import chalk6 from 'chalk';
8
8
  import { glob } from 'glob';
@@ -11,10 +11,10 @@ import { createHash } from 'crypto';
11
11
  import os from 'os';
12
12
 
13
13
  var getFilename = () => fileURLToPath(import.meta.url);
14
- var getDirname = () => path17.dirname(getFilename());
14
+ var getDirname = () => path18.dirname(getFilename());
15
15
  var __dirname$1 = /* @__PURE__ */ getDirname();
16
16
  async function copyTemplates(src, dest) {
17
- await fs15.copy(src, dest, {
17
+ await fs14.copy(src, dest, {
18
18
  overwrite: true,
19
19
  errorOnExist: false
20
20
  });
@@ -30,22 +30,22 @@ 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 fs15.readFile(file, "utf-8");
33
+ let content = await fs14.readFile(file, "utf-8");
34
34
  content = applyReplacements(content, replacements);
35
- await fs15.writeFile(file, content, "utf-8");
35
+ await fs14.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 fs15.readFile(file, "utf-8");
39
+ let content = await fs14.readFile(file, "utf-8");
40
40
  content = applyReplacements(content, replacements);
41
- await fs15.writeFile(file, content, "utf-8");
41
+ await fs14.writeFile(file, content, "utf-8");
42
42
  }
43
43
  }
44
44
  var __filename2 = fileURLToPath(import.meta.url);
45
- var __dirname2 = path17.dirname(__filename2);
45
+ var __dirname2 = path18.dirname(__filename2);
46
46
  function getTemplatesDir() {
47
- const rootDir = path17.resolve(__dirname2, "..");
48
- return path17.join(rootDir, "templates");
47
+ const rootDir = path18.resolve(__dirname2, "..");
48
+ return path18.join(rootDir, "templates");
49
49
  }
50
50
 
51
51
  // src/utils/i18n.ts
@@ -134,6 +134,8 @@ var I18N = {
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
135
  "context.actionOptionHint": "\uC2B9\uC778 \uC751\uB2F5 \uD615\uC2DD: `<\uB77C\uBCA8>` \uB610\uB294 `<\uB77C\uBCA8> OK` (\uC608: `A`, `A OK`)",
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
+ "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}",
137
139
  "context.readBuiltinDocFirst": "\uBA3C\uC800 \uB0B4\uC7A5 \uBB38\uC11C\uB97C \uD655\uC778\uD558\uC138\uC694: {command}",
138
140
  "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.",
139
141
  "context.list.docsCommitNeeded": "\uBB38\uC11C \uCEE4\uBC0B \uD544\uC694",
@@ -218,6 +220,8 @@ var I18N = {
218
220
  "github.optPrMerge": "\uC7AC\uC2DC\uB3C4/\uD5E4\uB4DC \uAC31\uC2E0\uACFC \uD568\uAED8 PR merge \uC218\uD589",
219
221
  "github.optPrConfirm": "\uC6D0\uACA9 \uC791\uC5C5(--create/--merge)\uC6A9 \uBA85\uC2DC\uC801 \uC2B9\uC778 \uD1A0\uD070. \uC0AC\uC6A9\uAC12: OK",
220
222
  "github.optPrRetry": "merge \uC7AC\uC2DC\uB3C4 \uD69F\uC218 (\uAE30\uBCF8: 3)",
223
+ "github.optPrScreenshots": "PR \uC2A4\uD06C\uB9B0\uC0F7 \uC139\uC158 \uBAA8\uB4DC (auto|on|off, \uAE30\uBCF8: auto)",
224
+ "github.optPrMermaid": "PR Mermaid \uC139\uC158 \uBAA8\uB4DC (auto|on|off, \uAE30\uBCF8: auto)",
221
225
  "github.optPrNoSyncTasks": "tasks.md PR URL/PR \uC0C1\uD0DC \uB3D9\uAE30\uD654\uB97C \uAC74\uB108\uB700",
222
226
  "github.optPrCommitSync": "tasks.md \uB3D9\uAE30\uD654 \uBCC0\uACBD\uC744 \uC790\uB3D9 commit/push",
223
227
  "github.invalidRepoComponentMismatch": "`--repo`\uC640 `--component`\uB97C \uD568\uAED8 \uC4F8 \uB54C\uB294 \uAC19\uC740 \uAC12\uC744 \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.",
@@ -227,6 +231,12 @@ var I18N = {
227
231
  "github.ghEmptyJson": "GitHub CLI JSON \uCD9C\uB825\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.",
228
232
  "github.ghInvalidJson": "GitHub CLI JSON \uD30C\uC2F1\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4: {snippet}",
229
233
  "github.sectionsMissing": "{kind} \uBCF8\uBB38\uC5D0 \uD544\uC218 \uC139\uC158\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: {sections}",
234
+ "github.todoPlaceholdersRemain": "{kind} \uBCF8\uBB38\uC5D0 TODO \uD56D\uBAA9\uC774 \uB0A8\uC544 \uC788\uC2B5\uB2C8\uB2E4. \uBAA9\uD45C/\uC644\uB8CC \uAE30\uC900 \uB4F1\uC744 \uCC44\uC6B4 \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
235
+ "github.artifactModeInvalid": "`--{kind}` \uAC12\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: {value}. \uD5C8\uC6A9\uAC12: auto,on,off",
236
+ "github.prScreenshotsSectionMissing": "PR \uBCF8\uBB38\uC5D0 \uD544\uC218 \uC139\uC158\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: {section}",
237
+ "github.prScreenshotImageMissing": "PR \uBCF8\uBB38\uC758 `{section}` \uC139\uC158\uC5D0 \uC774\uBBF8\uC9C0 \uB9C8\uD06C\uB2E4\uC6B4(`![](...)`)\uC744 \uCD94\uAC00\uD558\uC138\uC694.",
238
+ "github.prMermaidSectionMissing": "PR \uBCF8\uBB38\uC5D0 \uD544\uC218 \uC139\uC158\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: {section}",
239
+ "github.prMermaidBlockMissing": "PR \uBCF8\uBB38\uC758 `{section}` \uC139\uC158\uC5D0 ```mermaid \uCF54\uB4DC \uBE14\uB85D\uC744 \uCD94\uAC00\uD558\uC138\uC694.",
230
240
  "github.docsMissing": "\uAD00\uB828 \uBB38\uC11C \uACBD\uB85C\uAC00 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: {paths}",
231
241
  "github.noFeatures": "Feature\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
232
242
  "github.multipleFeaturesMatched": "\uC5EC\uB7EC Feature\uAC00 \uB9E4\uCE6D\uB418\uC5C8\uC2B5\uB2C8\uB2E4. feature \uC774\uB984(slug | F001 | F001-slug)\uC744 \uBA85\uC2DC\uD558\uC138\uC694.",
@@ -383,7 +393,7 @@ var I18N = {
383
393
  tasksImprove: "tasks.md\uB97C \uBCF4\uC644\uD558\uACE0 \uBB38\uC11C \uC0C1\uD0DC\uB97C Review\uB85C \uBCC0\uACBD\uD558\uC138\uC694.",
384
394
  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)",
385
395
  docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} \uAE30\uD68D \uBB38\uC11C"',
386
- 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. TODO\uB97C \uCC44\uC6B0\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.",
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.",
387
397
  docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
388
398
  docsCommitUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs: {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
389
399
  projectCommitIssueUpdate: 'cd "{projectGitCwd}" && git add -A && git commit -m "feat(#{issueNumber}): {folderName} \uAD6C\uD604 \uC5C5\uB370\uC774\uD2B8"',
@@ -400,7 +410,7 @@ var I18N = {
400
410
  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}",
401
411
  prePrReviewFindingsBlock: "\uC911\uC694 \uC774\uC288\uB294 \uC218\uC815/\uD569\uC758 \uD6C4\uC5D0\uB9CC PR \uC0DD\uC131",
402
412
  prePrReviewFindingsWarn: "\uB9AC\uC2A4\uD06C\uB97C \uACF5\uC720\uD558\uBA74 PR \uC0DD\uC131 \uC9C4\uD589 \uAC00\uB2A5",
403
- 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. TODO\uB97C \uCC44\uC6B0\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.",
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.",
404
414
  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)",
405
415
  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)",
406
416
  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.",
@@ -500,6 +510,8 @@ var I18N = {
500
510
  "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)",
501
511
  "context.actionOptionHint": "Approval reply format: `<label>` or `<label> OK` (e.g. `A`, `A OK`)",
502
512
  "context.actionExplainHint": "Before requesting approval, explain what each label will run/change with a one-line summary.",
513
+ "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}",
503
515
  "context.readBuiltinDocFirst": "Read built-in docs first: {command}",
504
516
  "context.tipDocsCommitRules": "Check commit message rules with `npx lee-spec-kit docs get git-workflow --json`.",
505
517
  "context.list.docsCommitNeeded": "Commit docs changes",
@@ -584,6 +596,8 @@ var I18N = {
584
596
  "github.optPrMerge": "Merge PR with retry and head-branch refresh",
585
597
  "github.optPrConfirm": "Explicit user approval token for remote operations (--create/--merge). Use: OK",
586
598
  "github.optPrRetry": "Retry count for merge (default: 3)",
599
+ "github.optPrScreenshots": "PR screenshots section mode (auto|on|off, default: auto)",
600
+ "github.optPrMermaid": "PR Mermaid section mode (auto|on|off, default: auto)",
587
601
  "github.optPrNoSyncTasks": "Do not sync PR URL/PR status into tasks.md",
588
602
  "github.optPrCommitSync": "Commit and push tasks.md metadata sync automatically",
589
603
  "github.invalidRepoComponentMismatch": "`--repo` and `--component` must reference the same value when both are provided.",
@@ -593,6 +607,12 @@ var I18N = {
593
607
  "github.ghEmptyJson": "GitHub CLI returned empty JSON output.",
594
608
  "github.ghInvalidJson": "GitHub CLI returned invalid JSON: {snippet}",
595
609
  "github.sectionsMissing": "{kind} body is missing required sections: {sections}",
610
+ "github.todoPlaceholdersRemain": "{kind} body still contains TODO placeholders. Fill goals/completion criteria before creating remotely.",
611
+ "github.artifactModeInvalid": "Invalid value for `--{kind}`: {value}. Allowed: auto,on,off",
612
+ "github.prScreenshotsSectionMissing": "PR body is missing required section: {section}",
613
+ "github.prScreenshotImageMissing": "Add image markdown (`![](...)`) to the `{section}` section in PR body.",
614
+ "github.prMermaidSectionMissing": "PR body is missing required section: {section}",
615
+ "github.prMermaidBlockMissing": "Add a ```mermaid code block to the `{section}` section in PR body.",
596
616
  "github.docsMissing": "Related document paths do not exist: {paths}",
597
617
  "github.noFeatures": "No features found.",
598
618
  "github.multipleFeaturesMatched": "Multiple features matched. Specify feature name (slug | F001 | F001-slug).",
@@ -749,7 +769,7 @@ var I18N = {
749
769
  tasksImprove: "Improve tasks.md and change Doc Status to Review.",
750
770
  tasksApproval: "Share tasks.md with the user and get progress approval (`A` or `A OK` format). (Then set Doc Status to Approved)",
751
771
  docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} planning docs"',
752
- 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`. Fill TODOs, get explicit user OK, run `--create --confirm OK`, then update issue number in spec.md/tasks.md and prepare a docs commit.",
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.",
753
773
  docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} docs update"',
754
774
  docsCommitUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs: {folderName} docs update"',
755
775
  projectCommitIssueUpdate: 'cd "{projectGitCwd}" && git add -A && git commit -m "feat(#{issueNumber}): {folderName} implementation update"',
@@ -766,7 +786,7 @@ var I18N = {
766
786
  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}",
767
787
  prePrReviewFindingsBlock: "major findings must be fixed/aligned before PR creation",
768
788
  prePrReviewFindingsWarn: "you may proceed after sharing the risks",
769
- 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`. Fill TODOs, get explicit user OK, run `--create --confirm OK`, then record the PR link in tasks.md.",
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.",
770
790
  prFillStatus: "Set PR Status in tasks.md to Review/Approved. (After merge, update it to Approved.)",
771
791
  prResolveReview: "Resolve review comments and update PR Status. (PR Status: Review \u2192 Approved)",
772
792
  prRequestReview: "Request review and update PR Status to Review.",
@@ -1183,17 +1203,17 @@ function sleep(ms) {
1183
1203
  return new Promise((resolve) => globalThis.setTimeout(resolve, ms));
1184
1204
  }
1185
1205
  function getDocsLockPath(docsDir) {
1186
- return path17.join(docsDir, ".lee-spec-kit.lock");
1206
+ return path18.join(docsDir, ".lee-spec-kit.lock");
1187
1207
  }
1188
1208
  function getInitLockPath(targetDir) {
1189
- return path17.join(
1190
- path17.dirname(targetDir),
1191
- `.lee-spec-kit.${path17.basename(targetDir)}.lock`
1209
+ return path18.join(
1210
+ path18.dirname(targetDir),
1211
+ `.lee-spec-kit.${path18.basename(targetDir)}.lock`
1192
1212
  );
1193
1213
  }
1194
1214
  async function isStaleLock(lockPath, staleMs) {
1195
1215
  try {
1196
- const stat = await fs15.stat(lockPath);
1216
+ const stat = await fs14.stat(lockPath);
1197
1217
  if (Date.now() - stat.mtimeMs <= staleMs) {
1198
1218
  return false;
1199
1219
  }
@@ -1208,7 +1228,7 @@ async function isStaleLock(lockPath, staleMs) {
1208
1228
  }
1209
1229
  async function readLockPayload(lockPath) {
1210
1230
  try {
1211
- const raw = await fs15.readFile(lockPath, "utf8");
1231
+ const raw = await fs14.readFile(lockPath, "utf8");
1212
1232
  const parsed = JSON.parse(raw);
1213
1233
  if (!parsed || typeof parsed !== "object") return null;
1214
1234
  return parsed;
@@ -1230,17 +1250,17 @@ function isProcessAlive(pid) {
1230
1250
  }
1231
1251
  }
1232
1252
  async function tryAcquire(lockPath, owner) {
1233
- await fs15.ensureDir(path17.dirname(lockPath));
1253
+ await fs14.ensureDir(path18.dirname(lockPath));
1234
1254
  try {
1235
- const fd = await fs15.open(lockPath, "wx");
1255
+ const fd = await fs14.open(lockPath, "wx");
1236
1256
  const payload = JSON.stringify(
1237
1257
  { pid: process.pid, owner: owner ?? "unknown", createdAt: (/* @__PURE__ */ new Date()).toISOString() },
1238
1258
  null,
1239
1259
  2
1240
1260
  );
1241
- await fs15.writeFile(fd, `${payload}
1261
+ await fs14.writeFile(fd, `${payload}
1242
1262
  `, { encoding: "utf8" });
1243
- await fs15.close(fd);
1263
+ await fs14.close(fd);
1244
1264
  return true;
1245
1265
  } catch (error) {
1246
1266
  if (error.code === "EEXIST") {
@@ -1254,9 +1274,9 @@ async function waitForLockRelease(lockPath, options = {}) {
1254
1274
  const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
1255
1275
  const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
1256
1276
  const startedAt = Date.now();
1257
- while (await fs15.pathExists(lockPath)) {
1277
+ while (await fs14.pathExists(lockPath)) {
1258
1278
  if (await isStaleLock(lockPath, staleMs)) {
1259
- await fs15.remove(lockPath);
1279
+ await fs14.remove(lockPath);
1260
1280
  break;
1261
1281
  }
1262
1282
  if (Date.now() - startedAt > timeoutMs) {
@@ -1274,7 +1294,7 @@ async function withFileLock(lockPath, task, options = {}) {
1274
1294
  const acquired = await tryAcquire(lockPath, options.owner);
1275
1295
  if (acquired) break;
1276
1296
  if (await isStaleLock(lockPath, staleMs)) {
1277
- await fs15.remove(lockPath);
1297
+ await fs14.remove(lockPath);
1278
1298
  continue;
1279
1299
  }
1280
1300
  if (Date.now() - startedAt > timeoutMs) {
@@ -1288,7 +1308,7 @@ async function withFileLock(lockPath, task, options = {}) {
1288
1308
  try {
1289
1309
  return await task();
1290
1310
  } finally {
1291
- await fs15.remove(lockPath).catch(() => {
1311
+ await fs14.remove(lockPath).catch(() => {
1292
1312
  });
1293
1313
  }
1294
1314
  }
@@ -1307,30 +1327,30 @@ var ENGINE_MANAGED_AGENT_FILES = [
1307
1327
  "pr-template.md"
1308
1328
  ];
1309
1329
  var ENGINE_MANAGED_AGENT_DIRS = ["skills"];
1310
- var ENGINE_MANAGED_FEATURE_PATH = path17.join(
1330
+ var ENGINE_MANAGED_FEATURE_PATH = path18.join(
1311
1331
  "features",
1312
1332
  "feature-base"
1313
1333
  );
1314
1334
  async function pruneEngineManagedDocs(docsDir) {
1315
1335
  const removed = [];
1316
1336
  for (const file of ENGINE_MANAGED_AGENT_FILES) {
1317
- const target = path17.join(docsDir, "agents", file);
1318
- if (await fs15.pathExists(target)) {
1319
- await fs15.remove(target);
1320
- removed.push(path17.relative(docsDir, target));
1337
+ const target = path18.join(docsDir, "agents", file);
1338
+ if (await fs14.pathExists(target)) {
1339
+ await fs14.remove(target);
1340
+ removed.push(path18.relative(docsDir, target));
1321
1341
  }
1322
1342
  }
1323
1343
  for (const dir of ENGINE_MANAGED_AGENT_DIRS) {
1324
- const target = path17.join(docsDir, "agents", dir);
1325
- if (await fs15.pathExists(target)) {
1326
- await fs15.remove(target);
1327
- removed.push(path17.relative(docsDir, target));
1344
+ const target = path18.join(docsDir, "agents", dir);
1345
+ if (await fs14.pathExists(target)) {
1346
+ await fs14.remove(target);
1347
+ removed.push(path18.relative(docsDir, target));
1328
1348
  }
1329
1349
  }
1330
- const featureBasePath = path17.join(docsDir, ENGINE_MANAGED_FEATURE_PATH);
1331
- if (await fs15.pathExists(featureBasePath)) {
1332
- await fs15.remove(featureBasePath);
1333
- removed.push(path17.relative(docsDir, featureBasePath));
1350
+ const featureBasePath = path18.join(docsDir, ENGINE_MANAGED_FEATURE_PATH);
1351
+ if (await fs14.pathExists(featureBasePath)) {
1352
+ await fs14.remove(featureBasePath);
1353
+ removed.push(path18.relative(docsDir, featureBasePath));
1334
1354
  }
1335
1355
  return removed;
1336
1356
  }
@@ -1383,7 +1403,7 @@ ${tr(lang2, "cli", "common.canceled")}`)
1383
1403
  }
1384
1404
  async function runInit(options) {
1385
1405
  const cwd = process.cwd();
1386
- const defaultName = path17.basename(cwd);
1406
+ const defaultName = path18.basename(cwd);
1387
1407
  let projectName = options.name || defaultName;
1388
1408
  let projectType = options.type;
1389
1409
  let components = parseComponentsOption(options.components);
@@ -1393,7 +1413,7 @@ async function runInit(options) {
1393
1413
  let pushDocs = typeof options.pushDocs === "boolean" ? options.pushDocs : void 0;
1394
1414
  let docsRemote = options.docsRemote;
1395
1415
  let projectRoot;
1396
- const targetDir = path17.resolve(cwd, options.dir || "./docs");
1416
+ const targetDir = path18.resolve(cwd, options.dir || "./docs");
1397
1417
  const skipPrompts = !!options.yes || !!options.nonInteractive;
1398
1418
  if (options.docsRepo && !["embedded", "standalone"].includes(options.docsRepo)) {
1399
1419
  throw createCliError(
@@ -1711,8 +1731,8 @@ async function runInit(options) {
1711
1731
  await withFileLock(
1712
1732
  initLockPath,
1713
1733
  async () => {
1714
- if (await fs15.pathExists(targetDir)) {
1715
- const files = await fs15.readdir(targetDir);
1734
+ if (await fs14.pathExists(targetDir)) {
1735
+ const files = await fs14.readdir(targetDir);
1716
1736
  if (files.length > 0) {
1717
1737
  if (options.force) {
1718
1738
  } else if (options.nonInteractive) {
@@ -1748,28 +1768,28 @@ async function runInit(options) {
1748
1768
  );
1749
1769
  console.log();
1750
1770
  const templatesDir = getTemplatesDir();
1751
- const commonPath = path17.join(templatesDir, lang, "common");
1771
+ const commonPath = path18.join(templatesDir, lang, "common");
1752
1772
  const templateProjectType = toTemplateProjectType(projectType);
1753
- const typePath = path17.join(templatesDir, lang, templateProjectType);
1754
- if (await fs15.pathExists(commonPath)) {
1773
+ const typePath = path18.join(templatesDir, lang, templateProjectType);
1774
+ if (await fs14.pathExists(commonPath)) {
1755
1775
  await copyTemplates(commonPath, targetDir);
1756
1776
  }
1757
- if (!await fs15.pathExists(typePath)) {
1777
+ if (!await fs14.pathExists(typePath)) {
1758
1778
  throw new Error(
1759
1779
  tr(lang, "cli", "init.error.templateNotFound", { path: typePath })
1760
1780
  );
1761
1781
  }
1762
1782
  await copyTemplates(typePath, targetDir);
1763
1783
  if (projectType === "multi" && !isDefaultFullstackComponents(components)) {
1764
- const featuresRoot = path17.join(targetDir, "features");
1765
- await fs15.remove(path17.join(featuresRoot, "fe"));
1766
- await fs15.remove(path17.join(featuresRoot, "be"));
1784
+ const featuresRoot = path18.join(targetDir, "features");
1785
+ await fs14.remove(path18.join(featuresRoot, "fe"));
1786
+ await fs14.remove(path18.join(featuresRoot, "be"));
1767
1787
  for (const component of components) {
1768
- const componentDir = path17.join(featuresRoot, component);
1769
- await fs15.ensureDir(componentDir);
1770
- const readmePath = path17.join(componentDir, "README.md");
1771
- if (!await fs15.pathExists(readmePath)) {
1772
- await fs15.writeFile(
1788
+ const componentDir = path18.join(featuresRoot, component);
1789
+ await fs14.ensureDir(componentDir);
1790
+ const readmePath = path18.join(componentDir, "README.md");
1791
+ if (!await fs14.pathExists(readmePath)) {
1792
+ await fs14.writeFile(
1773
1793
  readmePath,
1774
1794
  `# ${component.toUpperCase()} Features
1775
1795
 
@@ -1818,8 +1838,8 @@ Store ${component} feature specs here.
1818
1838
  config.projectRoot = projectRoot;
1819
1839
  }
1820
1840
  }
1821
- const configPath = path17.join(targetDir, ".lee-spec-kit.json");
1822
- await fs15.writeJson(configPath, config, { spaces: 2 });
1841
+ const configPath = path18.join(targetDir, ".lee-spec-kit.json");
1842
+ await fs14.writeJson(configPath, config, { spaces: 2 });
1823
1843
  console.log(chalk6.green(tr(lang, "cli", "init.log.docsCreated")));
1824
1844
  console.log();
1825
1845
  await initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote);
@@ -1887,7 +1907,7 @@ async function initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote) {
1887
1907
  console.log(chalk6.blue(tr(lang, "cli", "init.log.gitInit")));
1888
1908
  runGit(["init"], cwd);
1889
1909
  }
1890
- const relativePath = path17.relative(cwd, targetDir);
1910
+ const relativePath = path18.relative(cwd, targetDir);
1891
1911
  const stagedBeforeAdd = getCachedStagedFiles(cwd);
1892
1912
  if (relativePath === "." && stagedBeforeAdd && stagedBeforeAdd.length > 0) {
1893
1913
  console.log(
@@ -1944,17 +1964,17 @@ async function initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote) {
1944
1964
  }
1945
1965
  function getAncestorDirs(startDir) {
1946
1966
  const dirs = [];
1947
- let current = path17.resolve(startDir);
1967
+ let current = path18.resolve(startDir);
1948
1968
  while (true) {
1949
1969
  dirs.push(current);
1950
- const parent = path17.dirname(current);
1970
+ const parent = path18.dirname(current);
1951
1971
  if (parent === current) break;
1952
1972
  current = parent;
1953
1973
  }
1954
1974
  return dirs;
1955
1975
  }
1956
1976
  function hasWorkspaceBoundary(dir) {
1957
- return fs15.existsSync(path17.join(dir, "package.json")) || fs15.existsSync(path17.join(dir, ".git"));
1977
+ return fs14.existsSync(path18.join(dir, "package.json")) || fs14.existsSync(path18.join(dir, ".git"));
1958
1978
  }
1959
1979
  function getSearchBaseDirs(cwd) {
1960
1980
  const ancestors = getAncestorDirs(cwd);
@@ -1967,24 +1987,24 @@ function getSearchBaseDirs(cwd) {
1967
1987
  async function getConfig(cwd) {
1968
1988
  const explicitDocsDir = (process.env.LEE_SPEC_KIT_DOCS_DIR || "").trim();
1969
1989
  const baseDirs = [
1970
- ...explicitDocsDir ? [path17.resolve(explicitDocsDir)] : [],
1990
+ ...explicitDocsDir ? [path18.resolve(explicitDocsDir)] : [],
1971
1991
  ...getSearchBaseDirs(cwd)
1972
1992
  ];
1973
1993
  const visitedBaseDirs = /* @__PURE__ */ new Set();
1974
1994
  const visitedDocsDirs = /* @__PURE__ */ new Set();
1975
1995
  for (const baseDir of baseDirs) {
1976
- const resolvedBaseDir = path17.resolve(baseDir);
1996
+ const resolvedBaseDir = path18.resolve(baseDir);
1977
1997
  if (visitedBaseDirs.has(resolvedBaseDir)) continue;
1978
1998
  visitedBaseDirs.add(resolvedBaseDir);
1979
- const possibleDocsDirs = [path17.join(resolvedBaseDir, "docs"), resolvedBaseDir];
1999
+ const possibleDocsDirs = [path18.join(resolvedBaseDir, "docs"), resolvedBaseDir];
1980
2000
  for (const docsDir of possibleDocsDirs) {
1981
- const resolvedDocsDir = path17.resolve(docsDir);
2001
+ const resolvedDocsDir = path18.resolve(docsDir);
1982
2002
  if (visitedDocsDirs.has(resolvedDocsDir)) continue;
1983
2003
  visitedDocsDirs.add(resolvedDocsDir);
1984
- const configPath = path17.join(resolvedDocsDir, ".lee-spec-kit.json");
1985
- if (await fs15.pathExists(configPath)) {
2004
+ const configPath = path18.join(resolvedDocsDir, ".lee-spec-kit.json");
2005
+ if (await fs14.pathExists(configPath)) {
1986
2006
  try {
1987
- const configFile = await fs15.readJson(configPath);
2007
+ const configFile = await fs14.readJson(configPath);
1988
2008
  const projectType = normalizeProjectType(configFile.projectType);
1989
2009
  const components = resolveProjectComponents(
1990
2010
  projectType,
@@ -2007,22 +2027,22 @@ async function getConfig(cwd) {
2007
2027
  } catch {
2008
2028
  }
2009
2029
  }
2010
- const agentsPath = path17.join(resolvedDocsDir, "agents");
2011
- const featuresPath = path17.join(resolvedDocsDir, "features");
2012
- if (await fs15.pathExists(agentsPath) && await fs15.pathExists(featuresPath)) {
2013
- const bePath = path17.join(featuresPath, "be");
2014
- const fePath = path17.join(featuresPath, "fe");
2015
- const projectType = await fs15.pathExists(bePath) || await fs15.pathExists(fePath) ? "multi" : "single";
2030
+ const agentsPath = path18.join(resolvedDocsDir, "agents");
2031
+ const featuresPath = path18.join(resolvedDocsDir, "features");
2032
+ if (await fs14.pathExists(agentsPath) && await fs14.pathExists(featuresPath)) {
2033
+ const bePath = path18.join(featuresPath, "be");
2034
+ const fePath = path18.join(featuresPath, "fe");
2035
+ const projectType = await fs14.pathExists(bePath) || await fs14.pathExists(fePath) ? "multi" : "single";
2016
2036
  const components = projectType === "multi" ? resolveProjectComponents("multi", ["fe", "be"]) : void 0;
2017
2037
  const langProbeCandidates = [
2018
- path17.join(agentsPath, "custom.md"),
2019
- path17.join(agentsPath, "constitution.md"),
2020
- path17.join(agentsPath, "agents.md")
2038
+ path18.join(agentsPath, "custom.md"),
2039
+ path18.join(agentsPath, "constitution.md"),
2040
+ path18.join(agentsPath, "agents.md")
2021
2041
  ];
2022
2042
  let lang = "en";
2023
2043
  for (const candidate of langProbeCandidates) {
2024
- if (!await fs15.pathExists(candidate)) continue;
2025
- const content = await fs15.readFile(candidate, "utf-8");
2044
+ if (!await fs14.pathExists(candidate)) continue;
2045
+ const content = await fs14.readFile(candidate, "utf-8");
2026
2046
  if (/[가-힣]/.test(content)) {
2027
2047
  lang = "ko";
2028
2048
  break;
@@ -2068,14 +2088,14 @@ function sanitizeTasksForLocal(content, lang) {
2068
2088
  return normalizeTrailingBlankLines(next);
2069
2089
  }
2070
2090
  async function patchMarkdownIfExists(filePath, transform) {
2071
- if (!await fs15.pathExists(filePath)) return;
2072
- const content = await fs15.readFile(filePath, "utf-8");
2073
- await fs15.writeFile(filePath, transform(content), "utf-8");
2091
+ if (!await fs14.pathExists(filePath)) return;
2092
+ const content = await fs14.readFile(filePath, "utf-8");
2093
+ await fs14.writeFile(filePath, transform(content), "utf-8");
2074
2094
  }
2075
2095
  async function applyLocalWorkflowTemplateToFeatureDir(featureDir, lang) {
2076
- await patchMarkdownIfExists(path17.join(featureDir, "spec.md"), sanitizeSpecForLocal);
2096
+ await patchMarkdownIfExists(path18.join(featureDir, "spec.md"), sanitizeSpecForLocal);
2077
2097
  await patchMarkdownIfExists(
2078
- path17.join(featureDir, "tasks.md"),
2098
+ path18.join(featureDir, "tasks.md"),
2079
2099
  (content) => sanitizeTasksForLocal(content, lang)
2080
2100
  );
2081
2101
  }
@@ -2225,29 +2245,29 @@ async function runFeature(name, options) {
2225
2245
  }
2226
2246
  let featuresDir;
2227
2247
  if (projectType === "multi") {
2228
- featuresDir = path17.join(docsDir, "features", component);
2248
+ featuresDir = path18.join(docsDir, "features", component);
2229
2249
  } else {
2230
- featuresDir = path17.join(docsDir, "features");
2250
+ featuresDir = path18.join(docsDir, "features");
2231
2251
  }
2232
2252
  const featureFolderName = `${featureId}-${name}`;
2233
- const featureDir = path17.join(featuresDir, featureFolderName);
2234
- if (await fs15.pathExists(featureDir)) {
2253
+ const featureDir = path18.join(featuresDir, featureFolderName);
2254
+ if (await fs14.pathExists(featureDir)) {
2235
2255
  throw createCliError(
2236
2256
  "INVALID_ARGUMENT",
2237
2257
  tr(lang, "cli", "feature.folderExists", { path: featureDir })
2238
2258
  );
2239
2259
  }
2240
- const featureBasePath = path17.join(
2260
+ const featureBasePath = path18.join(
2241
2261
  getTemplatesDir(),
2242
2262
  lang,
2243
2263
  toTemplateProjectType(projectType),
2244
2264
  "features",
2245
2265
  "feature-base"
2246
2266
  );
2247
- if (!await fs15.pathExists(featureBasePath)) {
2267
+ if (!await fs14.pathExists(featureBasePath)) {
2248
2268
  throw createCliError("DOCS_NOT_FOUND", tr(lang, "cli", "feature.baseNotFound"));
2249
2269
  }
2250
- await fs15.copy(featureBasePath, featureDir);
2270
+ await fs14.copy(featureBasePath, featureDir);
2251
2271
  const idNumber = featureId.replace("F", "");
2252
2272
  const repoName = projectType === "multi" ? `{{projectName}}-${component}` : "{{projectName}}";
2253
2273
  const replacements = {
@@ -2294,7 +2314,7 @@ async function runFeature(name, options) {
2294
2314
  featureName: name,
2295
2315
  component: projectType === "multi" ? component : void 0,
2296
2316
  featurePath: featureDir,
2297
- featurePathFromDocs: path17.relative(docsDir, featureDir)
2317
+ featurePathFromDocs: path18.relative(docsDir, featureDir)
2298
2318
  };
2299
2319
  },
2300
2320
  { owner: "feature" }
@@ -2306,9 +2326,9 @@ function sleep2(ms) {
2306
2326
  async function waitForConfigAfterInit(cwd, timeoutMs = 8e3) {
2307
2327
  const explicitDocsDir = (process.env.LEE_SPEC_KIT_DOCS_DIR || "").trim();
2308
2328
  const candidates = [
2309
- ...explicitDocsDir ? [path17.resolve(explicitDocsDir)] : [],
2310
- path17.resolve(cwd, "docs"),
2311
- path17.resolve(cwd)
2329
+ ...explicitDocsDir ? [path18.resolve(explicitDocsDir)] : [],
2330
+ path18.resolve(cwd, "docs"),
2331
+ path18.resolve(cwd)
2312
2332
  ];
2313
2333
  const endAt = Date.now() + timeoutMs;
2314
2334
  while (Date.now() < endAt) {
@@ -2319,7 +2339,7 @@ async function waitForConfigAfterInit(cwd, timeoutMs = 8e3) {
2319
2339
  const initLockPath = getInitLockPath(dir);
2320
2340
  const docsLockPath = getDocsLockPath(dir);
2321
2341
  for (const lockPath of [initLockPath, docsLockPath]) {
2322
- if (await fs15.pathExists(lockPath)) {
2342
+ if (await fs14.pathExists(lockPath)) {
2323
2343
  sawLock = true;
2324
2344
  await waitForLockRelease(lockPath, {
2325
2345
  timeoutMs: Math.max(200, endAt - Date.now()),
@@ -2335,17 +2355,17 @@ async function waitForConfigAfterInit(cwd, timeoutMs = 8e3) {
2335
2355
  return getConfig(cwd);
2336
2356
  }
2337
2357
  async function getNextFeatureId(docsDir, projectType, components) {
2338
- const featuresDir = path17.join(docsDir, "features");
2358
+ const featuresDir = path18.join(docsDir, "features");
2339
2359
  let max = 0;
2340
2360
  const scanDirs = [];
2341
2361
  if (projectType === "multi") {
2342
- scanDirs.push(...components.map((component) => path17.join(featuresDir, component)));
2362
+ scanDirs.push(...components.map((component) => path18.join(featuresDir, component)));
2343
2363
  } else {
2344
2364
  scanDirs.push(featuresDir);
2345
2365
  }
2346
2366
  for (const dir of scanDirs) {
2347
- if (!await fs15.pathExists(dir)) continue;
2348
- const entries = await fs15.readdir(dir, { withFileTypes: true });
2367
+ if (!await fs14.pathExists(dir)) continue;
2368
+ const entries = await fs14.readdir(dir, { withFileTypes: true });
2349
2369
  for (const entry of entries) {
2350
2370
  if (!entry.isDirectory()) continue;
2351
2371
  const match = entry.name.match(/^F(\d+)-/);
@@ -3113,6 +3133,59 @@ function getGitStatusPorcelain(cwd, relativePaths) {
3113
3133
  return void 0;
3114
3134
  }
3115
3135
  }
3136
+ function normalizeInputPath(value) {
3137
+ return value.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
3138
+ }
3139
+ function toUniqueNormalizedPaths(relativePaths) {
3140
+ const seen = /* @__PURE__ */ new Set();
3141
+ const out = [];
3142
+ for (const value of relativePaths) {
3143
+ const normalized = normalizeInputPath(value);
3144
+ if (!normalized) continue;
3145
+ if (seen.has(normalized)) continue;
3146
+ seen.add(normalized);
3147
+ out.push(normalized);
3148
+ }
3149
+ return out;
3150
+ }
3151
+ function getTrackedGitPaths(cwd, relativePaths) {
3152
+ const inputs = toUniqueNormalizedPaths(relativePaths);
3153
+ if (inputs.length === 0) return /* @__PURE__ */ new Set();
3154
+ try {
3155
+ const out = execFileSync("git", ["ls-files", "--", ...inputs], {
3156
+ cwd,
3157
+ encoding: "utf-8",
3158
+ stdio: ["ignore", "pipe", "pipe"]
3159
+ });
3160
+ return new Set(
3161
+ out.split("\n").map((line) => normalizeInputPath(line)).filter(Boolean)
3162
+ );
3163
+ } catch {
3164
+ return void 0;
3165
+ }
3166
+ }
3167
+ function getIgnoredGitPaths(cwd, relativePaths) {
3168
+ const inputs = toUniqueNormalizedPaths(relativePaths);
3169
+ if (inputs.length === 0) return /* @__PURE__ */ new Set();
3170
+ try {
3171
+ const out = execFileSync("git", ["check-ignore", "--stdin"], {
3172
+ cwd,
3173
+ encoding: "utf-8",
3174
+ input: `${inputs.join("\n")}
3175
+ `,
3176
+ stdio: ["pipe", "pipe", "pipe"]
3177
+ });
3178
+ return new Set(
3179
+ out.split("\n").map((line) => normalizeInputPath(line)).filter(Boolean)
3180
+ );
3181
+ } catch (error) {
3182
+ if (error && typeof error === "object" && "status" in error) {
3183
+ const status = error.status;
3184
+ if (status === 1) return /* @__PURE__ */ new Set();
3185
+ }
3186
+ return void 0;
3187
+ }
3188
+ }
3116
3189
  function getLastCommitForPath(cwd, relativePath) {
3117
3190
  try {
3118
3191
  const out = execSync(`git rev-list -n 1 HEAD -- "${relativePath}"`, {
@@ -3260,13 +3333,13 @@ function parsePrLink(value) {
3260
3333
  return trimmed;
3261
3334
  }
3262
3335
  function normalizeGitPath(value) {
3263
- return value.split(path17.sep).join("/");
3336
+ return value.split(path18.sep).join("/");
3264
3337
  }
3265
3338
  function resolveProjectStatusPaths(projectGitCwd, docsDir) {
3266
- const relativeDocsDir = path17.relative(projectGitCwd, docsDir);
3339
+ const relativeDocsDir = path18.relative(projectGitCwd, docsDir);
3267
3340
  if (!relativeDocsDir) return [];
3268
- if (path17.isAbsolute(relativeDocsDir)) return [];
3269
- if (relativeDocsDir === ".." || relativeDocsDir.startsWith(`..${path17.sep}`)) {
3341
+ if (path18.isAbsolute(relativeDocsDir)) return [];
3342
+ if (relativeDocsDir === ".." || relativeDocsDir.startsWith(`..${path18.sep}`)) {
3270
3343
  return [];
3271
3344
  }
3272
3345
  const normalizedDocsDir = normalizeGitPath(relativeDocsDir).replace(/\/+$/, "");
@@ -3285,6 +3358,8 @@ function uniqueNormalizedPaths(values) {
3285
3358
  }
3286
3359
  return out;
3287
3360
  }
3361
+ var PROJECT_DIRTY_STATUS_CACHE = /* @__PURE__ */ new Map();
3362
+ var COMPONENT_STATUS_PATH_CACHE = /* @__PURE__ */ new Map();
3288
3363
  async function resolveComponentStatusPaths(projectGitCwd, component, workflow) {
3289
3364
  const configured = workflow?.componentPaths?.[component];
3290
3365
  const configuredCandidates = Array.isArray(configured) ? configured.map((value) => String(value).trim()).filter(Boolean) : [];
@@ -3298,19 +3373,27 @@ async function resolveComponentStatusPaths(projectGitCwd, component, workflow) {
3298
3373
  const normalizedCandidates = uniqueNormalizedPaths(
3299
3374
  candidates.map((candidate) => {
3300
3375
  if (!candidate) return "";
3301
- if (!path17.isAbsolute(candidate)) return candidate;
3302
- const relative = path17.relative(projectGitCwd, candidate);
3376
+ if (!path18.isAbsolute(candidate)) return candidate;
3377
+ const relative = path18.relative(projectGitCwd, candidate);
3303
3378
  if (!relative) return "";
3304
- if (relative === ".." || relative.startsWith(`..${path17.sep}`)) return "";
3379
+ if (relative === ".." || relative.startsWith(`..${path18.sep}`)) return "";
3305
3380
  return relative;
3306
3381
  }).filter(Boolean)
3307
3382
  );
3383
+ const cacheKey = JSON.stringify({
3384
+ projectGitCwd,
3385
+ component,
3386
+ normalizedCandidates
3387
+ });
3388
+ const cached = COMPONENT_STATUS_PATH_CACHE.get(cacheKey);
3389
+ if (cached) return [...cached];
3308
3390
  const existing = [];
3309
3391
  for (const candidate of normalizedCandidates) {
3310
- if (await fs15.pathExists(path17.join(projectGitCwd, candidate))) {
3392
+ if (await fs14.pathExists(path18.join(projectGitCwd, candidate))) {
3311
3393
  existing.push(candidate);
3312
3394
  }
3313
3395
  }
3396
+ COMPONENT_STATUS_PATH_CACHE.set(cacheKey, [...existing]);
3314
3397
  return existing;
3315
3398
  }
3316
3399
  function parseTasks(content) {
@@ -3370,31 +3453,31 @@ async function parseFeature(featurePath, type, context, options) {
3370
3453
  const lang = options.lang;
3371
3454
  const workflowPolicy = resolveWorkflowPolicy(options.workflow);
3372
3455
  const prePrReviewPolicy = resolvePrePrReviewPolicy(options.workflow);
3373
- const folderName = path17.basename(featurePath);
3456
+ const folderName = path18.basename(featurePath);
3374
3457
  const match = folderName.match(/^(F\d+)-(.+)$/);
3375
3458
  const id = match?.[1];
3376
3459
  const slug = match?.[2] || folderName;
3377
- const specPath = path17.join(featurePath, "spec.md");
3378
- const planPath = path17.join(featurePath, "plan.md");
3379
- const tasksPath = path17.join(featurePath, "tasks.md");
3460
+ const specPath = path18.join(featurePath, "spec.md");
3461
+ const planPath = path18.join(featurePath, "plan.md");
3462
+ const tasksPath = path18.join(featurePath, "tasks.md");
3380
3463
  let specStatus;
3381
3464
  let issueNumber;
3382
- const specExists = await fs15.pathExists(specPath);
3465
+ const specExists = await fs14.pathExists(specPath);
3383
3466
  if (specExists) {
3384
- const content = await fs15.readFile(specPath, "utf-8");
3467
+ const content = await fs14.readFile(specPath, "utf-8");
3385
3468
  const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
3386
3469
  specStatus = parseDocStatus(statusValue);
3387
3470
  const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
3388
3471
  issueNumber = parseIssueNumber(issueValue);
3389
3472
  }
3390
3473
  let planStatus;
3391
- const planExists = await fs15.pathExists(planPath);
3474
+ const planExists = await fs14.pathExists(planPath);
3392
3475
  if (planExists) {
3393
- const content = await fs15.readFile(planPath, "utf-8");
3476
+ const content = await fs14.readFile(planPath, "utf-8");
3394
3477
  const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
3395
3478
  planStatus = parseDocStatus(statusValue);
3396
3479
  }
3397
- const tasksExists = await fs15.pathExists(tasksPath);
3480
+ const tasksExists = await fs14.pathExists(tasksPath);
3398
3481
  const tasksSummary = { total: 0, todo: 0, doing: 0, done: 0 };
3399
3482
  let activeTask;
3400
3483
  let nextTodoTask;
@@ -3408,7 +3491,7 @@ async function parseFeature(featurePath, type, context, options) {
3408
3491
  let prStatusFieldExists = false;
3409
3492
  let prePrReviewFieldExists = false;
3410
3493
  if (tasksExists) {
3411
- const content = await fs15.readFile(tasksPath, "utf-8");
3494
+ const content = await fs14.readFile(tasksPath, "utf-8");
3412
3495
  const { summary, activeTask: active, nextTodoTask: nextTodo } = parseTasks(content);
3413
3496
  tasksSummary.total = summary.total;
3414
3497
  tasksSummary.todo = summary.todo;
@@ -3450,44 +3533,77 @@ async function parseFeature(featurePath, type, context, options) {
3450
3533
  slug,
3451
3534
  folderName
3452
3535
  );
3453
- const relativeFeaturePathFromDocs = path17.relative(context.docsDir, featurePath);
3454
- const docsPathIgnored = isGitPathIgnored(
3455
- context.docsGitCwd,
3456
- relativeFeaturePathFromDocs
3457
- );
3458
- const docsStatus = getGitStatusPorcelain(context.docsGitCwd, [relativeFeaturePathFromDocs]);
3459
- const docsHasUncommittedChanges = docsStatus === void 0 ? true : docsStatus.trim().length > 0;
3460
- const dirtyScopePolicy = resolveCodeDirtyScopePolicy(options.workflow, options.projectType);
3461
- let projectStatusPaths = [];
3462
- if (context.projectGitCwd) {
3463
- if (dirtyScopePolicy === "component" && type !== "single") {
3464
- const componentStatusPaths = await resolveComponentStatusPaths(
3465
- context.projectGitCwd,
3466
- type,
3467
- options.workflow
3468
- );
3469
- projectStatusPaths = componentStatusPaths.length > 0 ? componentStatusPaths : resolveProjectStatusPaths(context.projectGitCwd, context.docsDir);
3536
+ const relativeFeaturePathFromDocs = path18.relative(context.docsDir, featurePath);
3537
+ const normalizedFeaturePathFromDocs = normalizeGitPath(relativeFeaturePathFromDocs);
3538
+ const docsPathIgnored = typeof context.docsPathIgnored === "boolean" ? context.docsPathIgnored : isGitPathIgnored(context.docsGitCwd, normalizedFeaturePathFromDocs);
3539
+ let docsHasUncommittedChanges = typeof context.docsHasUncommittedChanges === "boolean" ? context.docsHasUncommittedChanges : false;
3540
+ let docsEverCommitted = typeof context.docsEverCommitted === "boolean" ? context.docsEverCommitted : false;
3541
+ let docsGitUnavailable = !!context.docsGitUnavailable;
3542
+ if (typeof context.docsHasUncommittedChanges !== "boolean") {
3543
+ const docsStatus = getGitStatusPorcelain(context.docsGitCwd, [normalizedFeaturePathFromDocs]);
3544
+ if (docsStatus === void 0) {
3545
+ docsGitUnavailable = true;
3546
+ docsHasUncommittedChanges = true;
3470
3547
  } else {
3471
- projectStatusPaths = resolveProjectStatusPaths(
3548
+ docsHasUncommittedChanges = docsStatus.trim().length > 0;
3549
+ }
3550
+ }
3551
+ if (typeof context.docsEverCommitted !== "boolean") {
3552
+ const docsLastCommit = getLastCommitForPath(
3553
+ context.docsGitCwd,
3554
+ normalizedFeaturePathFromDocs
3555
+ );
3556
+ docsEverCommitted = !!docsLastCommit;
3557
+ }
3558
+ let projectHasUncommittedChanges = typeof context.projectHasUncommittedChanges === "boolean" ? context.projectHasUncommittedChanges : false;
3559
+ let projectStatusUnavailable = !!context.projectStatusUnavailable;
3560
+ if (typeof context.projectHasUncommittedChanges !== "boolean" && context.projectGitCwd) {
3561
+ const dirtyScopePolicy = resolveCodeDirtyScopePolicy(options.workflow, options.projectType);
3562
+ const projectCacheKey = JSON.stringify({
3563
+ projectGitCwd: context.projectGitCwd,
3564
+ docsDir: context.docsDir,
3565
+ type,
3566
+ dirtyScopePolicy,
3567
+ componentPaths: options.workflow?.componentPaths?.[type] || []
3568
+ });
3569
+ const cachedStatus = PROJECT_DIRTY_STATUS_CACHE.get(projectCacheKey);
3570
+ if (cachedStatus) {
3571
+ projectHasUncommittedChanges = cachedStatus.hasUncommittedChanges;
3572
+ projectStatusUnavailable = cachedStatus.statusUnavailable;
3573
+ } else {
3574
+ let projectStatusPaths = [];
3575
+ if (dirtyScopePolicy === "component" && type !== "single") {
3576
+ const componentStatusPaths = await resolveComponentStatusPaths(
3577
+ context.projectGitCwd,
3578
+ type,
3579
+ options.workflow
3580
+ );
3581
+ projectStatusPaths = componentStatusPaths.length > 0 ? componentStatusPaths : resolveProjectStatusPaths(context.projectGitCwd, context.docsDir);
3582
+ } else {
3583
+ projectStatusPaths = resolveProjectStatusPaths(
3584
+ context.projectGitCwd,
3585
+ context.docsDir
3586
+ );
3587
+ }
3588
+ const projectStatus = getGitStatusPorcelain(
3472
3589
  context.projectGitCwd,
3473
- context.docsDir
3590
+ projectStatusPaths
3474
3591
  );
3592
+ projectStatusUnavailable = projectStatus === void 0;
3593
+ projectHasUncommittedChanges = projectStatus === void 0 ? false : projectStatus.trim().length > 0;
3594
+ PROJECT_DIRTY_STATUS_CACHE.set(projectCacheKey, {
3595
+ hasUncommittedChanges: projectHasUncommittedChanges,
3596
+ statusUnavailable: projectStatusUnavailable
3597
+ });
3475
3598
  }
3476
3599
  }
3477
- const projectStatus = context.projectGitCwd ? getGitStatusPorcelain(context.projectGitCwd, projectStatusPaths) : void 0;
3478
- const projectHasUncommittedChanges = projectStatus === void 0 ? false : projectStatus.trim().length > 0;
3479
- const docsLastCommit = getLastCommitForPath(
3480
- context.docsGitCwd,
3481
- relativeFeaturePathFromDocs
3482
- );
3483
- const docsEverCommitted = !!docsLastCommit;
3484
- if (docsStatus === void 0) {
3600
+ if (docsGitUnavailable) {
3485
3601
  warnings.push(tr(lang, "warnings", "docsGitUnavailable"));
3486
3602
  }
3487
3603
  if (docsPathIgnored === true) {
3488
3604
  warnings.push(
3489
3605
  tr(lang, "warnings", "docsPathIgnored", {
3490
- path: relativeFeaturePathFromDocs
3606
+ path: normalizedFeaturePathFromDocs
3491
3607
  })
3492
3608
  );
3493
3609
  }
@@ -3592,90 +3708,181 @@ async function parseFeature(featurePath, type, context, options) {
3592
3708
  );
3593
3709
  return { ...featureState, currentStep, actions, nextAction, warnings };
3594
3710
  }
3711
+ function normalizeRelPath(value) {
3712
+ return value.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
3713
+ }
3714
+ function parsePorcelainChangedPaths(porcelain) {
3715
+ const changed = [];
3716
+ for (const rawLine of porcelain.split("\n")) {
3717
+ if (!rawLine.trim()) continue;
3718
+ const payload = rawLine.slice(3).trim();
3719
+ if (!payload) continue;
3720
+ const pathCandidate = payload.includes(" -> ") ? payload.split(" -> ").at(-1) || "" : payload;
3721
+ const normalized = normalizeRelPath(pathCandidate.replace(/^"+|"+$/g, ""));
3722
+ if (!normalized) continue;
3723
+ changed.push(normalized);
3724
+ }
3725
+ return changed;
3726
+ }
3727
+ function findFeaturePathPrefix(normalizedPath, relativeFeaturePaths) {
3728
+ for (const featurePath of relativeFeaturePaths) {
3729
+ if (normalizedPath === featurePath) return featurePath;
3730
+ if (normalizedPath.startsWith(`${featurePath}/`)) return featurePath;
3731
+ const nestedPrefix = `/${featurePath}`;
3732
+ if (normalizedPath.endsWith(nestedPrefix)) return featurePath;
3733
+ if (normalizedPath.includes(`${nestedPrefix}/`)) return featurePath;
3734
+ }
3735
+ return void 0;
3736
+ }
3737
+ function buildDefaultDocsFeatureGitMeta(relativeFeaturePaths) {
3738
+ const map = /* @__PURE__ */ new Map();
3739
+ for (const featurePath of relativeFeaturePaths) {
3740
+ map.set(featurePath, {
3741
+ docsPathIgnored: false,
3742
+ docsHasUncommittedChanges: false,
3743
+ docsEverCommitted: false,
3744
+ docsGitUnavailable: false
3745
+ });
3746
+ }
3747
+ return map;
3748
+ }
3749
+ function buildDocsFeatureGitMeta(docsGitCwd, relativeFeaturePaths) {
3750
+ const normalizedFeaturePaths = relativeFeaturePaths.map(
3751
+ (value) => normalizeRelPath(value)
3752
+ );
3753
+ const map = buildDefaultDocsFeatureGitMeta(normalizedFeaturePaths);
3754
+ if (normalizedFeaturePaths.length === 0) return map;
3755
+ const docsStatus = getGitStatusPorcelain(docsGitCwd, normalizedFeaturePaths);
3756
+ if (docsStatus === void 0) {
3757
+ for (const featurePath of normalizedFeaturePaths) {
3758
+ const current = map.get(featurePath);
3759
+ if (!current) continue;
3760
+ current.docsGitUnavailable = true;
3761
+ current.docsHasUncommittedChanges = true;
3762
+ }
3763
+ } else {
3764
+ const changedPaths = parsePorcelainChangedPaths(docsStatus);
3765
+ for (const changedPath of changedPaths) {
3766
+ const featurePath = findFeaturePathPrefix(changedPath, normalizedFeaturePaths);
3767
+ if (!featurePath) continue;
3768
+ const current = map.get(featurePath);
3769
+ if (!current) continue;
3770
+ current.docsHasUncommittedChanges = true;
3771
+ }
3772
+ }
3773
+ const trackedPaths = getTrackedGitPaths(docsGitCwd, normalizedFeaturePaths);
3774
+ if (trackedPaths) {
3775
+ for (const trackedPath of trackedPaths) {
3776
+ const featurePath = findFeaturePathPrefix(
3777
+ normalizeRelPath(trackedPath),
3778
+ normalizedFeaturePaths
3779
+ );
3780
+ if (!featurePath) continue;
3781
+ const current = map.get(featurePath);
3782
+ if (!current) continue;
3783
+ current.docsEverCommitted = true;
3784
+ }
3785
+ }
3786
+ const ignoredPaths = getIgnoredGitPaths(docsGitCwd, normalizedFeaturePaths);
3787
+ if (ignoredPaths) {
3788
+ for (const ignoredPath of ignoredPaths) {
3789
+ const featurePath = findFeaturePathPrefix(
3790
+ normalizeRelPath(ignoredPath),
3791
+ normalizedFeaturePaths
3792
+ );
3793
+ if (!featurePath) continue;
3794
+ const current = map.get(featurePath);
3795
+ if (!current) continue;
3796
+ current.docsPathIgnored = true;
3797
+ }
3798
+ }
3799
+ return map;
3800
+ }
3595
3801
  async function scanFeatures(config) {
3596
3802
  const features = [];
3597
3803
  const warnings = [];
3598
3804
  const stepDefinitions = getStepDefinitions(config.lang, config.workflow);
3599
3805
  const docsBranch = getCurrentBranch(config.docsDir);
3600
3806
  const projectBranches = {};
3807
+ const projectGitCwds = {};
3601
3808
  let singleProject;
3602
3809
  if (config.projectType === "single") {
3603
3810
  singleProject = resolveProjectGitCwd(config, "single", config.lang);
3604
3811
  if (singleProject.warning) warnings.push(singleProject.warning);
3605
3812
  projectBranches.single = singleProject.cwd ? getCurrentBranch(singleProject.cwd) : "";
3813
+ projectGitCwds.single = singleProject.cwd ?? void 0;
3606
3814
  } else {
3607
3815
  const components = resolveProjectComponents(config.projectType, config.components);
3608
3816
  for (const component of components) {
3609
3817
  const project = resolveProjectGitCwd(config, component, config.lang);
3610
3818
  if (project.warning) warnings.push(project.warning);
3611
3819
  projectBranches[component] = project.cwd ? getCurrentBranch(project.cwd) : "";
3820
+ projectGitCwds[component] = project.cwd ?? void 0;
3612
3821
  }
3613
3822
  }
3823
+ const allFeatureDirs = [];
3824
+ const componentFeatureDirs = /* @__PURE__ */ new Map();
3614
3825
  if (config.projectType === "single") {
3615
3826
  const featureDirs = await glob("features/*/", {
3616
3827
  cwd: config.docsDir,
3617
3828
  absolute: true,
3618
3829
  ignore: ["**/feature-base/**"]
3619
3830
  });
3620
- for (const dir of featureDirs) {
3621
- if ((await fs15.stat(dir)).isDirectory()) {
3622
- features.push(
3623
- await parseFeature(
3624
- dir,
3625
- "single",
3626
- {
3627
- projectBranch: projectBranches.single,
3628
- docsBranch,
3629
- docsGitCwd: config.docsDir,
3630
- projectGitCwd: singleProject?.cwd ?? void 0,
3631
- docsDir: config.docsDir,
3632
- projectBranchAvailable: Boolean(singleProject?.cwd)
3633
- },
3634
- {
3635
- lang: config.lang,
3636
- stepDefinitions,
3637
- approval: config.approval,
3638
- workflow: config.workflow,
3639
- projectType: config.projectType
3640
- }
3641
- )
3642
- );
3643
- }
3644
- }
3831
+ componentFeatureDirs.set("single", featureDirs);
3832
+ allFeatureDirs.push(...featureDirs);
3645
3833
  } else {
3646
3834
  const components = resolveProjectComponents(config.projectType, config.components);
3647
3835
  for (const component of components) {
3648
- const project = resolveProjectGitCwd(config, component, config.lang);
3649
3836
  const componentDirs = await glob(`features/${component}/*/`, {
3650
3837
  cwd: config.docsDir,
3651
3838
  absolute: true
3652
3839
  });
3653
- for (const dir of componentDirs) {
3654
- if (!(await fs15.stat(dir)).isDirectory()) continue;
3655
- features.push(
3656
- await parseFeature(
3657
- dir,
3658
- component,
3659
- {
3660
- projectBranch: projectBranches[component] || "",
3661
- docsBranch,
3662
- docsGitCwd: config.docsDir,
3663
- projectGitCwd: project.cwd ?? void 0,
3664
- docsDir: config.docsDir,
3665
- projectBranchAvailable: Boolean(project.cwd)
3666
- },
3667
- {
3668
- lang: config.lang,
3669
- stepDefinitions,
3670
- approval: config.approval,
3671
- workflow: config.workflow,
3672
- projectType: config.projectType
3673
- }
3674
- )
3675
- );
3676
- }
3840
+ componentFeatureDirs.set(component, componentDirs);
3841
+ allFeatureDirs.push(...componentDirs);
3677
3842
  }
3678
3843
  }
3844
+ const relativeFeaturePaths = allFeatureDirs.map(
3845
+ (dir) => normalizeRelPath(path18.relative(config.docsDir, dir))
3846
+ );
3847
+ const docsGitMeta = buildDocsFeatureGitMeta(config.docsDir, relativeFeaturePaths);
3848
+ const parseTargets = config.projectType === "single" ? [{ type: "single", dirs: componentFeatureDirs.get("single") || [] }] : resolveProjectComponents(config.projectType, config.components).map((component) => ({
3849
+ type: component,
3850
+ dirs: componentFeatureDirs.get(component) || []
3851
+ }));
3852
+ for (const target of parseTargets) {
3853
+ const parsed = await Promise.all(
3854
+ target.dirs.map(async (dir) => {
3855
+ const relativeFeaturePathFromDocs = normalizeRelPath(
3856
+ path18.relative(config.docsDir, dir)
3857
+ );
3858
+ const docsMeta = docsGitMeta.get(relativeFeaturePathFromDocs);
3859
+ return parseFeature(
3860
+ dir,
3861
+ target.type,
3862
+ {
3863
+ projectBranch: projectBranches[target.type] || "",
3864
+ docsBranch,
3865
+ docsGitCwd: config.docsDir,
3866
+ projectGitCwd: projectGitCwds[target.type],
3867
+ docsDir: config.docsDir,
3868
+ projectBranchAvailable: Boolean(projectGitCwds[target.type]),
3869
+ docsPathIgnored: docsMeta?.docsPathIgnored,
3870
+ docsHasUncommittedChanges: docsMeta?.docsHasUncommittedChanges,
3871
+ docsEverCommitted: docsMeta?.docsEverCommitted,
3872
+ docsGitUnavailable: docsMeta?.docsGitUnavailable
3873
+ },
3874
+ {
3875
+ lang: config.lang,
3876
+ stepDefinitions,
3877
+ approval: config.approval,
3878
+ workflow: config.workflow,
3879
+ projectType: config.projectType
3880
+ }
3881
+ );
3882
+ })
3883
+ );
3884
+ features.push(...parsed);
3885
+ }
3679
3886
  return {
3680
3887
  features,
3681
3888
  branches: {
@@ -3715,13 +3922,13 @@ async function runStatus(options) {
3715
3922
  );
3716
3923
  }
3717
3924
  const { docsDir, projectType, projectName, lang } = config;
3718
- const featuresDir = path17.join(docsDir, "features");
3925
+ const featuresDir = path18.join(docsDir, "features");
3719
3926
  const scan = await scanFeatures(config);
3720
3927
  const features = [];
3721
3928
  const idMap = /* @__PURE__ */ new Map();
3722
3929
  for (const f of scan.features) {
3723
3930
  const id = f.id || "UNKNOWN";
3724
- const relPath = path17.relative(docsDir, f.path);
3931
+ const relPath = path18.relative(docsDir, f.path);
3725
3932
  if (!idMap.has(id)) idMap.set(id, []);
3726
3933
  idMap.get(id).push(relPath);
3727
3934
  if (!f.docs.specExists || !f.docs.tasksExists) continue;
@@ -3802,7 +4009,7 @@ async function runStatus(options) {
3802
4009
  }
3803
4010
  console.log();
3804
4011
  if (options.write) {
3805
- const outputPath = path17.join(featuresDir, "status.md");
4012
+ const outputPath = path18.join(featuresDir, "status.md");
3806
4013
  const date = getLocalDateString();
3807
4014
  const content = [
3808
4015
  "# Feature Status",
@@ -3817,7 +4024,7 @@ async function runStatus(options) {
3817
4024
  ),
3818
4025
  ""
3819
4026
  ].join("\n");
3820
- await fs15.writeFile(outputPath, content, "utf-8");
4027
+ await fs14.writeFile(outputPath, content, "utf-8");
3821
4028
  console.log(
3822
4029
  chalk6.green(
3823
4030
  tr(lang, "cli", "status.wrote", { path: outputPath })
@@ -3830,9 +4037,9 @@ function escapeRegExp2(value) {
3830
4037
  }
3831
4038
  async function getFeatureNameFromSpec(featureDir, fallbackSlug, fallbackFolderName) {
3832
4039
  try {
3833
- const specPath = path17.join(featureDir, "spec.md");
3834
- if (!await fs15.pathExists(specPath)) return fallbackSlug;
3835
- const content = await fs15.readFile(specPath, "utf-8");
4040
+ const specPath = path18.join(featureDir, "spec.md");
4041
+ if (!await fs14.pathExists(specPath)) return fallbackSlug;
4042
+ const content = await fs14.readFile(specPath, "utf-8");
3836
4043
  const keys = ["\uAE30\uB2A5\uBA85", "Feature Name"];
3837
4044
  for (const key of keys) {
3838
4045
  const regex = new RegExp(
@@ -3911,17 +4118,17 @@ async function runUpdate(options) {
3911
4118
  console.log(chalk6.blue(tr(lang, "cli", "update.updatingAgents")));
3912
4119
  }
3913
4120
  if (agentsMode === "all") {
3914
- const commonAgentsBase = path17.join(templatesDir, lang, "common", "agents");
3915
- const typeAgentsBase = path17.join(
4121
+ const commonAgentsBase = path18.join(templatesDir, lang, "common", "agents");
4122
+ const typeAgentsBase = path18.join(
3916
4123
  templatesDir,
3917
4124
  lang,
3918
4125
  toTemplateProjectType(projectType),
3919
4126
  "agents"
3920
4127
  );
3921
- const targetAgentsBase = path17.join(docsDir, "agents");
3922
- const commonAgents = agentsMode === "skills" ? path17.join(commonAgentsBase, "skills") : commonAgentsBase;
3923
- const typeAgents = agentsMode === "skills" ? path17.join(typeAgentsBase, "skills") : typeAgentsBase;
3924
- const targetAgents = agentsMode === "skills" ? path17.join(targetAgentsBase, "skills") : targetAgentsBase;
4128
+ const targetAgentsBase = path18.join(docsDir, "agents");
4129
+ const commonAgents = agentsMode === "skills" ? path18.join(commonAgentsBase, "skills") : commonAgentsBase;
4130
+ const typeAgents = agentsMode === "skills" ? path18.join(typeAgentsBase, "skills") : typeAgentsBase;
4131
+ const targetAgents = agentsMode === "skills" ? path18.join(targetAgentsBase, "skills") : targetAgentsBase;
3925
4132
  const featurePath = projectType === "multi" ? isDefaultFullstackComponents(config.components || []) ? "docs/features/{be|fe}" : "docs/features/{component}" : "docs/features";
3926
4133
  const projectName = config.projectName ?? "{{projectName}}";
3927
4134
  const commonReplacements = {
@@ -3931,7 +4138,7 @@ async function runUpdate(options) {
3931
4138
  const typeReplacements = {
3932
4139
  "{{projectName}}": projectName
3933
4140
  };
3934
- if (await fs15.pathExists(commonAgents)) {
4141
+ if (await fs14.pathExists(commonAgents)) {
3935
4142
  const count = await updateFolder(
3936
4143
  commonAgents,
3937
4144
  targetAgents,
@@ -3949,7 +4156,7 @@ async function runUpdate(options) {
3949
4156
  );
3950
4157
  updatedCount += count;
3951
4158
  }
3952
- if (await fs15.pathExists(typeAgents)) {
4159
+ if (await fs14.pathExists(typeAgents)) {
3953
4160
  const count = await updateFolder(
3954
4161
  typeAgents,
3955
4162
  targetAgents,
@@ -3999,24 +4206,24 @@ async function runUpdate(options) {
3999
4206
  async function updateFolder(sourceDir, targetDir, force, replacements, lang = DEFAULT_LANG, options = {}) {
4000
4207
  const protectedFiles = options.protectedFiles ?? /* @__PURE__ */ new Set(["custom.md", "constitution.md"]);
4001
4208
  const skipDirectories = options.skipDirectories ?? /* @__PURE__ */ new Set();
4002
- await fs15.ensureDir(targetDir);
4003
- const files = await fs15.readdir(sourceDir);
4209
+ await fs14.ensureDir(targetDir);
4210
+ const files = await fs14.readdir(sourceDir);
4004
4211
  let updatedCount = 0;
4005
4212
  for (const file of files) {
4006
- const sourcePath = path17.join(sourceDir, file);
4007
- const targetPath = path17.join(targetDir, file);
4008
- const stat = await fs15.stat(sourcePath);
4213
+ const sourcePath = path18.join(sourceDir, file);
4214
+ const targetPath = path18.join(targetDir, file);
4215
+ const stat = await fs14.stat(sourcePath);
4009
4216
  if (stat.isFile()) {
4010
4217
  if (protectedFiles.has(file)) {
4011
4218
  continue;
4012
4219
  }
4013
- let sourceContent = await fs15.readFile(sourcePath, "utf-8");
4220
+ let sourceContent = await fs14.readFile(sourcePath, "utf-8");
4014
4221
  if (replacements) {
4015
4222
  sourceContent = applyReplacements(sourceContent, replacements);
4016
4223
  }
4017
4224
  let shouldUpdate = true;
4018
- if (await fs15.pathExists(targetPath)) {
4019
- const targetContent = await fs15.readFile(targetPath, "utf-8");
4225
+ if (await fs14.pathExists(targetPath)) {
4226
+ const targetContent = await fs14.readFile(targetPath, "utf-8");
4020
4227
  if (sourceContent === targetContent) {
4021
4228
  continue;
4022
4229
  }
@@ -4030,7 +4237,7 @@ async function updateFolder(sourceDir, targetDir, force, replacements, lang = DE
4030
4237
  }
4031
4238
  }
4032
4239
  if (shouldUpdate) {
4033
- await fs15.writeFile(targetPath, sourceContent);
4240
+ await fs14.writeFile(targetPath, sourceContent);
4034
4241
  console.log(
4035
4242
  chalk6.gray(` \u{1F4C4} ${tr(lang, "cli", "update.fileUpdated", { file })}`)
4036
4243
  );
@@ -4087,7 +4294,7 @@ function extractPorcelainPaths(line) {
4087
4294
  function getDocsPorcelainStatus(docsDir, ignoredAbsPaths = []) {
4088
4295
  const top = getGitTopLevel2(docsDir);
4089
4296
  if (!top) return null;
4090
- const rel = path17.relative(top, docsDir) || ".";
4297
+ const rel = path18.relative(top, docsDir) || ".";
4091
4298
  try {
4092
4299
  const output = execFileSync("git", ["status", "--porcelain=v1", "--", rel], {
4093
4300
  cwd: top,
@@ -4099,7 +4306,7 @@ function getDocsPorcelainStatus(docsDir, ignoredAbsPaths = []) {
4099
4306
  }
4100
4307
  const ignoredRelPaths = new Set(
4101
4308
  ignoredAbsPaths.map(
4102
- (absPath) => normalizeGitPath2(path17.relative(top, absPath) || ".")
4309
+ (absPath) => normalizeGitPath2(path18.relative(top, absPath) || ".")
4103
4310
  )
4104
4311
  );
4105
4312
  const filtered = output.split("\n").filter((line) => {
@@ -4156,7 +4363,7 @@ ${tr(lang2, "cli", "common.canceled")}`));
4156
4363
  }
4157
4364
  async function runConfig(options) {
4158
4365
  const cwd = process.cwd();
4159
- const targetCwd = options.dir ? path17.resolve(cwd, options.dir) : cwd;
4366
+ const targetCwd = options.dir ? path18.resolve(cwd, options.dir) : cwd;
4160
4367
  const config = await getConfig(targetCwd);
4161
4368
  if (!config) {
4162
4369
  throw createCliError(
@@ -4164,7 +4371,7 @@ async function runConfig(options) {
4164
4371
  tr(DEFAULT_LANG, "cli", "common.configNotFound")
4165
4372
  );
4166
4373
  }
4167
- const configPath = path17.join(config.docsDir, ".lee-spec-kit.json");
4374
+ const configPath = path18.join(config.docsDir, ".lee-spec-kit.json");
4168
4375
  if (!options.projectRoot) {
4169
4376
  console.log();
4170
4377
  console.log(chalk6.blue(tr(config.lang, "cli", "config.currentTitle")));
@@ -4175,7 +4382,7 @@ async function runConfig(options) {
4175
4382
  )
4176
4383
  );
4177
4384
  console.log();
4178
- const configFile = await fs15.readJson(configPath);
4385
+ const configFile = await fs14.readJson(configPath);
4179
4386
  console.log(JSON.stringify(configFile, null, 2));
4180
4387
  console.log();
4181
4388
  return;
@@ -4183,7 +4390,7 @@ async function runConfig(options) {
4183
4390
  await withFileLock(
4184
4391
  getDocsLockPath(config.docsDir),
4185
4392
  async () => {
4186
- const configFile = await fs15.readJson(configPath);
4393
+ const configFile = await fs14.readJson(configPath);
4187
4394
  if (configFile.docsRepo !== "standalone") {
4188
4395
  console.log(
4189
4396
  chalk6.yellow(tr(config.lang, "cli", "config.projectRootStandaloneOnly"))
@@ -4262,7 +4469,7 @@ async function runConfig(options) {
4262
4469
  )
4263
4470
  );
4264
4471
  }
4265
- await fs15.writeJson(configPath, configFile, { spaces: 2 });
4472
+ await fs14.writeJson(configPath, configFile, { spaces: 2 });
4266
4473
  console.log();
4267
4474
  },
4268
4475
  { owner: "config" }
@@ -4516,42 +4723,42 @@ var BUILTIN_DOC_DEFINITIONS = [
4516
4723
  {
4517
4724
  id: "agents",
4518
4725
  title: { ko: "\uC5D0\uC774\uC804\uD2B8 \uC6B4\uC601 \uADDC\uCE59", en: "Agent Operating Rules" },
4519
- relativePath: (projectType, lang) => path17.join(lang, toTemplateProjectType(projectType), "agents", "agents.md")
4726
+ relativePath: (projectType, lang) => path18.join(lang, toTemplateProjectType(projectType), "agents", "agents.md")
4520
4727
  },
4521
4728
  {
4522
4729
  id: "git-workflow",
4523
4730
  title: { ko: "Git \uC6CC\uD06C\uD50C\uB85C\uC6B0", en: "Git Workflow" },
4524
- relativePath: (_, lang) => path17.join(lang, "common", "agents", "git-workflow.md")
4731
+ relativePath: (_, lang) => path18.join(lang, "common", "agents", "git-workflow.md")
4525
4732
  },
4526
4733
  {
4527
4734
  id: "issue-template",
4528
4735
  title: { ko: "Issue \uD15C\uD50C\uB9BF", en: "Issue Template" },
4529
- relativePath: (_, lang) => path17.join(lang, "common", "agents", "issue-template.md")
4736
+ relativePath: (_, lang) => path18.join(lang, "common", "agents", "issue-template.md")
4530
4737
  },
4531
4738
  {
4532
4739
  id: "pr-template",
4533
4740
  title: { ko: "PR \uD15C\uD50C\uB9BF", en: "PR Template" },
4534
- relativePath: (_, lang) => path17.join(lang, "common", "agents", "pr-template.md")
4741
+ relativePath: (_, lang) => path18.join(lang, "common", "agents", "pr-template.md")
4535
4742
  },
4536
4743
  {
4537
4744
  id: "create-feature",
4538
4745
  title: { ko: "create-feature \uC2A4\uD0AC", en: "create-feature skill" },
4539
- relativePath: (_, lang) => path17.join(lang, "common", "agents", "skills", "create-feature.md")
4746
+ relativePath: (_, lang) => path18.join(lang, "common", "agents", "skills", "create-feature.md")
4540
4747
  },
4541
4748
  {
4542
4749
  id: "execute-task",
4543
4750
  title: { ko: "execute-task \uC2A4\uD0AC", en: "execute-task skill" },
4544
- relativePath: (_, lang) => path17.join(lang, "common", "agents", "skills", "execute-task.md")
4751
+ relativePath: (_, lang) => path18.join(lang, "common", "agents", "skills", "execute-task.md")
4545
4752
  },
4546
4753
  {
4547
4754
  id: "create-issue",
4548
4755
  title: { ko: "create-issue \uC2A4\uD0AC", en: "create-issue skill" },
4549
- relativePath: (_, lang) => path17.join(lang, "common", "agents", "skills", "create-issue.md")
4756
+ relativePath: (_, lang) => path18.join(lang, "common", "agents", "skills", "create-issue.md")
4550
4757
  },
4551
4758
  {
4552
4759
  id: "create-pr",
4553
4760
  title: { ko: "create-pr \uC2A4\uD0AC", en: "create-pr skill" },
4554
- relativePath: (_, lang) => path17.join(lang, "common", "agents", "skills", "create-pr.md")
4761
+ relativePath: (_, lang) => path18.join(lang, "common", "agents", "skills", "create-pr.md")
4555
4762
  }
4556
4763
  ];
4557
4764
  var DOC_FOLLOWUPS = {
@@ -4635,7 +4842,7 @@ function listBuiltinDocs(projectType, lang) {
4635
4842
  id: doc.id,
4636
4843
  title: doc.title[lang],
4637
4844
  relativePath,
4638
- absolutePath: path17.join(templatesDir, relativePath)
4845
+ absolutePath: path18.join(templatesDir, relativePath)
4639
4846
  };
4640
4847
  });
4641
4848
  }
@@ -4644,7 +4851,7 @@ async function getBuiltinDoc(docId, projectType, lang) {
4644
4851
  if (!entry) {
4645
4852
  throw new Error(`Unknown builtin doc: ${docId}`);
4646
4853
  }
4647
- const content = await fs15.readFile(entry.absolutePath, "utf-8");
4854
+ const content = await fs14.readFile(entry.absolutePath, "utf-8");
4648
4855
  const hash = createHash("sha256").update(content).digest("hex").slice(0, 12);
4649
4856
  return {
4650
4857
  entry,
@@ -4673,6 +4880,25 @@ function listLabels(actionOptions) {
4673
4880
  if (actionOptions.length === 0) return "-";
4674
4881
  return actionOptions.map((o) => o.label).join(", ");
4675
4882
  }
4883
+ function resolveFeatureRefForApproval(state, featureName) {
4884
+ const raw = featureName?.trim() || state.matchedFeature?.folderName || "<slug|F001|F001-slug>";
4885
+ return raw;
4886
+ }
4887
+ function buildApprovalCommand(state, featureName, selectedComponent, execute) {
4888
+ const featureRef = resolveFeatureRefForApproval(state, featureName);
4889
+ const componentArg = selectedComponent ? ` --component ${selectedComponent}` : "";
4890
+ const executeArg = execute ? " --execute" : "";
4891
+ return `npx lee-spec-kit context ${featureRef}${componentArg} --approve <LABEL>${executeArg}`;
4892
+ }
4893
+ function buildFinalApprovalPrompt(lang, actionOptions) {
4894
+ if (actionOptions.length === 0) return "";
4895
+ const labels = listLabels(actionOptions);
4896
+ const example = actionOptions[0]?.label || "A";
4897
+ return tr(lang, "cli", "context.finalLabelPrompt", {
4898
+ labels,
4899
+ example
4900
+ });
4901
+ }
4676
4902
  function formatActionSummary2(action) {
4677
4903
  if (action.type === "command") {
4678
4904
  return `(${action.scope}) ${action.cmd}`;
@@ -4710,7 +4936,7 @@ function getCommandExecutionLockPath(action, config) {
4710
4936
  if (action.scope === "docs") {
4711
4937
  return getDocsLockPath(config.docsDir);
4712
4938
  }
4713
- return path17.join(action.cwd, ".lee-spec-kit.project.lock");
4939
+ return path18.join(action.cwd, ".lee-spec-kit.project.lock");
4714
4940
  }
4715
4941
  function contextCommand(program2) {
4716
4942
  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(
@@ -4848,6 +5074,19 @@ async function runContext(featureName, options) {
4848
5074
  }
4849
5075
  if (options.json) {
4850
5076
  const primaryAction = state.actionOptions[0] ?? null;
5077
+ const finalApprovalPrompt = buildFinalApprovalPrompt(lang, state.actionOptions);
5078
+ const approveCommand = buildApprovalCommand(
5079
+ state,
5080
+ featureName,
5081
+ selectedComponent,
5082
+ false
5083
+ );
5084
+ const executeCommand = buildApprovalCommand(
5085
+ state,
5086
+ featureName,
5087
+ selectedComponent,
5088
+ true
5089
+ );
4851
5090
  const result = {
4852
5091
  status: state.status,
4853
5092
  reasonCode: toReasonCode(state.status),
@@ -4888,6 +5127,10 @@ async function runContext(featureName, options) {
4888
5127
  },
4889
5128
  approvalRequest: {
4890
5129
  guidance: "Present each label with summary (e.g. `A: <summary>`) before asking for approval.",
5130
+ finalPrompt: finalApprovalPrompt,
5131
+ labels: state.actionOptions.map((o) => o.label),
5132
+ approveCommand,
5133
+ executeCommand,
4891
5134
  options: state.actionOptions.map((o) => ({
4892
5135
  label: o.label,
4893
5136
  summary: o.summary,
@@ -5087,7 +5330,7 @@ async function runContext(featureName, options) {
5087
5330
  if (f.issueNumber) {
5088
5331
  console.log(` \u2022 Issue: #${f.issueNumber}`);
5089
5332
  }
5090
- console.log(` \u2022 Path: ${path17.relative(cwd, f.path)}`);
5333
+ console.log(` \u2022 Path: ${path18.relative(cwd, f.path)}`);
5091
5334
  if (f.git.projectBranch) {
5092
5335
  console.log(` \u2022 Project Branch: ${f.git.projectBranch}`);
5093
5336
  }
@@ -5151,6 +5394,23 @@ async function runContext(featureName, options) {
5151
5394
  );
5152
5395
  }
5153
5396
  }
5397
+ if (actionOptions.length > 0) {
5398
+ const finalApprovalPrompt = buildFinalApprovalPrompt(lang, actionOptions);
5399
+ const executeCommand = buildApprovalCommand(
5400
+ state,
5401
+ featureName,
5402
+ selectedComponent,
5403
+ true
5404
+ );
5405
+ console.log(chalk6.cyan(` \u21B3 ${finalApprovalPrompt}`));
5406
+ console.log(
5407
+ chalk6.gray(
5408
+ ` \u21B3 ${tr(lang, "cli", "context.finalLabelCommandHint", {
5409
+ command: executeCommand
5410
+ })}`
5411
+ )
5412
+ );
5413
+ }
5154
5414
  console.log();
5155
5415
  }
5156
5416
  async function runApprovedOption(state, config, lang, featureName, selectionOptions, options) {
@@ -5324,7 +5584,7 @@ var FIXABLE_ISSUE_CODES = /* @__PURE__ */ new Set([
5324
5584
  ]);
5325
5585
  function formatPath(cwd, p) {
5326
5586
  if (!p) return "";
5327
- return path17.isAbsolute(p) ? path17.relative(cwd, p) : p;
5587
+ return path18.isAbsolute(p) ? path18.relative(cwd, p) : p;
5328
5588
  }
5329
5589
  function detectPlaceholders(content) {
5330
5590
  const patterns = [
@@ -5467,7 +5727,7 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
5467
5727
  const placeholderContext = {
5468
5728
  projectName: config.projectName,
5469
5729
  featureName: f.slug,
5470
- featurePath: f.docs.featurePathFromDocs || path17.relative(config.docsDir, f.path),
5730
+ featurePath: f.docs.featurePathFromDocs || path18.relative(config.docsDir, f.path),
5471
5731
  repoType: f.type,
5472
5732
  featureNumber
5473
5733
  };
@@ -5477,9 +5737,9 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
5477
5737
  "tasks.md"
5478
5738
  ];
5479
5739
  for (const file of files) {
5480
- const fullPath = path17.join(f.path, file);
5481
- if (!await fs15.pathExists(fullPath)) continue;
5482
- const original = await fs15.readFile(fullPath, "utf-8");
5740
+ const fullPath = path18.join(f.path, file);
5741
+ if (!await fs14.pathExists(fullPath)) continue;
5742
+ const original = await fs14.readFile(fullPath, "utf-8");
5483
5743
  let next = original;
5484
5744
  const changes = [];
5485
5745
  const placeholderFix = applyPlaceholderFixes(next, placeholderContext, config.lang);
@@ -5503,7 +5763,7 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
5503
5763
  }
5504
5764
  if (next === original) continue;
5505
5765
  if (!dryRun) {
5506
- await fs15.writeFile(fullPath, next, "utf-8");
5766
+ await fs14.writeFile(fullPath, next, "utf-8");
5507
5767
  }
5508
5768
  entries.push({
5509
5769
  path: formatPath(cwd, fullPath),
@@ -5522,8 +5782,8 @@ async function checkDocsStructure(config, cwd) {
5522
5782
  const issues = [];
5523
5783
  const requiredDirs = ["agents", "features", "prd", "designs", "ideas"];
5524
5784
  for (const dir of requiredDirs) {
5525
- const p = path17.join(config.docsDir, dir);
5526
- if (!await fs15.pathExists(p)) {
5785
+ const p = path18.join(config.docsDir, dir);
5786
+ if (!await fs14.pathExists(p)) {
5527
5787
  issues.push({
5528
5788
  level: "error",
5529
5789
  code: "missing_dir",
@@ -5532,8 +5792,8 @@ async function checkDocsStructure(config, cwd) {
5532
5792
  });
5533
5793
  }
5534
5794
  }
5535
- const configPath = path17.join(config.docsDir, ".lee-spec-kit.json");
5536
- if (!await fs15.pathExists(configPath)) {
5795
+ const configPath = path18.join(config.docsDir, ".lee-spec-kit.json");
5796
+ if (!await fs14.pathExists(configPath)) {
5537
5797
  issues.push({
5538
5798
  level: "warn",
5539
5799
  code: "missing_config",
@@ -5555,7 +5815,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
5555
5815
  }
5556
5816
  const idMap = /* @__PURE__ */ new Map();
5557
5817
  for (const f of features) {
5558
- const rel = f.docs.featurePathFromDocs || path17.relative(config.docsDir, f.path);
5818
+ const rel = f.docs.featurePathFromDocs || path18.relative(config.docsDir, f.path);
5559
5819
  const id = f.id || "UNKNOWN";
5560
5820
  if (!idMap.has(id)) idMap.set(id, []);
5561
5821
  idMap.get(id).push(rel);
@@ -5563,9 +5823,9 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
5563
5823
  if (!isInitialTemplateState) {
5564
5824
  const featureDocs = ["spec.md", "plan.md", "tasks.md"];
5565
5825
  for (const file of featureDocs) {
5566
- const p = path17.join(f.path, file);
5567
- if (!await fs15.pathExists(p)) continue;
5568
- const content = await fs15.readFile(p, "utf-8");
5826
+ const p = path18.join(f.path, file);
5827
+ if (!await fs14.pathExists(p)) continue;
5828
+ const content = await fs14.readFile(p, "utf-8");
5569
5829
  const placeholders = detectPlaceholders(content);
5570
5830
  if (placeholders.length === 0) continue;
5571
5831
  issues.push({
@@ -5578,9 +5838,9 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
5578
5838
  });
5579
5839
  }
5580
5840
  if (decisionsPlaceholderMode !== "off") {
5581
- const decisionsPath = path17.join(f.path, "decisions.md");
5582
- if (await fs15.pathExists(decisionsPath)) {
5583
- const content = await fs15.readFile(decisionsPath, "utf-8");
5841
+ const decisionsPath = path18.join(f.path, "decisions.md");
5842
+ if (await fs14.pathExists(decisionsPath)) {
5843
+ const content = await fs14.readFile(decisionsPath, "utf-8");
5584
5844
  const placeholders = detectPlaceholders(content);
5585
5845
  if (placeholders.length > 0) {
5586
5846
  issues.push({
@@ -5607,7 +5867,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
5607
5867
  level: "warn",
5608
5868
  code: "spec_status_unset",
5609
5869
  message: tr(config.lang, "cli", "doctor.issue.specStatusUnset"),
5610
- path: formatPath(cwd, path17.join(f.path, "spec.md"))
5870
+ path: formatPath(cwd, path18.join(f.path, "spec.md"))
5611
5871
  });
5612
5872
  }
5613
5873
  if (f.docs.planExists && !f.planStatus && !isInitialTemplateState) {
@@ -5615,7 +5875,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
5615
5875
  level: "warn",
5616
5876
  code: "plan_status_unset",
5617
5877
  message: tr(config.lang, "cli", "doctor.issue.planStatusUnset"),
5618
- path: formatPath(cwd, path17.join(f.path, "plan.md"))
5878
+ path: formatPath(cwd, path18.join(f.path, "plan.md"))
5619
5879
  });
5620
5880
  }
5621
5881
  if (f.docs.tasksExists && f.tasks.total === 0 && !isInitialTemplateState) {
@@ -5623,7 +5883,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
5623
5883
  level: "warn",
5624
5884
  code: "tasks_empty",
5625
5885
  message: tr(config.lang, "cli", "doctor.issue.tasksEmpty"),
5626
- path: formatPath(cwd, path17.join(f.path, "tasks.md"))
5886
+ path: formatPath(cwd, path18.join(f.path, "tasks.md"))
5627
5887
  });
5628
5888
  }
5629
5889
  if (f.docs.tasksExists && !f.docs.tasksDocStatusFieldExists && !isInitialTemplateState) {
@@ -5631,7 +5891,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
5631
5891
  level: "warn",
5632
5892
  code: "tasks_doc_status_missing",
5633
5893
  message: tr(config.lang, "cli", "doctor.issue.tasksDocStatusMissing"),
5634
- path: formatPath(cwd, path17.join(f.path, "tasks.md"))
5894
+ path: formatPath(cwd, path18.join(f.path, "tasks.md"))
5635
5895
  });
5636
5896
  }
5637
5897
  if (f.docs.tasksExists && f.docs.tasksDocStatusFieldExists && !f.tasksDocStatus && !isInitialTemplateState) {
@@ -5639,7 +5899,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
5639
5899
  level: "warn",
5640
5900
  code: "tasks_doc_status_unset",
5641
5901
  message: tr(config.lang, "cli", "doctor.issue.tasksDocStatusUnset"),
5642
- path: formatPath(cwd, path17.join(f.path, "tasks.md"))
5902
+ path: formatPath(cwd, path18.join(f.path, "tasks.md"))
5643
5903
  });
5644
5904
  }
5645
5905
  }
@@ -5663,7 +5923,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
5663
5923
  level: "warn",
5664
5924
  code: "missing_feature_id",
5665
5925
  message: tr(config.lang, "cli", "doctor.issue.missingFeatureId"),
5666
- path: formatPath(cwd, path17.join(config.docsDir, p))
5926
+ path: formatPath(cwd, path18.join(config.docsDir, p))
5667
5927
  });
5668
5928
  }
5669
5929
  return issues;
@@ -5785,7 +6045,7 @@ function doctorCommand(program2) {
5785
6045
  }
5786
6046
  console.log();
5787
6047
  console.log(chalk6.bold(tr(lang, "cli", "doctor.title")));
5788
- console.log(chalk6.gray(`- Docs: ${path17.relative(cwd, docsDir)}`));
6048
+ console.log(chalk6.gray(`- Docs: ${path18.relative(cwd, docsDir)}`));
5789
6049
  console.log(chalk6.gray(`- Type: ${projectType}`));
5790
6050
  console.log(chalk6.gray(`- Lang: ${lang}`));
5791
6051
  console.log();
@@ -5964,7 +6224,7 @@ async function runView(featureName, options) {
5964
6224
  }
5965
6225
  console.log();
5966
6226
  console.log(chalk6.bold("\u{1F4CA} Workflow View"));
5967
- console.log(chalk6.gray(`- Docs: ${path17.relative(cwd, config.docsDir)}`));
6227
+ console.log(chalk6.gray(`- Docs: ${path18.relative(cwd, config.docsDir)}`));
5968
6228
  console.log(
5969
6229
  chalk6.gray(
5970
6230
  `- Features: ${state.features.length} (open ${state.openFeatures.length} / done ${state.doneFeatures.length})`
@@ -6245,45 +6505,109 @@ async function runFlow(featureName, options) {
6245
6505
  console.log(chalk6.gray("Tip: add --approve <LABEL> [--execute] to run the selected atomic action."));
6246
6506
  console.log();
6247
6507
  }
6508
+
6509
+ // src/utils/github-draft-contract.ts
6510
+ var ISSUE_CONTRACT = {
6511
+ kind: "issue",
6512
+ requiredSections: {
6513
+ ko: ["\uAC1C\uC694", "\uBAA9\uD45C", "\uC644\uB8CC \uAE30\uC900", "\uAD00\uB828 \uBB38\uC11C", "\uB77C\uBCA8"],
6514
+ en: ["Overview", "Goals", "Completion Criteria", "Related Documents", "Labels"]
6515
+ },
6516
+ artifacts: []
6517
+ };
6518
+ var PR_CONTRACT = {
6519
+ kind: "pr",
6520
+ requiredSections: {
6521
+ ko: ["\uAC1C\uC694", "\uBCC0\uACBD \uC0AC\uD56D", "\uD14C\uC2A4\uD2B8", "\uAD00\uB828 \uBB38\uC11C"],
6522
+ en: ["Overview", "Changes", "Tests", "Related Documents"]
6523
+ },
6524
+ artifacts: [
6525
+ {
6526
+ id: "screenshots",
6527
+ headings: { ko: "\uC2A4\uD06C\uB9B0\uC0F7", en: "Screenshots" },
6528
+ bodyRule: "image-markdown"
6529
+ },
6530
+ {
6531
+ id: "mermaid",
6532
+ headings: { ko: "\uC544\uD0A4\uD14D\uCC98 \uB2E4\uC774\uC5B4\uADF8\uB7A8", en: "Architecture Diagram" },
6533
+ bodyRule: "mermaid-fence"
6534
+ }
6535
+ ]
6536
+ };
6537
+ var CONTRACTS = {
6538
+ issue: ISSUE_CONTRACT,
6539
+ pr: PR_CONTRACT
6540
+ };
6541
+ function getGithubDraftRequiredSections(kind, lang) {
6542
+ return [...CONTRACTS[kind].requiredSections[lang]];
6543
+ }
6544
+ function getGithubDraftArtifactHeading(kind, artifactId, lang) {
6545
+ const artifact = CONTRACTS[kind].artifacts.find((item) => item.id === artifactId);
6546
+ if (!artifact) return null;
6547
+ return artifact.headings[lang];
6548
+ }
6549
+ function getGithubDraftContractView(kind, lang) {
6550
+ const definition = CONTRACTS[kind];
6551
+ return {
6552
+ kind: definition.kind,
6553
+ requiredSections: [...definition.requiredSections[lang]],
6554
+ artifacts: definition.artifacts.map((artifact) => ({
6555
+ id: artifact.id,
6556
+ section: artifact.headings[lang],
6557
+ bodyRule: artifact.bodyRule
6558
+ }))
6559
+ };
6560
+ }
6561
+ function getGithubDraftContractForBuiltinDoc(docId, lang) {
6562
+ if (docId === "create-issue" || docId === "issue-template") {
6563
+ return getGithubDraftContractView("issue", lang);
6564
+ }
6565
+ if (docId === "create-pr" || docId === "pr-template") {
6566
+ return getGithubDraftContractView("pr", lang);
6567
+ }
6568
+ return null;
6569
+ }
6570
+
6571
+ // src/commands/github.ts
6248
6572
  function tg(lang, key, vars = {}) {
6249
6573
  return tr(lang, "cli", `github.${key}`, vars);
6250
6574
  }
6251
6575
  function detectGithubCliLangSync(cwd) {
6252
6576
  const explicitDocsDir = (process.env.LEE_SPEC_KIT_DOCS_DIR || "").trim();
6253
- const startDirs = [explicitDocsDir ? path17.resolve(explicitDocsDir) : "", path17.resolve(cwd)].filter(Boolean);
6577
+ const startDirs = [explicitDocsDir ? path18.resolve(explicitDocsDir) : "", path18.resolve(cwd)].filter(Boolean);
6254
6578
  const scanOrder = [];
6255
6579
  const seen = /* @__PURE__ */ new Set();
6256
6580
  for (const start of startDirs) {
6257
6581
  let current = start;
6258
6582
  while (true) {
6259
- const abs = path17.resolve(current);
6583
+ const abs = path18.resolve(current);
6260
6584
  if (!seen.has(abs)) {
6261
6585
  scanOrder.push(abs);
6262
6586
  seen.add(abs);
6263
6587
  }
6264
- const parent = path17.dirname(abs);
6588
+ const parent = path18.dirname(abs);
6265
6589
  if (parent === abs) break;
6266
6590
  current = parent;
6267
6591
  }
6268
6592
  }
6269
6593
  for (const base of scanOrder) {
6270
- for (const docsDir of [path17.join(base, "docs"), base]) {
6271
- const configPath = path17.join(docsDir, ".lee-spec-kit.json");
6272
- if (fs15.existsSync(configPath)) {
6594
+ for (const docsDir of [path18.join(base, "docs"), base]) {
6595
+ const configPath = path18.join(docsDir, ".lee-spec-kit.json");
6596
+ if (fs14.existsSync(configPath)) {
6273
6597
  try {
6274
- const parsed = fs15.readJsonSync(configPath);
6598
+ const parsed = fs14.readJsonSync(configPath);
6275
6599
  if (parsed?.lang === "ko" || parsed?.lang === "en") return parsed.lang;
6276
6600
  } catch {
6277
6601
  }
6278
6602
  }
6279
- const agentsPath = path17.join(docsDir, "agents");
6280
- const featuresPath = path17.join(docsDir, "features");
6281
- if (!fs15.existsSync(agentsPath) || !fs15.existsSync(featuresPath)) continue;
6603
+ const agentsPath = path18.join(docsDir, "agents");
6604
+ const featuresPath = path18.join(docsDir, "features");
6605
+ if (!fs14.existsSync(agentsPath) || !fs14.existsSync(featuresPath)) continue;
6282
6606
  for (const probe of ["custom.md", "constitution.md", "agents.md"]) {
6283
- const file = path17.join(agentsPath, probe);
6284
- if (!fs15.existsSync(file)) continue;
6607
+ const file = path18.join(agentsPath, probe);
6608
+ if (!fs14.existsSync(file)) continue;
6285
6609
  try {
6286
- const content = fs15.readFileSync(file, "utf-8");
6610
+ const content = fs14.readFileSync(file, "utf-8");
6287
6611
  if (/[가-힣]/.test(content)) return "ko";
6288
6612
  } catch {
6289
6613
  }
@@ -6384,7 +6708,7 @@ function ensureSections(body, sections, kind, lang) {
6384
6708
  }
6385
6709
  function ensureDocsExist(docsDir, relativePaths, lang) {
6386
6710
  const missing = relativePaths.filter(
6387
- (relativePath) => !fs15.existsSync(path17.join(docsDir, relativePath))
6711
+ (relativePath) => !fs14.existsSync(path18.join(docsDir, relativePath))
6388
6712
  );
6389
6713
  if (missing.length > 0) {
6390
6714
  throw createCliError(
@@ -6394,13 +6718,62 @@ function ensureDocsExist(docsDir, relativePaths, lang) {
6394
6718
  }
6395
6719
  }
6396
6720
  function buildDefaultBodyFileName(kind, docsDir, component) {
6397
- const key = `${path17.resolve(docsDir)}::${component.trim().toLowerCase()}`;
6721
+ const key = `${path18.resolve(docsDir)}::${component.trim().toLowerCase()}`;
6398
6722
  const digest = createHash("sha1").update(key).digest("hex").slice(0, 12);
6399
6723
  return `lee-spec-kit.${digest}.${kind}.md`;
6400
6724
  }
6401
6725
  function toBodyFilePath(raw, kind, docsDir, component) {
6402
- const selected = raw?.trim() || path17.join(os.tmpdir(), buildDefaultBodyFileName(kind, docsDir, component));
6403
- return path17.resolve(selected);
6726
+ const selected = raw?.trim() || path18.join(os.tmpdir(), buildDefaultBodyFileName(kind, docsDir, component));
6727
+ return path18.resolve(selected);
6728
+ }
6729
+ function toProjectRootDocsPath(relativePathFromDocs) {
6730
+ const normalized = relativePathFromDocs.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
6731
+ if (normalized.startsWith("docs/")) return normalized;
6732
+ return `docs/${normalized}`;
6733
+ }
6734
+ function toBodyDocPaths(paths) {
6735
+ return {
6736
+ ...paths,
6737
+ specPath: toProjectRootDocsPath(paths.specPath),
6738
+ planPath: toProjectRootDocsPath(paths.planPath),
6739
+ tasksPath: toProjectRootDocsPath(paths.tasksPath)
6740
+ };
6741
+ }
6742
+ var TODO_PLACEHOLDER_PATTERN = /(^|\n)\s*-\s*\[[ xX]\]\s*TODO:/m;
6743
+ function ensureNoTodoPlaceholders(body, kind, lang) {
6744
+ if (!TODO_PLACEHOLDER_PATTERN.test(body)) return;
6745
+ throw createCliError(
6746
+ "PRECONDITION_FAILED",
6747
+ tg(lang, "todoPlaceholdersRemain", { kind })
6748
+ );
6749
+ }
6750
+ function parsePrArtifactMode(raw, kind, lang) {
6751
+ const value = (raw || "auto").trim().toLowerCase();
6752
+ if (value === "auto" || value === "on" || value === "off") {
6753
+ return value;
6754
+ }
6755
+ throw createCliError(
6756
+ "INVALID_ARGUMENT",
6757
+ tg(lang, "artifactModeInvalid", { kind, value })
6758
+ );
6759
+ }
6760
+ function resolvePrArtifactPolicy(config, feature, options) {
6761
+ const screenshotsMode = parsePrArtifactMode(
6762
+ options.screenshots,
6763
+ "screenshots",
6764
+ config.lang
6765
+ );
6766
+ const mermaidMode = parsePrArtifactMode(
6767
+ options.mermaid,
6768
+ "mermaid",
6769
+ config.lang
6770
+ );
6771
+ const includeScreenshots = screenshotsMode === "on" ? true : screenshotsMode === "off" ? false : config.pr?.screenshots?.upload ?? false;
6772
+ const includeMermaid = mermaidMode === "on" ? true : mermaidMode === "off" ? false : feature.type === "be";
6773
+ return {
6774
+ includeScreenshots,
6775
+ includeMermaid
6776
+ };
6404
6777
  }
6405
6778
  async function resolveFeatureOrThrow(featureName, options, lang) {
6406
6779
  const config = await getConfig(process.cwd());
@@ -6440,27 +6813,38 @@ function getFeatureDocPaths(feature) {
6440
6813
  function normalizeHeading(value) {
6441
6814
  return value.trim().replace(/\s+/g, " ").toLowerCase();
6442
6815
  }
6443
- function extractMarkdownSection(content, headings) {
6816
+ function extractMarkdownByHeadings(content, headings, levels) {
6444
6817
  const targets = new Set(headings.map((heading) => normalizeHeading(heading)));
6445
6818
  const lines = content.split("\n");
6446
6819
  let start = -1;
6820
+ let startLevel = 0;
6821
+ const levelSet = new Set(levels);
6447
6822
  for (let i = 0; i < lines.length; i++) {
6448
- const match = lines[i].match(/^\s*##\s+(.+?)\s*$/);
6823
+ const match = lines[i].match(/^\s*(#{2,6})\s+(.+?)\s*$/);
6449
6824
  if (!match) continue;
6450
- if (!targets.has(normalizeHeading(match[1]))) continue;
6825
+ const level = match[1].length;
6826
+ if (!levelSet.has(level)) continue;
6827
+ if (!targets.has(normalizeHeading(match[2]))) continue;
6451
6828
  start = i + 1;
6829
+ startLevel = level;
6452
6830
  break;
6453
6831
  }
6454
6832
  if (start < 0) return void 0;
6455
6833
  let end = lines.length;
6456
6834
  for (let i = start; i < lines.length; i++) {
6457
- if (/^\s*##\s+/.test(lines[i])) {
6835
+ const heading = lines[i].match(/^\s*(#{2,6})\s+(.+?)\s*$/);
6836
+ if (!heading) continue;
6837
+ const level = heading[1].length;
6838
+ if (level <= startLevel) {
6458
6839
  end = i;
6459
6840
  break;
6460
6841
  }
6461
6842
  }
6462
6843
  return lines.slice(start, end).join("\n");
6463
6844
  }
6845
+ function extractMarkdownSection(content, headings) {
6846
+ return extractMarkdownByHeadings(content, headings, [2]);
6847
+ }
6464
6848
  function isTemplateLine(line) {
6465
6849
  const trimmed = line.trim();
6466
6850
  if (!trimmed) return true;
@@ -6476,6 +6860,337 @@ function sanitizeOverviewSection(raw) {
6476
6860
  if (lines.length === 0) return void 0;
6477
6861
  return lines.slice(0, 5).join("\n");
6478
6862
  }
6863
+ function sanitizeDraftItem(raw) {
6864
+ const trimmed = raw.replace(/^\s*-\s*\[[ xX]\]\s*/, "").replace(/^\s*-\s+/, "").replace(/^\s*###\s+/, "").trim();
6865
+ const plain = trimmed.replace(/\*\*/g, "").trim();
6866
+ if (!plain) return void 0;
6867
+ if (isTemplateLine(plain)) return void 0;
6868
+ if (/^todo:/i.test(plain)) return void 0;
6869
+ if (/\{[^}]*\}/.test(plain)) return void 0;
6870
+ if (/^(as a|i want|so that)\b/i.test(plain)) return void 0;
6871
+ if (/^acceptance criteria:?$/i.test(plain)) return void 0;
6872
+ return plain.replace(/\s+/g, " ");
6873
+ }
6874
+ function uniqItems(items) {
6875
+ const seen = /* @__PURE__ */ new Set();
6876
+ const ordered = [];
6877
+ for (const item of items) {
6878
+ const normalized = item.trim().toLowerCase();
6879
+ if (!normalized || seen.has(normalized)) continue;
6880
+ seen.add(normalized);
6881
+ ordered.push(item.trim());
6882
+ }
6883
+ return ordered;
6884
+ }
6885
+ function normalizeSemanticKey(value) {
6886
+ return value.toLowerCase().replace(/[`'"(){}\[\].,:;!?/\\\-_|]/g, " ").replace(/\s+/g, " ").trim().replace(/\s/g, "");
6887
+ }
6888
+ function uniqItemsByContainment(items) {
6889
+ const kept = [];
6890
+ const keys = [];
6891
+ for (const item of items) {
6892
+ const clean = item.trim();
6893
+ if (!clean) continue;
6894
+ const key = normalizeSemanticKey(clean);
6895
+ if (!key) continue;
6896
+ let replaced = false;
6897
+ for (let i = 0; i < keys.length; i++) {
6898
+ const current = keys[i];
6899
+ if (!current.includes(key) && !key.includes(current)) continue;
6900
+ if (key.length > current.length) {
6901
+ keys[i] = key;
6902
+ kept[i] = clean;
6903
+ }
6904
+ replaced = true;
6905
+ break;
6906
+ }
6907
+ if (!replaced) {
6908
+ keys.push(key);
6909
+ kept.push(clean);
6910
+ }
6911
+ }
6912
+ return kept;
6913
+ }
6914
+ function extractSectionLines(raw) {
6915
+ if (!raw) return [];
6916
+ return uniqItems(
6917
+ raw.split("\n").map((line) => sanitizeDraftItem(line)).filter((line) => !!line)
6918
+ );
6919
+ }
6920
+ function extractSectionHeadings(raw) {
6921
+ if (!raw) return [];
6922
+ return uniqItems(
6923
+ raw.split("\n").map((line) => line.match(/^\s*###\s+(.+?)\s*$/)?.[1] || "").map((line) => sanitizeDraftItem(line)).filter((line) => !!line).map((line) => line.replace(/^FR-\d+:\s*/i, "").trim())
6924
+ );
6925
+ }
6926
+ function toChecklistLines(items) {
6927
+ return items.map((item) => `- [ ] ${item}`).join("\n");
6928
+ }
6929
+ function extractChecklistItems(raw) {
6930
+ if (!raw) return [];
6931
+ return uniqItems(
6932
+ raw.split("\n").map((line) => {
6933
+ const match = line.match(/^\s*-\s*\[[ xX]\]\s+(.+?)\s*$/);
6934
+ if (!match) return void 0;
6935
+ return sanitizeDraftItem(match[1]);
6936
+ }).filter((line) => !!line)
6937
+ );
6938
+ }
6939
+ function extractTaskTitles(tasksContent) {
6940
+ return uniqItems(
6941
+ tasksContent.split("\n").map((line) => {
6942
+ const match = line.match(
6943
+ /^\s*-\s*\[(?:TODO|DOING|DONE)\][^\n]*?\s+(?:T-[A-Z0-9-]+\s+)?(.+?)\s*$/
6944
+ );
6945
+ if (!match) return void 0;
6946
+ return sanitizeDraftItem(match[1]);
6947
+ }).filter((line) => !!line)
6948
+ );
6949
+ }
6950
+ function extractTasksAcceptanceItems(tasksContent) {
6951
+ const lines = tasksContent.split("\n");
6952
+ const accepted = [];
6953
+ let inAcceptance = false;
6954
+ for (const line of lines) {
6955
+ if (/^\s*-\s*Acceptance\s*:\s*$/i.test(line)) {
6956
+ inAcceptance = true;
6957
+ continue;
6958
+ }
6959
+ if (inAcceptance && /^\s*-\s*Checklist\s*:\s*$/i.test(line)) {
6960
+ inAcceptance = false;
6961
+ continue;
6962
+ }
6963
+ if (!inAcceptance) continue;
6964
+ const match = line.match(/^\s*-\s*\[[ xX]\]\s+(.+?)\s*$/) || line.match(/^\s*-\s+(.+?)\s*$/);
6965
+ if (!match) continue;
6966
+ const item = sanitizeDraftItem(match[1]);
6967
+ if (!item) continue;
6968
+ accepted.push(item);
6969
+ }
6970
+ return uniqItems(accepted);
6971
+ }
6972
+ function extractScopeItemsFromPlan(planContent, lang) {
6973
+ const section = extractMarkdownSection(
6974
+ planContent,
6975
+ ["\uBC94\uC704(\uBA85\uD655\uD654)", "\uBC94\uC704", "Scope", "Scope Clarification"]
6976
+ );
6977
+ if (!section) return { include: [], exclude: [] };
6978
+ const lines = section.split("\n");
6979
+ const include = [];
6980
+ const exclude = [];
6981
+ let mode = null;
6982
+ const includePatterns = lang === "ko" ? [/포함/] : [/in\s*scope/i, /^include$/i, /included/i];
6983
+ const excludePatterns = lang === "ko" ? [/비포함/, /제외/] : [/out\s*of\s*scope/i, /^exclude$/i, /excluded/i];
6984
+ for (const line of lines) {
6985
+ const plain = line.replace(/\*\*/g, "").trim();
6986
+ if (!plain) continue;
6987
+ if (excludePatterns.some((re) => re.test(plain))) {
6988
+ mode = "exclude";
6989
+ continue;
6990
+ }
6991
+ if (includePatterns.some((re) => re.test(plain))) {
6992
+ mode = "include";
6993
+ continue;
6994
+ }
6995
+ const bullet = line.match(/^\s*-\s+(.+?)\s*$/)?.[1];
6996
+ if (!bullet) continue;
6997
+ const item = sanitizeDraftItem(bullet);
6998
+ if (!item) continue;
6999
+ if (mode === "include") {
7000
+ include.push(item);
7001
+ } else if (mode === "exclude") {
7002
+ exclude.push(item);
7003
+ }
7004
+ }
7005
+ return {
7006
+ include: uniqItems(include),
7007
+ exclude: uniqItems(exclude)
7008
+ };
7009
+ }
7010
+ function getIssueGoalsAndCriteria(specContent, planContent, tasksContent, overview, lang) {
7011
+ const purposeLines = extractSectionLines(
7012
+ extractMarkdownSection(specContent, ["\uBAA9\uC801", "Purpose"])
7013
+ );
7014
+ const requirementHeadings = extractSectionHeadings(
7015
+ extractMarkdownSection(specContent, ["\uAE30\uB2A5 \uC694\uAD6C\uC0AC\uD56D", "Functional Requirements"])
7016
+ );
7017
+ const userStoryChecklist = extractChecklistItems(
7018
+ extractMarkdownSection(specContent, ["\uC0AC\uC6A9\uC790 \uC2A4\uD1A0\uB9AC", "User Stories"])
7019
+ );
7020
+ const tasksAcceptance = extractTasksAcceptanceItems(tasksContent);
7021
+ const scopeFromPlan = extractScopeItemsFromPlan(planContent, lang);
7022
+ const taskTitles = extractTaskTitles(tasksContent);
7023
+ const goals = uniqItemsByContainment(uniqItems([
7024
+ ...requirementHeadings,
7025
+ ...scopeFromPlan.include,
7026
+ ...purposeLines.slice(0, 1),
7027
+ sanitizeDraftItem(overview) || ""
7028
+ ])).slice(0, 5);
7029
+ while (goals.length < 3) {
7030
+ goals.push(
7031
+ lang === "ko" ? goals.length === 0 ? "spec.md \uBAA9\uC801\uC5D0 \uB9DE\uCDB0 \uAD6C\uD604 \uBC94\uC704\uB97C \uD655\uC815\uD55C\uB2E4." : goals.length === 1 ? "\uD3EC\uD568/\uC81C\uC678 \uBC94\uC704\uB97C \uBA85\uD655\uD788 \uC815\uC758\uD558\uACE0 \uBB38\uC11C\uC640 \uAD6C\uD604\uC744 \uC77C\uCE58\uC2DC\uD0A8\uB2E4." : "\uAD00\uB828 \uD14C\uC2A4\uD2B8 \uBC0F \uAC80\uC99D \uACBD\uB85C\uB97C \uD3EC\uD568\uD574 \uAE30\uB2A5 \uC644\uC131\uB3C4\uB97C \uD655\uBCF4\uD55C\uB2E4." : goals.length === 0 ? "Define implementation scope aligned with spec.md purpose." : goals.length === 1 ? "Clarify in-scope and out-of-scope boundaries and keep docs/code aligned." : "Ensure feature completeness with concrete validation paths."
7032
+ );
7033
+ }
7034
+ const criteria = uniqItemsByContainment(
7035
+ uniqItems([...userStoryChecklist, ...tasksAcceptance])
7036
+ ).slice(0, 6);
7037
+ while (criteria.length < 4) {
7038
+ criteria.push(
7039
+ lang === "ko" ? criteria.length === 0 ? "\uD575\uC2EC \uC0AC\uC6A9\uC790 \uC2DC\uB098\uB9AC\uC624\uC5D0\uC11C \uBAA9\uD45C \uB3D9\uC791\uC774 \uC7AC\uD604\uB41C\uB2E4." : criteria.length === 1 ? "\uC694\uAD6C\uC0AC\uD56D\uBCC4 \uC218\uC6A9 \uAE30\uC900\uC774 \uBAA8\uB450 \uCDA9\uC871\uB41C\uB2E4." : criteria.length === 2 ? "\uAC80\uC99D \uBC29\uBC95(\uD14C\uC2A4\uD2B8/\uC218\uB3D9 \uC2DC\uB098\uB9AC\uC624)\uC744 \uAE30\uB85D\uD574 \uC644\uB8CC\uB97C \uD655\uC778\uD560 \uC218 \uC788\uB2E4." : "\uD68C\uADC0 \uC5C6\uC774 \uAE30\uC874 \uB3D9\uC791\uACFC\uC758 \uD638\uD658\uC131\uC744 \uC720\uC9C0\uD55C\uB2E4." : criteria.length === 0 ? "Core user scenarios reproduce expected behavior." : criteria.length === 1 ? "All requirement-level acceptance criteria are satisfied." : criteria.length === 2 ? "Validation method (tests/manual scenarios) is documented for completion checks." : "Compatibility is preserved without regressions."
7040
+ );
7041
+ }
7042
+ const scope = uniqItemsByContainment(
7043
+ uniqItems([...scopeFromPlan.include, ...taskTitles])
7044
+ ).slice(0, 6);
7045
+ return {
7046
+ goals: goals.slice(0, 5),
7047
+ criteria: criteria.slice(0, 6),
7048
+ scope
7049
+ };
7050
+ }
7051
+ function extractPlanChangeTargets(planContent, lang) {
7052
+ const section = extractMarkdownSection(
7053
+ planContent,
7054
+ [
7055
+ "\uBCC0\uACBD \uB300\uC0C1(\uC608\uC0C1)",
7056
+ "\uBCC0\uACBD \uB300\uC0C1",
7057
+ "Changed Files",
7058
+ "Change Targets",
7059
+ "Expected Changes"
7060
+ ]
7061
+ );
7062
+ if (!section) return [];
7063
+ const lines = section.split("\n");
7064
+ const out = [];
7065
+ let inCode = false;
7066
+ for (const line of lines) {
7067
+ if (/^\s*```/.test(line)) {
7068
+ inCode = !inCode;
7069
+ continue;
7070
+ }
7071
+ if (inCode) {
7072
+ const trimmed = line.trim();
7073
+ if (!trimmed) continue;
7074
+ if (!/[\\/]/.test(trimmed)) continue;
7075
+ out.push(
7076
+ lang === "ko" ? `\`${trimmed}\` \uBCC0\uACBD` : `Update \`${trimmed}\``
7077
+ );
7078
+ continue;
7079
+ }
7080
+ const bullet = line.match(/^\s*-\s+(.+?)\s*$/)?.[1];
7081
+ if (!bullet) continue;
7082
+ const item = sanitizeDraftItem(bullet);
7083
+ if (!item) continue;
7084
+ out.push(item);
7085
+ }
7086
+ return uniqItemsByContainment(uniqItems(out));
7087
+ }
7088
+ function extractCommandsFromSection(raw) {
7089
+ if (!raw) return [];
7090
+ const commands = [];
7091
+ for (const match of raw.matchAll(/`([^`]+)`/g)) {
7092
+ const candidate = match[1].trim();
7093
+ if (!candidate) continue;
7094
+ if (/\{[^}]*\}/.test(candidate)) continue;
7095
+ if (!/\b(pnpm|npm|yarn|bun|vitest|jest|tsx?|node)\b/.test(candidate)) continue;
7096
+ commands.push(candidate);
7097
+ }
7098
+ for (const line of raw.split("\n")) {
7099
+ const trimmed = line.trim().replace(/^-+\s*/, "");
7100
+ if (!trimmed) continue;
7101
+ if (!/\b(pnpm|npm|yarn|bun|vitest|jest|tsx?|node)\b/.test(trimmed)) continue;
7102
+ if (/\{[^}]*\}/.test(trimmed)) continue;
7103
+ commands.push(trimmed);
7104
+ }
7105
+ return uniqItems(commands);
7106
+ }
7107
+ function extractRecordedTestLines(tasksContent, planContent, lang) {
7108
+ const section = extractMarkdownByHeadings(
7109
+ tasksContent,
7110
+ ["\uD14C\uC2A4\uD2B8 \uC2E4\uD589 \uAE30\uB85D", "Tests Run", "Test Run Log", "Test Execution Log"],
7111
+ [3, 2]
7112
+ );
7113
+ const records = [];
7114
+ if (section) {
7115
+ for (const line of section.split("\n")) {
7116
+ if (!line.trim().startsWith("|")) continue;
7117
+ const cells = line.split("|").slice(1, -1).map((cell) => cell.trim().replace(/`/g, ""));
7118
+ if (cells.length < 3) continue;
7119
+ const [cmd, time, result] = cells;
7120
+ if (!cmd || /^명령어$/i.test(cmd) || /^command$/i.test(cmd) || /^-+$/.test(cmd)) continue;
7121
+ if (/\{[^}]*\}/.test(cmd) || /\{[^}]*\}/.test(result || "")) continue;
7122
+ const renderedResult = result || (lang === "ko" ? "\uBBF8\uAE30\uB85D" : "not recorded");
7123
+ const renderedTime = time && time !== "-" ? ` (${time})` : "";
7124
+ records.push(`\`${cmd}\` \u2014 ${renderedResult}${renderedTime}`);
7125
+ }
7126
+ const lines = section.split("\n");
7127
+ for (let i = 0; i < lines.length; i++) {
7128
+ const commandMatch = lines[i].match(/^\s*-\s*(?:명령어|Command)\s*:\s*`?([^`]+?)`?\s*$/i);
7129
+ if (!commandMatch) continue;
7130
+ const command = commandMatch[1].trim();
7131
+ if (!command || /\{[^}]*\}/.test(command)) continue;
7132
+ let result = "";
7133
+ for (let j = i + 1; j < Math.min(lines.length, i + 5); j++) {
7134
+ const resultMatch = lines[j].match(/^\s*-\s*(?:결과|Result)\s*:\s*(.+?)\s*$/i);
7135
+ if (!resultMatch) continue;
7136
+ result = resultMatch[1].replace(/`/g, "").trim();
7137
+ break;
7138
+ }
7139
+ if (!result || /\{[^}]*\}/.test(result)) continue;
7140
+ records.push(`\`${command}\` \u2014 ${result}`);
7141
+ }
7142
+ }
7143
+ if (records.length > 0) {
7144
+ return uniqItemsByContainment(uniqItems(records));
7145
+ }
7146
+ const plannedCommands = extractCommandsFromSection(
7147
+ extractMarkdownByHeadings(
7148
+ planContent,
7149
+ ["\uAC80\uC99D \uBA85\uB839(\uC608\uC815)", "Validation Commands", "Verification Commands"],
7150
+ [3, 2]
7151
+ )
7152
+ );
7153
+ if (plannedCommands.length > 0) {
7154
+ return plannedCommands.map(
7155
+ (command) => lang === "ko" ? `\`${command}\` \u2014 \uC2E4\uD589 \uACB0\uACFC\uB97C \uAE30\uB85D\uD558\uC138\uC694.` : `\`${command}\` \u2014 record execution result.`
7156
+ );
7157
+ }
7158
+ return [];
7159
+ }
7160
+ function getPrChangesAndTests(specContent, planContent, tasksContent, overview, lang) {
7161
+ const requirementHeadings = extractSectionHeadings(
7162
+ extractMarkdownSection(specContent, ["\uAE30\uB2A5 \uC694\uAD6C\uC0AC\uD56D", "Functional Requirements"])
7163
+ );
7164
+ const scopeFromPlan = extractScopeItemsFromPlan(planContent, lang).include;
7165
+ const planTargets = extractPlanChangeTargets(planContent, lang);
7166
+ const taskTitles = extractTaskTitles(tasksContent);
7167
+ const changes = uniqItemsByContainment(
7168
+ uniqItems([
7169
+ ...taskTitles,
7170
+ ...scopeFromPlan,
7171
+ ...requirementHeadings,
7172
+ ...planTargets,
7173
+ sanitizeDraftItem(overview) || ""
7174
+ ])
7175
+ ).slice(0, 6);
7176
+ while (changes.length < 3) {
7177
+ changes.push(
7178
+ lang === "ko" ? changes.length === 0 ? "\uD575\uC2EC \uAD6C\uD604 \uBCC0\uACBD \uC0AC\uD56D\uC744 \uC694\uC57D\uD574 \uBC18\uC601\uD55C\uB2E4." : changes.length === 1 ? "\uC601\uD5A5 \uBC94\uC704(\uD638\uD658\uC131/\uB9C8\uC774\uADF8\uB808\uC774\uC158 \uD3EC\uD568)\uB97C \uBA85\uC2DC\uD55C\uB2E4." : "\uBB38\uC11C\uC640 \uAD6C\uD604 \uAC04 \uBD88\uC77C\uCE58\uAC00 \uC5C6\uB3C4\uB85D \uC815\uD569\uC131\uC744 \uC810\uAC80\uD55C\uB2E4." : changes.length === 0 ? "Summarize key implementation changes." : changes.length === 1 ? "Document impact scope (including compatibility/migration)." : "Verify document and implementation consistency."
7179
+ );
7180
+ }
7181
+ const tests = uniqItemsByContainment(
7182
+ uniqItems(extractRecordedTestLines(tasksContent, planContent, lang))
7183
+ ).slice(0, 4);
7184
+ while (tests.length < 2) {
7185
+ tests.push(
7186
+ lang === "ko" ? tests.length === 0 ? "\uAD00\uB828 \uD14C\uC2A4\uD2B8\uB97C \uC2E4\uD589\uD558\uACE0 \uACB0\uACFC\uB97C \uAE30\uB85D\uD55C\uB2E4." : "\uBBF8\uC2E4\uD589 \uD14C\uC2A4\uD2B8\uAC00 \uC788\uC73C\uBA74 \uC0AC\uC720\uC640 \uB9AC\uC2A4\uD06C\uB97C \uAE30\uB85D\uD55C\uB2E4." : tests.length === 0 ? "Run relevant tests and record results." : "If tests were not run, record rationale and risk."
7187
+ );
7188
+ }
7189
+ return {
7190
+ changes: changes.slice(0, 6),
7191
+ tests: tests.slice(0, 4)
7192
+ };
7193
+ }
6479
7194
  function resolveOverviewFromSpec(specContent, feature, lang) {
6480
7195
  const fromPurpose = sanitizeOverviewSection(
6481
7196
  extractMarkdownSection(specContent, ["\uBAA9\uC801", "Purpose"])
@@ -6487,7 +7202,112 @@ function resolveOverviewFromSpec(specContent, feature, lang) {
6487
7202
  if (fromOverview) return fromOverview;
6488
7203
  return lang === "ko" ? `\`${feature.folderName}\` \uAE30\uB2A5 \uAC1C\uC694\uB97C spec.md \uAE30\uC900\uC73C\uB85C \uC791\uC131\uD558\uC138\uC694.` : `Summarize feature \`${feature.folderName}\` from spec.md.`;
6489
7204
  }
6490
- function buildIssueBody(overview, labels, paths, lang) {
7205
+ function getPrScreenshotsHeading(lang) {
7206
+ return getGithubDraftArtifactHeading("pr", "screenshots", lang) || (lang === "ko" ? "\uC2A4\uD06C\uB9B0\uC0F7" : "Screenshots");
7207
+ }
7208
+ function getPrMermaidHeading(lang) {
7209
+ return getGithubDraftArtifactHeading("pr", "mermaid", lang) || (lang === "ko" ? "\uC544\uD0A4\uD14D\uCC98 \uB2E4\uC774\uC5B4\uADF8\uB7A8" : "Architecture Diagram");
7210
+ }
7211
+ function buildPrScreenshotsSection(lang) {
7212
+ if (lang === "ko") {
7213
+ return `
7214
+ ## \uC2A4\uD06C\uB9B0\uC0F7
7215
+
7216
+ - [ ] \uC5C5\uB85C\uB4DC\uD55C \uC2A4\uD06C\uB9B0\uC0F7 URL\uC744 \uD3EC\uD568\uD558\uC138\uC694. (\uC608: \`![](https://...)\`)
7217
+ `;
7218
+ }
7219
+ return `
7220
+ ## Screenshots
7221
+
7222
+ - [ ] Include uploaded screenshot URL(s). (e.g. \`![](https://...)\`)
7223
+ `;
7224
+ }
7225
+ function buildPrMermaidSection(lang) {
7226
+ if (lang === "ko") {
7227
+ return `
7228
+ ## \uC544\uD0A4\uD14D\uCC98 \uB2E4\uC774\uC5B4\uADF8\uB7A8
7229
+
7230
+ \`\`\`mermaid
7231
+ sequenceDiagram
7232
+ participant Client
7233
+ participant API
7234
+ participant Service
7235
+ participant DB
7236
+ Client->>API: Request
7237
+ API->>Service: Execute
7238
+ Service->>DB: Query/Command
7239
+ DB-->>Service: Result
7240
+ Service-->>API: Response DTO
7241
+ API-->>Client: Response
7242
+ \`\`\`
7243
+ `;
7244
+ }
7245
+ return `
7246
+ ## Architecture Diagram
7247
+
7248
+ \`\`\`mermaid
7249
+ sequenceDiagram
7250
+ participant Client
7251
+ participant API
7252
+ participant Service
7253
+ participant DB
7254
+ Client->>API: Request
7255
+ API->>Service: Execute
7256
+ Service->>DB: Query/Command
7257
+ DB-->>Service: Result
7258
+ Service-->>API: Response DTO
7259
+ API-->>Client: Response
7260
+ \`\`\`
7261
+ `;
7262
+ }
7263
+ function ensurePrArtifacts(body, policy, lang) {
7264
+ if (policy.includeScreenshots) {
7265
+ const heading = getPrScreenshotsHeading(lang);
7266
+ const section = extractMarkdownByHeadings(body, [heading], [2]);
7267
+ if (!section) {
7268
+ throw createCliError(
7269
+ "PRECONDITION_FAILED",
7270
+ tg(lang, "prScreenshotsSectionMissing", { section: heading })
7271
+ );
7272
+ }
7273
+ if (!/!\[[^\]]*]\((?!\s*\))[^)]+\)/m.test(section)) {
7274
+ throw createCliError(
7275
+ "PRECONDITION_FAILED",
7276
+ tg(lang, "prScreenshotImageMissing", { section: heading })
7277
+ );
7278
+ }
7279
+ }
7280
+ if (policy.includeMermaid) {
7281
+ const heading = getPrMermaidHeading(lang);
7282
+ const section = extractMarkdownByHeadings(body, [heading], [2]);
7283
+ if (!section) {
7284
+ throw createCliError(
7285
+ "PRECONDITION_FAILED",
7286
+ tg(lang, "prMermaidSectionMissing", { section: heading })
7287
+ );
7288
+ }
7289
+ if (!/```mermaid[\s\S]*?```/m.test(section)) {
7290
+ throw createCliError(
7291
+ "PRECONDITION_FAILED",
7292
+ tg(lang, "prMermaidBlockMissing", { section: heading })
7293
+ );
7294
+ }
7295
+ }
7296
+ }
7297
+ function buildIssueBody(specContent, planContent, tasksContent, overview, labels, paths, lang) {
7298
+ const bodyPaths = toBodyDocPaths(paths);
7299
+ const draft = getIssueGoalsAndCriteria(specContent, planContent, tasksContent, overview, lang);
7300
+ const goals = toChecklistLines(draft.goals);
7301
+ const criteria = toChecklistLines(draft.criteria);
7302
+ const scopeSection = draft.scope.length > 0 ? lang === "ko" ? `
7303
+ ## \uC791\uC5C5 \uBC94\uC704(\uC608\uC815)
7304
+
7305
+ ${draft.scope.map((item) => `- ${item}`).join("\n")}
7306
+ ` : `
7307
+ ## Planned Scope
7308
+
7309
+ ${draft.scope.map((item) => `- ${item}`).join("\n")}
7310
+ ` : "";
6491
7311
  if (lang === "ko") {
6492
7312
  return `## \uAC1C\uC694
6493
7313
 
@@ -6495,19 +7315,18 @@ ${overview}
6495
7315
 
6496
7316
  ## \uBAA9\uD45C
6497
7317
 
6498
- - [ ] TODO: spec.md \uBAA9\uC801/\uBC94\uC704\uB97C \uBC14\uD0D5\uC73C\uB85C \uBAA9\uD45C\uB97C \uC791\uC131\uD558\uC138\uC694.
6499
- - [ ] TODO: \uAD6C\uD604 \uBC94\uC704(\uD3EC\uD568/\uC81C\uC678)\uB97C \uAD6C\uCCB4\uC801\uC73C\uB85C \uC791\uC131\uD558\uC138\uC694.
7318
+ ${goals}
6500
7319
 
6501
7320
  ## \uC644\uB8CC \uAE30\uC900
6502
7321
 
6503
- - [ ] TODO: \uAC80\uC99D \uAC00\uB2A5\uD55C \uC644\uB8CC \uAE30\uC900\uC744 \uC791\uC131\uD558\uC138\uC694.
6504
- - [ ] TODO: \uC644\uB8CC \uD655\uC778 \uBC29\uBC95(\uD14C\uC2A4\uD2B8/\uC2DC\uB098\uB9AC\uC624)\uC744 \uC791\uC131\uD558\uC138\uC694.
7322
+ ${criteria}
7323
+ ${scopeSection}
6505
7324
 
6506
7325
  ## \uAD00\uB828 \uBB38\uC11C
6507
7326
 
6508
- - **Spec**: \`${paths.specPath}\`
6509
- - **Plan**: \`${paths.planPath}\`
6510
- - **Tasks**: \`${paths.tasksPath}\`
7327
+ - **Spec**: \`${bodyPaths.specPath}\`
7328
+ - **Plan**: \`${bodyPaths.planPath}\`
7329
+ - **Tasks**: \`${bodyPaths.tasksPath}\`
6511
7330
 
6512
7331
  ## \uB77C\uBCA8
6513
7332
 
@@ -6520,29 +7339,40 @@ ${overview}
6520
7339
 
6521
7340
  ## Goals
6522
7341
 
6523
- - [ ] TODO: Fill concrete goals based on spec.md.
6524
- - [ ] TODO: Clarify in-scope and out-of-scope boundaries.
7342
+ ${goals}
6525
7343
 
6526
7344
  ## Completion Criteria
6527
7345
 
6528
- - [ ] TODO: Define verifiable completion criteria.
6529
- - [ ] TODO: Add validation/test conditions for completion.
7346
+ ${criteria}
7347
+ ${scopeSection}
6530
7348
 
6531
7349
  ## Related Documents
6532
7350
 
6533
- - **Spec**: \`${paths.specPath}\`
6534
- - **Plan**: \`${paths.planPath}\`
6535
- - **Tasks**: \`${paths.tasksPath}\`
7351
+ - **Spec**: \`${bodyPaths.specPath}\`
7352
+ - **Plan**: \`${bodyPaths.planPath}\`
7353
+ - **Tasks**: \`${bodyPaths.tasksPath}\`
6536
7354
 
6537
7355
  ## Labels
6538
7356
 
6539
7357
  ${labels.map((label) => `- \`${label}\``).join("\n")}
6540
7358
  `;
6541
7359
  }
6542
- function buildPrBody(feature, overview, paths, lang) {
7360
+ function buildPrBody(feature, specContent, planContent, tasksContent, overview, paths, artifactPolicy, lang) {
7361
+ const bodyPaths = toBodyDocPaths(paths);
6543
7362
  const closes = feature.issueNumber ? `
6544
7363
  Closes #${feature.issueNumber}
6545
7364
  ` : "\n";
7365
+ const draft = getPrChangesAndTests(
7366
+ specContent,
7367
+ planContent,
7368
+ tasksContent,
7369
+ overview,
7370
+ lang
7371
+ );
7372
+ const changes = toChecklistLines(draft.changes);
7373
+ const tests = toChecklistLines(draft.tests);
7374
+ const screenshotsSection = artifactPolicy.includeScreenshots ? buildPrScreenshotsSection(lang) : "";
7375
+ const mermaidSection = artifactPolicy.includeMermaid ? buildPrMermaidSection(lang) : "";
6546
7376
  if (lang === "ko") {
6547
7377
  return `## \uAC1C\uC694
6548
7378
 
@@ -6550,20 +7380,20 @@ ${overview}
6550
7380
 
6551
7381
  ## \uBCC0\uACBD \uC0AC\uD56D
6552
7382
 
6553
- - [ ] TODO: \uD575\uC2EC \uCF54\uB4DC \uBCC0\uACBD \uC0AC\uD56D\uC744 \uC694\uC57D\uD558\uC138\uC694.
6554
- - [ ] TODO: \uC601\uD5A5 \uBC94\uC704/\uD638\uD658\uC131(\uB9C8\uC774\uADF8\uB808\uC774\uC158 \uD3EC\uD568)\uC744 \uC791\uC131\uD558\uC138\uC694.
7383
+ ${changes}
6555
7384
 
6556
7385
  ## \uD14C\uC2A4\uD2B8
6557
7386
 
6558
7387
  ### \uC2E4\uD589\uD55C \uD14C\uC2A4\uD2B8
6559
7388
 
6560
- - [ ] TODO: \`<\uC2E4\uD589\uD55C \uD14C\uC2A4\uD2B8 \uBA85\uB839\uC5B4>\` \u2014 PASS/FAIL
6561
- - [ ] TODO: \uBBF8\uC2E4\uD589 \uD14C\uC2A4\uD2B8\uAC00 \uC788\uB2E4\uBA74 \uC0AC\uC720\uB97C \uC791\uC131\uD558\uC138\uC694.
7389
+ ${tests}
7390
+ ${screenshotsSection}
7391
+ ${mermaidSection}
6562
7392
 
6563
7393
  ## \uAD00\uB828 \uBB38\uC11C
6564
7394
 
6565
- - **Spec**: \`${paths.specPath}\`
6566
- - **Tasks**: \`${paths.tasksPath}\`${closes}`;
7395
+ - **Spec**: \`${bodyPaths.specPath}\`
7396
+ - **Tasks**: \`${bodyPaths.tasksPath}\`${closes}`;
6567
7397
  }
6568
7398
  return `## Overview
6569
7399
 
@@ -6571,26 +7401,26 @@ ${overview}
6571
7401
 
6572
7402
  ## Changes
6573
7403
 
6574
- - [ ] TODO: Summarize key code changes in this PR.
6575
- - [ ] TODO: Describe impact/scope (including migration if any).
7404
+ ${changes}
6576
7405
 
6577
7406
  ## Tests
6578
7407
 
6579
7408
  ### Tests Run
6580
7409
 
6581
- - [ ] TODO: \`<test command>\` \u2014 PASS/FAIL
6582
- - [ ] TODO: If tests were not run, explain why.
7410
+ ${tests}
7411
+ ${screenshotsSection}
7412
+ ${mermaidSection}
6583
7413
 
6584
7414
  ## Related Documents
6585
7415
 
6586
- - **Spec**: \`${paths.specPath}\`
6587
- - **Tasks**: \`${paths.tasksPath}\`${closes}`;
7416
+ - **Spec**: \`${bodyPaths.specPath}\`
7417
+ - **Tasks**: \`${bodyPaths.tasksPath}\`${closes}`;
6588
7418
  }
6589
7419
  function getRequiredIssueSections(lang) {
6590
- return lang === "ko" ? ["\uAC1C\uC694", "\uBAA9\uD45C", "\uC644\uB8CC \uAE30\uC900", "\uAD00\uB828 \uBB38\uC11C", "\uB77C\uBCA8"] : ["Overview", "Goals", "Completion Criteria", "Related Documents", "Labels"];
7420
+ return getGithubDraftRequiredSections("issue", lang);
6591
7421
  }
6592
7422
  function getRequiredPrSections(lang) {
6593
- return lang === "ko" ? ["\uAC1C\uC694", "\uBCC0\uACBD \uC0AC\uD56D", "\uD14C\uC2A4\uD2B8", "\uAD00\uB828 \uBB38\uC11C"] : ["Overview", "Changes", "Tests", "Related Documents"];
7423
+ return getGithubDraftRequiredSections("pr", lang);
6594
7424
  }
6595
7425
  function replaceListField(content, keys, value) {
6596
7426
  for (const key of keys) {
@@ -6621,10 +7451,10 @@ function insertFieldInGithubIssueSection(content, key, value) {
6621
7451
  return { content: lines.join("\n"), changed: true };
6622
7452
  }
6623
7453
  function syncTasksPrMetadata(tasksPath, prUrl, nextStatus, lang) {
6624
- if (!fs15.existsSync(tasksPath)) {
7454
+ if (!fs14.existsSync(tasksPath)) {
6625
7455
  throw createCliError("DOCS_NOT_FOUND", tg(lang, "tasksNotFound", { path: tasksPath }));
6626
7456
  }
6627
- const original = fs15.readFileSync(tasksPath, "utf-8");
7457
+ const original = fs14.readFileSync(tasksPath, "utf-8");
6628
7458
  let next = original;
6629
7459
  let changed = false;
6630
7460
  const prReplaced = replaceListField(next, ["PR", "Pull Request"], prUrl);
@@ -6648,7 +7478,7 @@ function syncTasksPrMetadata(tasksPath, prUrl, nextStatus, lang) {
6648
7478
  changed = changed || inserted.changed;
6649
7479
  }
6650
7480
  if (changed) {
6651
- fs15.writeFileSync(tasksPath, next, "utf-8");
7481
+ fs14.writeFileSync(tasksPath, next, "utf-8");
6652
7482
  }
6653
7483
  return { changed, path: tasksPath };
6654
7484
  }
@@ -6676,7 +7506,7 @@ function ensureCleanWorktree(cwd, lang) {
6676
7506
  }
6677
7507
  }
6678
7508
  function commitAndPushPath(cwd, absPath, message, lang) {
6679
- const relativePath = path17.relative(cwd, absPath) || absPath;
7509
+ const relativePath = path18.relative(cwd, absPath) || absPath;
6680
7510
  const status = runProcessOrThrow(
6681
7511
  "git",
6682
7512
  ["status", "--porcelain=v1", "--", relativePath],
@@ -6808,15 +7638,25 @@ function githubCommand(program2) {
6808
7638
  [paths.specPath, paths.planPath, paths.tasksPath],
6809
7639
  config.lang
6810
7640
  );
6811
- const specContent = await fs15.readFile(path17.join(config.docsDir, paths.specPath), "utf-8");
7641
+ const specContent = await fs14.readFile(path18.join(config.docsDir, paths.specPath), "utf-8");
7642
+ const planContent = await fs14.readFile(path18.join(config.docsDir, paths.planPath), "utf-8");
7643
+ const tasksContent = await fs14.readFile(path18.join(config.docsDir, paths.tasksPath), "utf-8");
6812
7644
  const overview = resolveOverviewFromSpec(specContent, feature, config.lang);
6813
7645
  const title = options.title?.trim() || tg(config.lang, "issueDefaultTitle", {
6814
7646
  slug: feature.slug,
6815
7647
  folder: feature.folderName
6816
7648
  });
6817
- const body = buildIssueBody(overview, labels, paths, config.lang);
7649
+ const generatedBody = buildIssueBody(
7650
+ specContent,
7651
+ planContent,
7652
+ tasksContent,
7653
+ overview,
7654
+ labels,
7655
+ paths,
7656
+ config.lang
7657
+ );
6818
7658
  ensureSections(
6819
- body,
7659
+ generatedBody,
6820
7660
  getRequiredIssueSections(config.lang),
6821
7661
  tg(config.lang, "kindIssue"),
6822
7662
  config.lang
@@ -6827,10 +7667,23 @@ function githubCommand(program2) {
6827
7667
  config.docsDir,
6828
7668
  feature.type
6829
7669
  );
6830
- await fs15.ensureDir(path17.dirname(bodyFile));
6831
- await fs15.writeFile(bodyFile, body, "utf-8");
7670
+ const explicitBodyFile = (options.bodyFile || "").trim();
7671
+ let body = generatedBody;
7672
+ if (options.create && explicitBodyFile && await fs14.pathExists(bodyFile)) {
7673
+ body = await fs14.readFile(bodyFile, "utf-8");
7674
+ ensureSections(
7675
+ body,
7676
+ getRequiredIssueSections(config.lang),
7677
+ tg(config.lang, "kindIssue"),
7678
+ config.lang
7679
+ );
7680
+ } else {
7681
+ await fs14.ensureDir(path18.dirname(bodyFile));
7682
+ await fs14.writeFile(bodyFile, generatedBody, "utf-8");
7683
+ }
6832
7684
  let issueUrl;
6833
7685
  if (options.create) {
7686
+ ensureNoTodoPlaceholders(body, tg(config.lang, "kindIssue"), config.lang);
6834
7687
  assertRemoteApproval(
6835
7688
  options.confirm,
6836
7689
  tg(config.lang, "operationIssueCreate"),
@@ -6914,7 +7767,7 @@ function githubCommand(program2) {
6914
7767
  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(
6915
7768
  "--confirm <reply>",
6916
7769
  tg(commandLang, "optPrConfirm")
6917
- ).option("--retry <count>", tg(commandLang, "optPrRetry")).option("--no-sync-tasks", tg(commandLang, "optPrNoSyncTasks")).option("--commit-sync", tg(commandLang, "optPrCommitSync")).action(async (featureName, options) => {
7770
+ ).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) => {
6918
7771
  try {
6919
7772
  const selectedComponent = resolveComponentOption4(options, commandLang);
6920
7773
  const { config, feature } = await resolveFeatureOrThrow(featureName, {
@@ -6923,7 +7776,10 @@ function githubCommand(program2) {
6923
7776
  const labels = parseLabels(options.labels, config.lang);
6924
7777
  const paths = getFeatureDocPaths(feature);
6925
7778
  ensureDocsExist(config.docsDir, [paths.specPath, paths.tasksPath], config.lang);
6926
- const specContent = await fs15.readFile(path17.join(config.docsDir, paths.specPath), "utf-8");
7779
+ const specContent = await fs14.readFile(path18.join(config.docsDir, paths.specPath), "utf-8");
7780
+ const planPath = path18.join(config.docsDir, paths.planPath);
7781
+ const planContent = await fs14.pathExists(planPath) ? await fs14.readFile(planPath, "utf-8") : "";
7782
+ const tasksContent = await fs14.readFile(path18.join(config.docsDir, paths.tasksPath), "utf-8");
6927
7783
  const overview = resolveOverviewFromSpec(specContent, feature, config.lang);
6928
7784
  const defaultTitle = feature.issueNumber ? tg(config.lang, "prDefaultTitleWithIssue", {
6929
7785
  issue: feature.issueNumber,
@@ -6932,9 +7788,19 @@ function githubCommand(program2) {
6932
7788
  slug: feature.slug
6933
7789
  });
6934
7790
  const title = options.title?.trim() || defaultTitle;
6935
- const body = buildPrBody(feature, overview, paths, config.lang);
7791
+ const artifactPolicy = resolvePrArtifactPolicy(config, feature, options);
7792
+ const generatedBody = buildPrBody(
7793
+ feature,
7794
+ specContent,
7795
+ planContent,
7796
+ tasksContent,
7797
+ overview,
7798
+ paths,
7799
+ artifactPolicy,
7800
+ config.lang
7801
+ );
6936
7802
  ensureSections(
6937
- body,
7803
+ generatedBody,
6938
7804
  getRequiredPrSections(config.lang),
6939
7805
  tg(config.lang, "kindPr"),
6940
7806
  config.lang
@@ -6945,13 +7811,27 @@ function githubCommand(program2) {
6945
7811
  config.docsDir,
6946
7812
  feature.type
6947
7813
  );
6948
- await fs15.ensureDir(path17.dirname(bodyFile));
6949
- await fs15.writeFile(bodyFile, body, "utf-8");
7814
+ const explicitBodyFile = (options.bodyFile || "").trim();
7815
+ let body = generatedBody;
7816
+ if (options.create && explicitBodyFile && await fs14.pathExists(bodyFile)) {
7817
+ body = await fs14.readFile(bodyFile, "utf-8");
7818
+ ensureSections(
7819
+ body,
7820
+ getRequiredPrSections(config.lang),
7821
+ tg(config.lang, "kindPr"),
7822
+ config.lang
7823
+ );
7824
+ } else {
7825
+ await fs14.ensureDir(path18.dirname(bodyFile));
7826
+ await fs14.writeFile(bodyFile, generatedBody, "utf-8");
7827
+ }
6950
7828
  const retryCount = toRetryCount(options.retry, config.lang);
6951
7829
  let prUrl = options.pr?.trim() || "";
6952
7830
  let mergedAttempts;
6953
7831
  let syncChanged = false;
6954
7832
  if (options.create) {
7833
+ ensureNoTodoPlaceholders(body, tg(config.lang, "kindPr"), config.lang);
7834
+ ensurePrArtifacts(body, artifactPolicy, config.lang);
6955
7835
  assertRemoteApproval(
6956
7836
  options.confirm,
6957
7837
  tg(config.lang, "operationPrCreate"),
@@ -6995,7 +7875,7 @@ function githubCommand(program2) {
6995
7875
  }
6996
7876
  if (prUrl && options.syncTasks !== false) {
6997
7877
  const synced = syncTasksPrMetadata(
6998
- path17.join(config.docsDir, paths.tasksPath),
7878
+ path18.join(config.docsDir, paths.tasksPath),
6999
7879
  prUrl,
7000
7880
  "Review",
7001
7881
  config.lang
@@ -7046,6 +7926,10 @@ function githubCommand(program2) {
7046
7926
  labels,
7047
7927
  body,
7048
7928
  bodyFile,
7929
+ artifactPolicy: {
7930
+ screenshots: artifactPolicy.includeScreenshots,
7931
+ mermaid: artifactPolicy.includeMermaid
7932
+ },
7049
7933
  prUrl: prUrl || void 0,
7050
7934
  syncChanged,
7051
7935
  merged: !!options.merge,
@@ -7187,6 +8071,7 @@ function docsCommand(program2) {
7187
8071
  id,
7188
8072
  command: toBuiltinDocCommand(id)
7189
8073
  }));
8074
+ const contract = getGithubDraftContractForBuiltinDoc(docId, config.lang);
7190
8075
  if (options.json) {
7191
8076
  console.log(
7192
8077
  JSON.stringify(
@@ -7202,7 +8087,8 @@ function docsCommand(program2) {
7202
8087
  hash: loaded.hash,
7203
8088
  content: loaded.content
7204
8089
  },
7205
- requiredDocs: followups
8090
+ requiredDocs: followups,
8091
+ contract: contract || void 0
7206
8092
  },
7207
8093
  null,
7208
8094
  2
@@ -7210,7 +8096,7 @@ function docsCommand(program2) {
7210
8096
  );
7211
8097
  return;
7212
8098
  }
7213
- const relativeFromCwd = path17.relative(process.cwd(), loaded.entry.absolutePath);
8099
+ const relativeFromCwd = path18.relative(process.cwd(), loaded.entry.absolutePath);
7214
8100
  console.log();
7215
8101
  console.log(chalk6.bold(`\u{1F4C4} ${loaded.entry.id}: ${loaded.entry.title}`));
7216
8102
  console.log(
@@ -7300,13 +8186,13 @@ ${version}
7300
8186
  }
7301
8187
  return `${ascii}${footer}`;
7302
8188
  }
7303
- var CACHE_FILE = path17.join(os.homedir(), ".lee-spec-kit-version-cache.json");
8189
+ var CACHE_FILE = path18.join(os.homedir(), ".lee-spec-kit-version-cache.json");
7304
8190
  var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
7305
8191
  function getCurrentVersion() {
7306
8192
  try {
7307
- const packageJsonPath = path17.join(__dirname$1, "..", "package.json");
7308
- if (fs15.existsSync(packageJsonPath)) {
7309
- const pkg = fs15.readJsonSync(packageJsonPath);
8193
+ const packageJsonPath = path18.join(__dirname$1, "..", "package.json");
8194
+ if (fs14.existsSync(packageJsonPath)) {
8195
+ const pkg = fs14.readJsonSync(packageJsonPath);
7310
8196
  return pkg.version;
7311
8197
  }
7312
8198
  } catch {
@@ -7315,8 +8201,8 @@ function getCurrentVersion() {
7315
8201
  }
7316
8202
  function readCache() {
7317
8203
  try {
7318
- if (fs15.existsSync(CACHE_FILE)) {
7319
- return fs15.readJsonSync(CACHE_FILE);
8204
+ if (fs14.existsSync(CACHE_FILE)) {
8205
+ return fs14.readJsonSync(CACHE_FILE);
7320
8206
  }
7321
8207
  } catch {
7322
8208
  }
@@ -7404,9 +8290,9 @@ function shouldCheckForUpdates() {
7404
8290
  if (shouldCheckForUpdates()) checkForUpdates();
7405
8291
  function getCliVersion() {
7406
8292
  try {
7407
- const packageJsonPath = path17.join(__dirname$1, "..", "package.json");
7408
- if (fs15.existsSync(packageJsonPath)) {
7409
- const pkg = fs15.readJsonSync(packageJsonPath);
8293
+ const packageJsonPath = path18.join(__dirname$1, "..", "package.json");
8294
+ if (fs14.existsSync(packageJsonPath)) {
8295
+ const pkg = fs14.readJsonSync(packageJsonPath);
7410
8296
  if (pkg?.version) return String(pkg.version);
7411
8297
  }
7412
8298
  } catch {