lee-spec-kit 0.6.3 → 0.6.4

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/README.en.md CHANGED
@@ -217,6 +217,7 @@ Error payloads (`status: "error"`) include `reasonCode` and labeled `suggestions
217
217
  ### Built-in Docs
218
218
 
219
219
  If you do not restore `agents.md` into project docs, fetch CLI-managed guides directly:
220
+ `docs get create-issue|issue-template|create-pr|pr-template --json` also returns `contract` (required sections / artifact rules).
220
221
 
221
222
  ```bash
222
223
  # list built-in docs
@@ -285,6 +286,9 @@ npx lee-spec-kit github issue F001 --create --confirm OK --labels enhancement,fr
285
286
  # Generate PR body
286
287
  npx lee-spec-kit github pr F001
287
288
 
289
+ # Generate PR body (force screenshots/Mermaid sections)
290
+ npx lee-spec-kit github pr F001 --screenshots on --mermaid on
291
+
288
292
  # Generate + create PR + sync tasks.md metadata + merge with retry
289
293
  npx lee-spec-kit github pr F001 --create --merge --confirm OK --labels enhancement,frontend
290
294
  ```
@@ -295,6 +299,7 @@ Key points:
295
299
  - Labels are validated (at least one required).
296
300
  - `--create`/`--merge` are remote operations and require `--confirm OK`.
297
301
  - PR helper can sync `tasks.md` PR URL/PR Status automatically (`--no-sync-tasks` to skip).
302
+ - PR artifact sections are controlled by `--screenshots (auto|on|off)` and `--mermaid (auto|on|off)`.
298
303
  - Merge includes retry and automatic head-branch refresh (fetch/rebase/force-push) on out-of-date failures.
299
304
 
300
305
  ### Status
package/README.md CHANGED
@@ -239,6 +239,7 @@ npx lee-spec-kit context F001 --approve A --execute --execute-strict
239
239
  ### CLI 내장 문서 조회
240
240
 
241
241
  `agents.md`를 프로젝트에 복구하지 않는 환경에서는 아래 명령으로 내장 가이드를 직접 조회할 수 있습니다.
242
+ `docs get create-issue|issue-template|create-pr|pr-template --json` 응답에는 `contract`(필수 섹션/아티팩트 규칙)도 포함됩니다.
242
243
 
243
244
  ```bash
244
245
  # 조회 가능한 내장 문서 목록
@@ -307,6 +308,9 @@ npx lee-spec-kit github issue F001 --create --confirm OK --labels enhancement,fr
307
308
  # PR 본문 생성
308
309
  npx lee-spec-kit github pr F001
309
310
 
311
+ # PR 본문 생성 (스크린샷/Mermaid 섹션 강제 포함)
312
+ npx lee-spec-kit github pr F001 --screenshots on --mermaid on
313
+
310
314
  # PR 본문 생성 + PR 생성 + tasks.md 메타데이터 동기화 + merge 재시도
311
315
  npx lee-spec-kit github pr F001 --create --merge --confirm OK --labels enhancement,frontend
312
316
  ```
@@ -317,6 +321,7 @@ npx lee-spec-kit github pr F001 --create --merge --confirm OK --labels enhanceme
317
321
  - 라벨은 최소 1개 이상 필수입니다.
318
322
  - `--create`/`--merge`는 원격 작업이므로 `--confirm OK`가 필요합니다.
319
323
  - PR helper는 기본적으로 `tasks.md`의 `PR`/`PR Status`를 동기화합니다. (`--no-sync-tasks`로 비활성화)
324
+ - PR helper의 아티팩트 섹션은 `--screenshots (auto|on|off)`, `--mermaid (auto|on|off)`로 제어할 수 있습니다.
320
325
  - merge 시 head 브랜치가 뒤쳐진 경우 fetch/rebase/force-push 후 재시도합니다.
321
326
 
322
327
  ### 상태 확인
package/dist/index.js CHANGED
@@ -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.",
@@ -4673,6 +4693,25 @@ function listLabels(actionOptions) {
4673
4693
  if (actionOptions.length === 0) return "-";
4674
4694
  return actionOptions.map((o) => o.label).join(", ");
4675
4695
  }
4696
+ function resolveFeatureRefForApproval(state, featureName) {
4697
+ const raw = featureName?.trim() || state.matchedFeature?.folderName || "<slug|F001|F001-slug>";
4698
+ return raw;
4699
+ }
4700
+ function buildApprovalCommand(state, featureName, selectedComponent, execute) {
4701
+ const featureRef = resolveFeatureRefForApproval(state, featureName);
4702
+ const componentArg = selectedComponent ? ` --component ${selectedComponent}` : "";
4703
+ const executeArg = execute ? " --execute" : "";
4704
+ return `npx lee-spec-kit context ${featureRef}${componentArg} --approve <LABEL>${executeArg}`;
4705
+ }
4706
+ function buildFinalApprovalPrompt(lang, actionOptions) {
4707
+ if (actionOptions.length === 0) return "";
4708
+ const labels = listLabels(actionOptions);
4709
+ const example = actionOptions[0]?.label || "A";
4710
+ return tr(lang, "cli", "context.finalLabelPrompt", {
4711
+ labels,
4712
+ example
4713
+ });
4714
+ }
4676
4715
  function formatActionSummary2(action) {
4677
4716
  if (action.type === "command") {
4678
4717
  return `(${action.scope}) ${action.cmd}`;
@@ -4848,6 +4887,19 @@ async function runContext(featureName, options) {
4848
4887
  }
4849
4888
  if (options.json) {
4850
4889
  const primaryAction = state.actionOptions[0] ?? null;
4890
+ const finalApprovalPrompt = buildFinalApprovalPrompt(lang, state.actionOptions);
4891
+ const approveCommand = buildApprovalCommand(
4892
+ state,
4893
+ featureName,
4894
+ selectedComponent,
4895
+ false
4896
+ );
4897
+ const executeCommand = buildApprovalCommand(
4898
+ state,
4899
+ featureName,
4900
+ selectedComponent,
4901
+ true
4902
+ );
4851
4903
  const result = {
4852
4904
  status: state.status,
4853
4905
  reasonCode: toReasonCode(state.status),
@@ -4888,6 +4940,10 @@ async function runContext(featureName, options) {
4888
4940
  },
4889
4941
  approvalRequest: {
4890
4942
  guidance: "Present each label with summary (e.g. `A: <summary>`) before asking for approval.",
4943
+ finalPrompt: finalApprovalPrompt,
4944
+ labels: state.actionOptions.map((o) => o.label),
4945
+ approveCommand,
4946
+ executeCommand,
4891
4947
  options: state.actionOptions.map((o) => ({
4892
4948
  label: o.label,
4893
4949
  summary: o.summary,
@@ -5151,6 +5207,23 @@ async function runContext(featureName, options) {
5151
5207
  );
5152
5208
  }
5153
5209
  }
5210
+ if (actionOptions.length > 0) {
5211
+ const finalApprovalPrompt = buildFinalApprovalPrompt(lang, actionOptions);
5212
+ const executeCommand = buildApprovalCommand(
5213
+ state,
5214
+ featureName,
5215
+ selectedComponent,
5216
+ true
5217
+ );
5218
+ console.log(chalk6.cyan(` \u21B3 ${finalApprovalPrompt}`));
5219
+ console.log(
5220
+ chalk6.gray(
5221
+ ` \u21B3 ${tr(lang, "cli", "context.finalLabelCommandHint", {
5222
+ command: executeCommand
5223
+ })}`
5224
+ )
5225
+ );
5226
+ }
5154
5227
  console.log();
5155
5228
  }
5156
5229
  async function runApprovedOption(state, config, lang, featureName, selectionOptions, options) {
@@ -6245,6 +6318,70 @@ async function runFlow(featureName, options) {
6245
6318
  console.log(chalk6.gray("Tip: add --approve <LABEL> [--execute] to run the selected atomic action."));
6246
6319
  console.log();
6247
6320
  }
6321
+
6322
+ // src/utils/github-draft-contract.ts
6323
+ var ISSUE_CONTRACT = {
6324
+ kind: "issue",
6325
+ requiredSections: {
6326
+ ko: ["\uAC1C\uC694", "\uBAA9\uD45C", "\uC644\uB8CC \uAE30\uC900", "\uAD00\uB828 \uBB38\uC11C", "\uB77C\uBCA8"],
6327
+ en: ["Overview", "Goals", "Completion Criteria", "Related Documents", "Labels"]
6328
+ },
6329
+ artifacts: []
6330
+ };
6331
+ var PR_CONTRACT = {
6332
+ kind: "pr",
6333
+ requiredSections: {
6334
+ ko: ["\uAC1C\uC694", "\uBCC0\uACBD \uC0AC\uD56D", "\uD14C\uC2A4\uD2B8", "\uAD00\uB828 \uBB38\uC11C"],
6335
+ en: ["Overview", "Changes", "Tests", "Related Documents"]
6336
+ },
6337
+ artifacts: [
6338
+ {
6339
+ id: "screenshots",
6340
+ headings: { ko: "\uC2A4\uD06C\uB9B0\uC0F7", en: "Screenshots" },
6341
+ bodyRule: "image-markdown"
6342
+ },
6343
+ {
6344
+ id: "mermaid",
6345
+ headings: { ko: "\uC544\uD0A4\uD14D\uCC98 \uB2E4\uC774\uC5B4\uADF8\uB7A8", en: "Architecture Diagram" },
6346
+ bodyRule: "mermaid-fence"
6347
+ }
6348
+ ]
6349
+ };
6350
+ var CONTRACTS = {
6351
+ issue: ISSUE_CONTRACT,
6352
+ pr: PR_CONTRACT
6353
+ };
6354
+ function getGithubDraftRequiredSections(kind, lang) {
6355
+ return [...CONTRACTS[kind].requiredSections[lang]];
6356
+ }
6357
+ function getGithubDraftArtifactHeading(kind, artifactId, lang) {
6358
+ const artifact = CONTRACTS[kind].artifacts.find((item) => item.id === artifactId);
6359
+ if (!artifact) return null;
6360
+ return artifact.headings[lang];
6361
+ }
6362
+ function getGithubDraftContractView(kind, lang) {
6363
+ const definition = CONTRACTS[kind];
6364
+ return {
6365
+ kind: definition.kind,
6366
+ requiredSections: [...definition.requiredSections[lang]],
6367
+ artifacts: definition.artifacts.map((artifact) => ({
6368
+ id: artifact.id,
6369
+ section: artifact.headings[lang],
6370
+ bodyRule: artifact.bodyRule
6371
+ }))
6372
+ };
6373
+ }
6374
+ function getGithubDraftContractForBuiltinDoc(docId, lang) {
6375
+ if (docId === "create-issue" || docId === "issue-template") {
6376
+ return getGithubDraftContractView("issue", lang);
6377
+ }
6378
+ if (docId === "create-pr" || docId === "pr-template") {
6379
+ return getGithubDraftContractView("pr", lang);
6380
+ }
6381
+ return null;
6382
+ }
6383
+
6384
+ // src/commands/github.ts
6248
6385
  function tg(lang, key, vars = {}) {
6249
6386
  return tr(lang, "cli", `github.${key}`, vars);
6250
6387
  }
@@ -6402,6 +6539,55 @@ function toBodyFilePath(raw, kind, docsDir, component) {
6402
6539
  const selected = raw?.trim() || path17.join(os.tmpdir(), buildDefaultBodyFileName(kind, docsDir, component));
6403
6540
  return path17.resolve(selected);
6404
6541
  }
6542
+ function toProjectRootDocsPath(relativePathFromDocs) {
6543
+ const normalized = relativePathFromDocs.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
6544
+ if (normalized.startsWith("docs/")) return normalized;
6545
+ return `docs/${normalized}`;
6546
+ }
6547
+ function toBodyDocPaths(paths) {
6548
+ return {
6549
+ ...paths,
6550
+ specPath: toProjectRootDocsPath(paths.specPath),
6551
+ planPath: toProjectRootDocsPath(paths.planPath),
6552
+ tasksPath: toProjectRootDocsPath(paths.tasksPath)
6553
+ };
6554
+ }
6555
+ var TODO_PLACEHOLDER_PATTERN = /(^|\n)\s*-\s*\[[ xX]\]\s*TODO:/m;
6556
+ function ensureNoTodoPlaceholders(body, kind, lang) {
6557
+ if (!TODO_PLACEHOLDER_PATTERN.test(body)) return;
6558
+ throw createCliError(
6559
+ "PRECONDITION_FAILED",
6560
+ tg(lang, "todoPlaceholdersRemain", { kind })
6561
+ );
6562
+ }
6563
+ function parsePrArtifactMode(raw, kind, lang) {
6564
+ const value = (raw || "auto").trim().toLowerCase();
6565
+ if (value === "auto" || value === "on" || value === "off") {
6566
+ return value;
6567
+ }
6568
+ throw createCliError(
6569
+ "INVALID_ARGUMENT",
6570
+ tg(lang, "artifactModeInvalid", { kind, value })
6571
+ );
6572
+ }
6573
+ function resolvePrArtifactPolicy(config, feature, options) {
6574
+ const screenshotsMode = parsePrArtifactMode(
6575
+ options.screenshots,
6576
+ "screenshots",
6577
+ config.lang
6578
+ );
6579
+ const mermaidMode = parsePrArtifactMode(
6580
+ options.mermaid,
6581
+ "mermaid",
6582
+ config.lang
6583
+ );
6584
+ const includeScreenshots = screenshotsMode === "on" ? true : screenshotsMode === "off" ? false : config.pr?.screenshots?.upload ?? false;
6585
+ const includeMermaid = mermaidMode === "on" ? true : mermaidMode === "off" ? false : feature.type === "be";
6586
+ return {
6587
+ includeScreenshots,
6588
+ includeMermaid
6589
+ };
6590
+ }
6405
6591
  async function resolveFeatureOrThrow(featureName, options, lang) {
6406
6592
  const config = await getConfig(process.cwd());
6407
6593
  if (!config) {
@@ -6440,27 +6626,38 @@ function getFeatureDocPaths(feature) {
6440
6626
  function normalizeHeading(value) {
6441
6627
  return value.trim().replace(/\s+/g, " ").toLowerCase();
6442
6628
  }
6443
- function extractMarkdownSection(content, headings) {
6629
+ function extractMarkdownByHeadings(content, headings, levels) {
6444
6630
  const targets = new Set(headings.map((heading) => normalizeHeading(heading)));
6445
6631
  const lines = content.split("\n");
6446
6632
  let start = -1;
6633
+ let startLevel = 0;
6634
+ const levelSet = new Set(levels);
6447
6635
  for (let i = 0; i < lines.length; i++) {
6448
- const match = lines[i].match(/^\s*##\s+(.+?)\s*$/);
6636
+ const match = lines[i].match(/^\s*(#{2,6})\s+(.+?)\s*$/);
6449
6637
  if (!match) continue;
6450
- if (!targets.has(normalizeHeading(match[1]))) continue;
6638
+ const level = match[1].length;
6639
+ if (!levelSet.has(level)) continue;
6640
+ if (!targets.has(normalizeHeading(match[2]))) continue;
6451
6641
  start = i + 1;
6642
+ startLevel = level;
6452
6643
  break;
6453
6644
  }
6454
6645
  if (start < 0) return void 0;
6455
6646
  let end = lines.length;
6456
6647
  for (let i = start; i < lines.length; i++) {
6457
- if (/^\s*##\s+/.test(lines[i])) {
6648
+ const heading = lines[i].match(/^\s*(#{2,6})\s+(.+?)\s*$/);
6649
+ if (!heading) continue;
6650
+ const level = heading[1].length;
6651
+ if (level <= startLevel) {
6458
6652
  end = i;
6459
6653
  break;
6460
6654
  }
6461
6655
  }
6462
6656
  return lines.slice(start, end).join("\n");
6463
6657
  }
6658
+ function extractMarkdownSection(content, headings) {
6659
+ return extractMarkdownByHeadings(content, headings, [2]);
6660
+ }
6464
6661
  function isTemplateLine(line) {
6465
6662
  const trimmed = line.trim();
6466
6663
  if (!trimmed) return true;
@@ -6476,6 +6673,337 @@ function sanitizeOverviewSection(raw) {
6476
6673
  if (lines.length === 0) return void 0;
6477
6674
  return lines.slice(0, 5).join("\n");
6478
6675
  }
6676
+ function sanitizeDraftItem(raw) {
6677
+ const trimmed = raw.replace(/^\s*-\s*\[[ xX]\]\s*/, "").replace(/^\s*-\s+/, "").replace(/^\s*###\s+/, "").trim();
6678
+ const plain = trimmed.replace(/\*\*/g, "").trim();
6679
+ if (!plain) return void 0;
6680
+ if (isTemplateLine(plain)) return void 0;
6681
+ if (/^todo:/i.test(plain)) return void 0;
6682
+ if (/\{[^}]*\}/.test(plain)) return void 0;
6683
+ if (/^(as a|i want|so that)\b/i.test(plain)) return void 0;
6684
+ if (/^acceptance criteria:?$/i.test(plain)) return void 0;
6685
+ return plain.replace(/\s+/g, " ");
6686
+ }
6687
+ function uniqItems(items) {
6688
+ const seen = /* @__PURE__ */ new Set();
6689
+ const ordered = [];
6690
+ for (const item of items) {
6691
+ const normalized = item.trim().toLowerCase();
6692
+ if (!normalized || seen.has(normalized)) continue;
6693
+ seen.add(normalized);
6694
+ ordered.push(item.trim());
6695
+ }
6696
+ return ordered;
6697
+ }
6698
+ function normalizeSemanticKey(value) {
6699
+ return value.toLowerCase().replace(/[`'"(){}\[\].,:;!?/\\\-_|]/g, " ").replace(/\s+/g, " ").trim().replace(/\s/g, "");
6700
+ }
6701
+ function uniqItemsByContainment(items) {
6702
+ const kept = [];
6703
+ const keys = [];
6704
+ for (const item of items) {
6705
+ const clean = item.trim();
6706
+ if (!clean) continue;
6707
+ const key = normalizeSemanticKey(clean);
6708
+ if (!key) continue;
6709
+ let replaced = false;
6710
+ for (let i = 0; i < keys.length; i++) {
6711
+ const current = keys[i];
6712
+ if (!current.includes(key) && !key.includes(current)) continue;
6713
+ if (key.length > current.length) {
6714
+ keys[i] = key;
6715
+ kept[i] = clean;
6716
+ }
6717
+ replaced = true;
6718
+ break;
6719
+ }
6720
+ if (!replaced) {
6721
+ keys.push(key);
6722
+ kept.push(clean);
6723
+ }
6724
+ }
6725
+ return kept;
6726
+ }
6727
+ function extractSectionLines(raw) {
6728
+ if (!raw) return [];
6729
+ return uniqItems(
6730
+ raw.split("\n").map((line) => sanitizeDraftItem(line)).filter((line) => !!line)
6731
+ );
6732
+ }
6733
+ function extractSectionHeadings(raw) {
6734
+ if (!raw) return [];
6735
+ return uniqItems(
6736
+ 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())
6737
+ );
6738
+ }
6739
+ function toChecklistLines(items) {
6740
+ return items.map((item) => `- [ ] ${item}`).join("\n");
6741
+ }
6742
+ function extractChecklistItems(raw) {
6743
+ if (!raw) return [];
6744
+ return uniqItems(
6745
+ raw.split("\n").map((line) => {
6746
+ const match = line.match(/^\s*-\s*\[[ xX]\]\s+(.+?)\s*$/);
6747
+ if (!match) return void 0;
6748
+ return sanitizeDraftItem(match[1]);
6749
+ }).filter((line) => !!line)
6750
+ );
6751
+ }
6752
+ function extractTaskTitles(tasksContent) {
6753
+ return uniqItems(
6754
+ tasksContent.split("\n").map((line) => {
6755
+ const match = line.match(
6756
+ /^\s*-\s*\[(?:TODO|DOING|DONE)\][^\n]*?\s+(?:T-[A-Z0-9-]+\s+)?(.+?)\s*$/
6757
+ );
6758
+ if (!match) return void 0;
6759
+ return sanitizeDraftItem(match[1]);
6760
+ }).filter((line) => !!line)
6761
+ );
6762
+ }
6763
+ function extractTasksAcceptanceItems(tasksContent) {
6764
+ const lines = tasksContent.split("\n");
6765
+ const accepted = [];
6766
+ let inAcceptance = false;
6767
+ for (const line of lines) {
6768
+ if (/^\s*-\s*Acceptance\s*:\s*$/i.test(line)) {
6769
+ inAcceptance = true;
6770
+ continue;
6771
+ }
6772
+ if (inAcceptance && /^\s*-\s*Checklist\s*:\s*$/i.test(line)) {
6773
+ inAcceptance = false;
6774
+ continue;
6775
+ }
6776
+ if (!inAcceptance) continue;
6777
+ const match = line.match(/^\s*-\s*\[[ xX]\]\s+(.+?)\s*$/) || line.match(/^\s*-\s+(.+?)\s*$/);
6778
+ if (!match) continue;
6779
+ const item = sanitizeDraftItem(match[1]);
6780
+ if (!item) continue;
6781
+ accepted.push(item);
6782
+ }
6783
+ return uniqItems(accepted);
6784
+ }
6785
+ function extractScopeItemsFromPlan(planContent, lang) {
6786
+ const section = extractMarkdownSection(
6787
+ planContent,
6788
+ ["\uBC94\uC704(\uBA85\uD655\uD654)", "\uBC94\uC704", "Scope", "Scope Clarification"]
6789
+ );
6790
+ if (!section) return { include: [], exclude: [] };
6791
+ const lines = section.split("\n");
6792
+ const include = [];
6793
+ const exclude = [];
6794
+ let mode = null;
6795
+ const includePatterns = lang === "ko" ? [/포함/] : [/in\s*scope/i, /^include$/i, /included/i];
6796
+ const excludePatterns = lang === "ko" ? [/비포함/, /제외/] : [/out\s*of\s*scope/i, /^exclude$/i, /excluded/i];
6797
+ for (const line of lines) {
6798
+ const plain = line.replace(/\*\*/g, "").trim();
6799
+ if (!plain) continue;
6800
+ if (excludePatterns.some((re) => re.test(plain))) {
6801
+ mode = "exclude";
6802
+ continue;
6803
+ }
6804
+ if (includePatterns.some((re) => re.test(plain))) {
6805
+ mode = "include";
6806
+ continue;
6807
+ }
6808
+ const bullet = line.match(/^\s*-\s+(.+?)\s*$/)?.[1];
6809
+ if (!bullet) continue;
6810
+ const item = sanitizeDraftItem(bullet);
6811
+ if (!item) continue;
6812
+ if (mode === "include") {
6813
+ include.push(item);
6814
+ } else if (mode === "exclude") {
6815
+ exclude.push(item);
6816
+ }
6817
+ }
6818
+ return {
6819
+ include: uniqItems(include),
6820
+ exclude: uniqItems(exclude)
6821
+ };
6822
+ }
6823
+ function getIssueGoalsAndCriteria(specContent, planContent, tasksContent, overview, lang) {
6824
+ const purposeLines = extractSectionLines(
6825
+ extractMarkdownSection(specContent, ["\uBAA9\uC801", "Purpose"])
6826
+ );
6827
+ const requirementHeadings = extractSectionHeadings(
6828
+ extractMarkdownSection(specContent, ["\uAE30\uB2A5 \uC694\uAD6C\uC0AC\uD56D", "Functional Requirements"])
6829
+ );
6830
+ const userStoryChecklist = extractChecklistItems(
6831
+ extractMarkdownSection(specContent, ["\uC0AC\uC6A9\uC790 \uC2A4\uD1A0\uB9AC", "User Stories"])
6832
+ );
6833
+ const tasksAcceptance = extractTasksAcceptanceItems(tasksContent);
6834
+ const scopeFromPlan = extractScopeItemsFromPlan(planContent, lang);
6835
+ const taskTitles = extractTaskTitles(tasksContent);
6836
+ const goals = uniqItemsByContainment(uniqItems([
6837
+ ...requirementHeadings,
6838
+ ...scopeFromPlan.include,
6839
+ ...purposeLines.slice(0, 1),
6840
+ sanitizeDraftItem(overview) || ""
6841
+ ])).slice(0, 5);
6842
+ while (goals.length < 3) {
6843
+ goals.push(
6844
+ 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."
6845
+ );
6846
+ }
6847
+ const criteria = uniqItemsByContainment(
6848
+ uniqItems([...userStoryChecklist, ...tasksAcceptance])
6849
+ ).slice(0, 6);
6850
+ while (criteria.length < 4) {
6851
+ criteria.push(
6852
+ 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."
6853
+ );
6854
+ }
6855
+ const scope = uniqItemsByContainment(
6856
+ uniqItems([...scopeFromPlan.include, ...taskTitles])
6857
+ ).slice(0, 6);
6858
+ return {
6859
+ goals: goals.slice(0, 5),
6860
+ criteria: criteria.slice(0, 6),
6861
+ scope
6862
+ };
6863
+ }
6864
+ function extractPlanChangeTargets(planContent, lang) {
6865
+ const section = extractMarkdownSection(
6866
+ planContent,
6867
+ [
6868
+ "\uBCC0\uACBD \uB300\uC0C1(\uC608\uC0C1)",
6869
+ "\uBCC0\uACBD \uB300\uC0C1",
6870
+ "Changed Files",
6871
+ "Change Targets",
6872
+ "Expected Changes"
6873
+ ]
6874
+ );
6875
+ if (!section) return [];
6876
+ const lines = section.split("\n");
6877
+ const out = [];
6878
+ let inCode = false;
6879
+ for (const line of lines) {
6880
+ if (/^\s*```/.test(line)) {
6881
+ inCode = !inCode;
6882
+ continue;
6883
+ }
6884
+ if (inCode) {
6885
+ const trimmed = line.trim();
6886
+ if (!trimmed) continue;
6887
+ if (!/[\\/]/.test(trimmed)) continue;
6888
+ out.push(
6889
+ lang === "ko" ? `\`${trimmed}\` \uBCC0\uACBD` : `Update \`${trimmed}\``
6890
+ );
6891
+ continue;
6892
+ }
6893
+ const bullet = line.match(/^\s*-\s+(.+?)\s*$/)?.[1];
6894
+ if (!bullet) continue;
6895
+ const item = sanitizeDraftItem(bullet);
6896
+ if (!item) continue;
6897
+ out.push(item);
6898
+ }
6899
+ return uniqItemsByContainment(uniqItems(out));
6900
+ }
6901
+ function extractCommandsFromSection(raw) {
6902
+ if (!raw) return [];
6903
+ const commands = [];
6904
+ for (const match of raw.matchAll(/`([^`]+)`/g)) {
6905
+ const candidate = match[1].trim();
6906
+ if (!candidate) continue;
6907
+ if (/\{[^}]*\}/.test(candidate)) continue;
6908
+ if (!/\b(pnpm|npm|yarn|bun|vitest|jest|tsx?|node)\b/.test(candidate)) continue;
6909
+ commands.push(candidate);
6910
+ }
6911
+ for (const line of raw.split("\n")) {
6912
+ const trimmed = line.trim().replace(/^-+\s*/, "");
6913
+ if (!trimmed) continue;
6914
+ if (!/\b(pnpm|npm|yarn|bun|vitest|jest|tsx?|node)\b/.test(trimmed)) continue;
6915
+ if (/\{[^}]*\}/.test(trimmed)) continue;
6916
+ commands.push(trimmed);
6917
+ }
6918
+ return uniqItems(commands);
6919
+ }
6920
+ function extractRecordedTestLines(tasksContent, planContent, lang) {
6921
+ const section = extractMarkdownByHeadings(
6922
+ tasksContent,
6923
+ ["\uD14C\uC2A4\uD2B8 \uC2E4\uD589 \uAE30\uB85D", "Tests Run", "Test Run Log", "Test Execution Log"],
6924
+ [3, 2]
6925
+ );
6926
+ const records = [];
6927
+ if (section) {
6928
+ for (const line of section.split("\n")) {
6929
+ if (!line.trim().startsWith("|")) continue;
6930
+ const cells = line.split("|").slice(1, -1).map((cell) => cell.trim().replace(/`/g, ""));
6931
+ if (cells.length < 3) continue;
6932
+ const [cmd, time, result] = cells;
6933
+ if (!cmd || /^명령어$/i.test(cmd) || /^command$/i.test(cmd) || /^-+$/.test(cmd)) continue;
6934
+ if (/\{[^}]*\}/.test(cmd) || /\{[^}]*\}/.test(result || "")) continue;
6935
+ const renderedResult = result || (lang === "ko" ? "\uBBF8\uAE30\uB85D" : "not recorded");
6936
+ const renderedTime = time && time !== "-" ? ` (${time})` : "";
6937
+ records.push(`\`${cmd}\` \u2014 ${renderedResult}${renderedTime}`);
6938
+ }
6939
+ const lines = section.split("\n");
6940
+ for (let i = 0; i < lines.length; i++) {
6941
+ const commandMatch = lines[i].match(/^\s*-\s*(?:명령어|Command)\s*:\s*`?([^`]+?)`?\s*$/i);
6942
+ if (!commandMatch) continue;
6943
+ const command = commandMatch[1].trim();
6944
+ if (!command || /\{[^}]*\}/.test(command)) continue;
6945
+ let result = "";
6946
+ for (let j = i + 1; j < Math.min(lines.length, i + 5); j++) {
6947
+ const resultMatch = lines[j].match(/^\s*-\s*(?:결과|Result)\s*:\s*(.+?)\s*$/i);
6948
+ if (!resultMatch) continue;
6949
+ result = resultMatch[1].replace(/`/g, "").trim();
6950
+ break;
6951
+ }
6952
+ if (!result || /\{[^}]*\}/.test(result)) continue;
6953
+ records.push(`\`${command}\` \u2014 ${result}`);
6954
+ }
6955
+ }
6956
+ if (records.length > 0) {
6957
+ return uniqItemsByContainment(uniqItems(records));
6958
+ }
6959
+ const plannedCommands = extractCommandsFromSection(
6960
+ extractMarkdownByHeadings(
6961
+ planContent,
6962
+ ["\uAC80\uC99D \uBA85\uB839(\uC608\uC815)", "Validation Commands", "Verification Commands"],
6963
+ [3, 2]
6964
+ )
6965
+ );
6966
+ if (plannedCommands.length > 0) {
6967
+ return plannedCommands.map(
6968
+ (command) => lang === "ko" ? `\`${command}\` \u2014 \uC2E4\uD589 \uACB0\uACFC\uB97C \uAE30\uB85D\uD558\uC138\uC694.` : `\`${command}\` \u2014 record execution result.`
6969
+ );
6970
+ }
6971
+ return [];
6972
+ }
6973
+ function getPrChangesAndTests(specContent, planContent, tasksContent, overview, lang) {
6974
+ const requirementHeadings = extractSectionHeadings(
6975
+ extractMarkdownSection(specContent, ["\uAE30\uB2A5 \uC694\uAD6C\uC0AC\uD56D", "Functional Requirements"])
6976
+ );
6977
+ const scopeFromPlan = extractScopeItemsFromPlan(planContent, lang).include;
6978
+ const planTargets = extractPlanChangeTargets(planContent, lang);
6979
+ const taskTitles = extractTaskTitles(tasksContent);
6980
+ const changes = uniqItemsByContainment(
6981
+ uniqItems([
6982
+ ...taskTitles,
6983
+ ...scopeFromPlan,
6984
+ ...requirementHeadings,
6985
+ ...planTargets,
6986
+ sanitizeDraftItem(overview) || ""
6987
+ ])
6988
+ ).slice(0, 6);
6989
+ while (changes.length < 3) {
6990
+ changes.push(
6991
+ 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."
6992
+ );
6993
+ }
6994
+ const tests = uniqItemsByContainment(
6995
+ uniqItems(extractRecordedTestLines(tasksContent, planContent, lang))
6996
+ ).slice(0, 4);
6997
+ while (tests.length < 2) {
6998
+ tests.push(
6999
+ 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."
7000
+ );
7001
+ }
7002
+ return {
7003
+ changes: changes.slice(0, 6),
7004
+ tests: tests.slice(0, 4)
7005
+ };
7006
+ }
6479
7007
  function resolveOverviewFromSpec(specContent, feature, lang) {
6480
7008
  const fromPurpose = sanitizeOverviewSection(
6481
7009
  extractMarkdownSection(specContent, ["\uBAA9\uC801", "Purpose"])
@@ -6487,7 +7015,112 @@ function resolveOverviewFromSpec(specContent, feature, lang) {
6487
7015
  if (fromOverview) return fromOverview;
6488
7016
  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
7017
  }
6490
- function buildIssueBody(overview, labels, paths, lang) {
7018
+ function getPrScreenshotsHeading(lang) {
7019
+ return getGithubDraftArtifactHeading("pr", "screenshots", lang) || (lang === "ko" ? "\uC2A4\uD06C\uB9B0\uC0F7" : "Screenshots");
7020
+ }
7021
+ function getPrMermaidHeading(lang) {
7022
+ return getGithubDraftArtifactHeading("pr", "mermaid", lang) || (lang === "ko" ? "\uC544\uD0A4\uD14D\uCC98 \uB2E4\uC774\uC5B4\uADF8\uB7A8" : "Architecture Diagram");
7023
+ }
7024
+ function buildPrScreenshotsSection(lang) {
7025
+ if (lang === "ko") {
7026
+ return `
7027
+ ## \uC2A4\uD06C\uB9B0\uC0F7
7028
+
7029
+ - [ ] \uC5C5\uB85C\uB4DC\uD55C \uC2A4\uD06C\uB9B0\uC0F7 URL\uC744 \uD3EC\uD568\uD558\uC138\uC694. (\uC608: \`![](https://...)\`)
7030
+ `;
7031
+ }
7032
+ return `
7033
+ ## Screenshots
7034
+
7035
+ - [ ] Include uploaded screenshot URL(s). (e.g. \`![](https://...)\`)
7036
+ `;
7037
+ }
7038
+ function buildPrMermaidSection(lang) {
7039
+ if (lang === "ko") {
7040
+ return `
7041
+ ## \uC544\uD0A4\uD14D\uCC98 \uB2E4\uC774\uC5B4\uADF8\uB7A8
7042
+
7043
+ \`\`\`mermaid
7044
+ sequenceDiagram
7045
+ participant Client
7046
+ participant API
7047
+ participant Service
7048
+ participant DB
7049
+ Client->>API: Request
7050
+ API->>Service: Execute
7051
+ Service->>DB: Query/Command
7052
+ DB-->>Service: Result
7053
+ Service-->>API: Response DTO
7054
+ API-->>Client: Response
7055
+ \`\`\`
7056
+ `;
7057
+ }
7058
+ return `
7059
+ ## Architecture Diagram
7060
+
7061
+ \`\`\`mermaid
7062
+ sequenceDiagram
7063
+ participant Client
7064
+ participant API
7065
+ participant Service
7066
+ participant DB
7067
+ Client->>API: Request
7068
+ API->>Service: Execute
7069
+ Service->>DB: Query/Command
7070
+ DB-->>Service: Result
7071
+ Service-->>API: Response DTO
7072
+ API-->>Client: Response
7073
+ \`\`\`
7074
+ `;
7075
+ }
7076
+ function ensurePrArtifacts(body, policy, lang) {
7077
+ if (policy.includeScreenshots) {
7078
+ const heading = getPrScreenshotsHeading(lang);
7079
+ const section = extractMarkdownByHeadings(body, [heading], [2]);
7080
+ if (!section) {
7081
+ throw createCliError(
7082
+ "PRECONDITION_FAILED",
7083
+ tg(lang, "prScreenshotsSectionMissing", { section: heading })
7084
+ );
7085
+ }
7086
+ if (!/!\[[^\]]*]\((?!\s*\))[^)]+\)/m.test(section)) {
7087
+ throw createCliError(
7088
+ "PRECONDITION_FAILED",
7089
+ tg(lang, "prScreenshotImageMissing", { section: heading })
7090
+ );
7091
+ }
7092
+ }
7093
+ if (policy.includeMermaid) {
7094
+ const heading = getPrMermaidHeading(lang);
7095
+ const section = extractMarkdownByHeadings(body, [heading], [2]);
7096
+ if (!section) {
7097
+ throw createCliError(
7098
+ "PRECONDITION_FAILED",
7099
+ tg(lang, "prMermaidSectionMissing", { section: heading })
7100
+ );
7101
+ }
7102
+ if (!/```mermaid[\s\S]*?```/m.test(section)) {
7103
+ throw createCliError(
7104
+ "PRECONDITION_FAILED",
7105
+ tg(lang, "prMermaidBlockMissing", { section: heading })
7106
+ );
7107
+ }
7108
+ }
7109
+ }
7110
+ function buildIssueBody(specContent, planContent, tasksContent, overview, labels, paths, lang) {
7111
+ const bodyPaths = toBodyDocPaths(paths);
7112
+ const draft = getIssueGoalsAndCriteria(specContent, planContent, tasksContent, overview, lang);
7113
+ const goals = toChecklistLines(draft.goals);
7114
+ const criteria = toChecklistLines(draft.criteria);
7115
+ const scopeSection = draft.scope.length > 0 ? lang === "ko" ? `
7116
+ ## \uC791\uC5C5 \uBC94\uC704(\uC608\uC815)
7117
+
7118
+ ${draft.scope.map((item) => `- ${item}`).join("\n")}
7119
+ ` : `
7120
+ ## Planned Scope
7121
+
7122
+ ${draft.scope.map((item) => `- ${item}`).join("\n")}
7123
+ ` : "";
6491
7124
  if (lang === "ko") {
6492
7125
  return `## \uAC1C\uC694
6493
7126
 
@@ -6495,19 +7128,18 @@ ${overview}
6495
7128
 
6496
7129
  ## \uBAA9\uD45C
6497
7130
 
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.
7131
+ ${goals}
6500
7132
 
6501
7133
  ## \uC644\uB8CC \uAE30\uC900
6502
7134
 
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.
7135
+ ${criteria}
7136
+ ${scopeSection}
6505
7137
 
6506
7138
  ## \uAD00\uB828 \uBB38\uC11C
6507
7139
 
6508
- - **Spec**: \`${paths.specPath}\`
6509
- - **Plan**: \`${paths.planPath}\`
6510
- - **Tasks**: \`${paths.tasksPath}\`
7140
+ - **Spec**: \`${bodyPaths.specPath}\`
7141
+ - **Plan**: \`${bodyPaths.planPath}\`
7142
+ - **Tasks**: \`${bodyPaths.tasksPath}\`
6511
7143
 
6512
7144
  ## \uB77C\uBCA8
6513
7145
 
@@ -6520,29 +7152,40 @@ ${overview}
6520
7152
 
6521
7153
  ## Goals
6522
7154
 
6523
- - [ ] TODO: Fill concrete goals based on spec.md.
6524
- - [ ] TODO: Clarify in-scope and out-of-scope boundaries.
7155
+ ${goals}
6525
7156
 
6526
7157
  ## Completion Criteria
6527
7158
 
6528
- - [ ] TODO: Define verifiable completion criteria.
6529
- - [ ] TODO: Add validation/test conditions for completion.
7159
+ ${criteria}
7160
+ ${scopeSection}
6530
7161
 
6531
7162
  ## Related Documents
6532
7163
 
6533
- - **Spec**: \`${paths.specPath}\`
6534
- - **Plan**: \`${paths.planPath}\`
6535
- - **Tasks**: \`${paths.tasksPath}\`
7164
+ - **Spec**: \`${bodyPaths.specPath}\`
7165
+ - **Plan**: \`${bodyPaths.planPath}\`
7166
+ - **Tasks**: \`${bodyPaths.tasksPath}\`
6536
7167
 
6537
7168
  ## Labels
6538
7169
 
6539
7170
  ${labels.map((label) => `- \`${label}\``).join("\n")}
6540
7171
  `;
6541
7172
  }
6542
- function buildPrBody(feature, overview, paths, lang) {
7173
+ function buildPrBody(feature, specContent, planContent, tasksContent, overview, paths, artifactPolicy, lang) {
7174
+ const bodyPaths = toBodyDocPaths(paths);
6543
7175
  const closes = feature.issueNumber ? `
6544
7176
  Closes #${feature.issueNumber}
6545
7177
  ` : "\n";
7178
+ const draft = getPrChangesAndTests(
7179
+ specContent,
7180
+ planContent,
7181
+ tasksContent,
7182
+ overview,
7183
+ lang
7184
+ );
7185
+ const changes = toChecklistLines(draft.changes);
7186
+ const tests = toChecklistLines(draft.tests);
7187
+ const screenshotsSection = artifactPolicy.includeScreenshots ? buildPrScreenshotsSection(lang) : "";
7188
+ const mermaidSection = artifactPolicy.includeMermaid ? buildPrMermaidSection(lang) : "";
6546
7189
  if (lang === "ko") {
6547
7190
  return `## \uAC1C\uC694
6548
7191
 
@@ -6550,20 +7193,20 @@ ${overview}
6550
7193
 
6551
7194
  ## \uBCC0\uACBD \uC0AC\uD56D
6552
7195
 
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.
7196
+ ${changes}
6555
7197
 
6556
7198
  ## \uD14C\uC2A4\uD2B8
6557
7199
 
6558
7200
  ### \uC2E4\uD589\uD55C \uD14C\uC2A4\uD2B8
6559
7201
 
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.
7202
+ ${tests}
7203
+ ${screenshotsSection}
7204
+ ${mermaidSection}
6562
7205
 
6563
7206
  ## \uAD00\uB828 \uBB38\uC11C
6564
7207
 
6565
- - **Spec**: \`${paths.specPath}\`
6566
- - **Tasks**: \`${paths.tasksPath}\`${closes}`;
7208
+ - **Spec**: \`${bodyPaths.specPath}\`
7209
+ - **Tasks**: \`${bodyPaths.tasksPath}\`${closes}`;
6567
7210
  }
6568
7211
  return `## Overview
6569
7212
 
@@ -6571,26 +7214,26 @@ ${overview}
6571
7214
 
6572
7215
  ## Changes
6573
7216
 
6574
- - [ ] TODO: Summarize key code changes in this PR.
6575
- - [ ] TODO: Describe impact/scope (including migration if any).
7217
+ ${changes}
6576
7218
 
6577
7219
  ## Tests
6578
7220
 
6579
7221
  ### Tests Run
6580
7222
 
6581
- - [ ] TODO: \`<test command>\` \u2014 PASS/FAIL
6582
- - [ ] TODO: If tests were not run, explain why.
7223
+ ${tests}
7224
+ ${screenshotsSection}
7225
+ ${mermaidSection}
6583
7226
 
6584
7227
  ## Related Documents
6585
7228
 
6586
- - **Spec**: \`${paths.specPath}\`
6587
- - **Tasks**: \`${paths.tasksPath}\`${closes}`;
7229
+ - **Spec**: \`${bodyPaths.specPath}\`
7230
+ - **Tasks**: \`${bodyPaths.tasksPath}\`${closes}`;
6588
7231
  }
6589
7232
  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"];
7233
+ return getGithubDraftRequiredSections("issue", lang);
6591
7234
  }
6592
7235
  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"];
7236
+ return getGithubDraftRequiredSections("pr", lang);
6594
7237
  }
6595
7238
  function replaceListField(content, keys, value) {
6596
7239
  for (const key of keys) {
@@ -6809,14 +7452,24 @@ function githubCommand(program2) {
6809
7452
  config.lang
6810
7453
  );
6811
7454
  const specContent = await fs15.readFile(path17.join(config.docsDir, paths.specPath), "utf-8");
7455
+ const planContent = await fs15.readFile(path17.join(config.docsDir, paths.planPath), "utf-8");
7456
+ const tasksContent = await fs15.readFile(path17.join(config.docsDir, paths.tasksPath), "utf-8");
6812
7457
  const overview = resolveOverviewFromSpec(specContent, feature, config.lang);
6813
7458
  const title = options.title?.trim() || tg(config.lang, "issueDefaultTitle", {
6814
7459
  slug: feature.slug,
6815
7460
  folder: feature.folderName
6816
7461
  });
6817
- const body = buildIssueBody(overview, labels, paths, config.lang);
7462
+ const generatedBody = buildIssueBody(
7463
+ specContent,
7464
+ planContent,
7465
+ tasksContent,
7466
+ overview,
7467
+ labels,
7468
+ paths,
7469
+ config.lang
7470
+ );
6818
7471
  ensureSections(
6819
- body,
7472
+ generatedBody,
6820
7473
  getRequiredIssueSections(config.lang),
6821
7474
  tg(config.lang, "kindIssue"),
6822
7475
  config.lang
@@ -6827,10 +7480,23 @@ function githubCommand(program2) {
6827
7480
  config.docsDir,
6828
7481
  feature.type
6829
7482
  );
6830
- await fs15.ensureDir(path17.dirname(bodyFile));
6831
- await fs15.writeFile(bodyFile, body, "utf-8");
7483
+ const explicitBodyFile = (options.bodyFile || "").trim();
7484
+ let body = generatedBody;
7485
+ if (options.create && explicitBodyFile && await fs15.pathExists(bodyFile)) {
7486
+ body = await fs15.readFile(bodyFile, "utf-8");
7487
+ ensureSections(
7488
+ body,
7489
+ getRequiredIssueSections(config.lang),
7490
+ tg(config.lang, "kindIssue"),
7491
+ config.lang
7492
+ );
7493
+ } else {
7494
+ await fs15.ensureDir(path17.dirname(bodyFile));
7495
+ await fs15.writeFile(bodyFile, generatedBody, "utf-8");
7496
+ }
6832
7497
  let issueUrl;
6833
7498
  if (options.create) {
7499
+ ensureNoTodoPlaceholders(body, tg(config.lang, "kindIssue"), config.lang);
6834
7500
  assertRemoteApproval(
6835
7501
  options.confirm,
6836
7502
  tg(config.lang, "operationIssueCreate"),
@@ -6914,7 +7580,7 @@ function githubCommand(program2) {
6914
7580
  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
7581
  "--confirm <reply>",
6916
7582
  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) => {
7583
+ ).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
7584
  try {
6919
7585
  const selectedComponent = resolveComponentOption4(options, commandLang);
6920
7586
  const { config, feature } = await resolveFeatureOrThrow(featureName, {
@@ -6924,6 +7590,9 @@ function githubCommand(program2) {
6924
7590
  const paths = getFeatureDocPaths(feature);
6925
7591
  ensureDocsExist(config.docsDir, [paths.specPath, paths.tasksPath], config.lang);
6926
7592
  const specContent = await fs15.readFile(path17.join(config.docsDir, paths.specPath), "utf-8");
7593
+ const planPath = path17.join(config.docsDir, paths.planPath);
7594
+ const planContent = await fs15.pathExists(planPath) ? await fs15.readFile(planPath, "utf-8") : "";
7595
+ const tasksContent = await fs15.readFile(path17.join(config.docsDir, paths.tasksPath), "utf-8");
6927
7596
  const overview = resolveOverviewFromSpec(specContent, feature, config.lang);
6928
7597
  const defaultTitle = feature.issueNumber ? tg(config.lang, "prDefaultTitleWithIssue", {
6929
7598
  issue: feature.issueNumber,
@@ -6932,9 +7601,19 @@ function githubCommand(program2) {
6932
7601
  slug: feature.slug
6933
7602
  });
6934
7603
  const title = options.title?.trim() || defaultTitle;
6935
- const body = buildPrBody(feature, overview, paths, config.lang);
7604
+ const artifactPolicy = resolvePrArtifactPolicy(config, feature, options);
7605
+ const generatedBody = buildPrBody(
7606
+ feature,
7607
+ specContent,
7608
+ planContent,
7609
+ tasksContent,
7610
+ overview,
7611
+ paths,
7612
+ artifactPolicy,
7613
+ config.lang
7614
+ );
6936
7615
  ensureSections(
6937
- body,
7616
+ generatedBody,
6938
7617
  getRequiredPrSections(config.lang),
6939
7618
  tg(config.lang, "kindPr"),
6940
7619
  config.lang
@@ -6945,13 +7624,27 @@ function githubCommand(program2) {
6945
7624
  config.docsDir,
6946
7625
  feature.type
6947
7626
  );
6948
- await fs15.ensureDir(path17.dirname(bodyFile));
6949
- await fs15.writeFile(bodyFile, body, "utf-8");
7627
+ const explicitBodyFile = (options.bodyFile || "").trim();
7628
+ let body = generatedBody;
7629
+ if (options.create && explicitBodyFile && await fs15.pathExists(bodyFile)) {
7630
+ body = await fs15.readFile(bodyFile, "utf-8");
7631
+ ensureSections(
7632
+ body,
7633
+ getRequiredPrSections(config.lang),
7634
+ tg(config.lang, "kindPr"),
7635
+ config.lang
7636
+ );
7637
+ } else {
7638
+ await fs15.ensureDir(path17.dirname(bodyFile));
7639
+ await fs15.writeFile(bodyFile, generatedBody, "utf-8");
7640
+ }
6950
7641
  const retryCount = toRetryCount(options.retry, config.lang);
6951
7642
  let prUrl = options.pr?.trim() || "";
6952
7643
  let mergedAttempts;
6953
7644
  let syncChanged = false;
6954
7645
  if (options.create) {
7646
+ ensureNoTodoPlaceholders(body, tg(config.lang, "kindPr"), config.lang);
7647
+ ensurePrArtifacts(body, artifactPolicy, config.lang);
6955
7648
  assertRemoteApproval(
6956
7649
  options.confirm,
6957
7650
  tg(config.lang, "operationPrCreate"),
@@ -7046,6 +7739,10 @@ function githubCommand(program2) {
7046
7739
  labels,
7047
7740
  body,
7048
7741
  bodyFile,
7742
+ artifactPolicy: {
7743
+ screenshots: artifactPolicy.includeScreenshots,
7744
+ mermaid: artifactPolicy.includeMermaid
7745
+ },
7049
7746
  prUrl: prUrl || void 0,
7050
7747
  syncChanged,
7051
7748
  merged: !!options.merge,
@@ -7187,6 +7884,7 @@ function docsCommand(program2) {
7187
7884
  id,
7188
7885
  command: toBuiltinDocCommand(id)
7189
7886
  }));
7887
+ const contract = getGithubDraftContractForBuiltinDoc(docId, config.lang);
7190
7888
  if (options.json) {
7191
7889
  console.log(
7192
7890
  JSON.stringify(
@@ -7202,7 +7900,8 @@ function docsCommand(program2) {
7202
7900
  hash: loaded.hash,
7203
7901
  content: loaded.content
7204
7902
  },
7205
- requiredDocs: followups
7903
+ requiredDocs: followups,
7904
+ contract: contract || void 0
7206
7905
  },
7207
7906
  null,
7208
7907
  2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lee-spec-kit",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Project documentation structure generator for AI-assisted development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -47,7 +47,7 @@ Before creating issue, share and wait for explicit approval (OK):
47
47
  - Full body draft (from `body`)
48
48
  - Labels
49
49
 
50
- Also fill all `TODO` items in Goals/Completion Criteria before creating.
50
+ Also refine Goals/Completion Criteria based on spec before creating.
51
51
 
52
52
  ### 3. Create Issue
53
53
 
@@ -26,6 +26,9 @@ npx lee-spec-kit docs get pr-template --json
26
26
 
27
27
  # 2) Generate draft body (no remote action)
28
28
  npx lee-spec-kit github pr F001 --json
29
+ # - Force screenshots section: --screenshots on
30
+ # - Force Mermaid section: --mermaid on
31
+ # - Auto policy (default): --screenshots auto --mermaid auto
29
32
  ```
30
33
 
31
34
  Use `docs get pr-template --json` output as the section policy,
@@ -131,7 +134,7 @@ Before creating PR, share the following **in a code block** and wait for **expli
131
134
  - Full body draft (from `body`)
132
135
  - Labels (at least 1; cannot be empty)
133
136
 
134
- Before approval/create, fill all `TODO` items in the generated draft.
137
+ Before approval/create, refine the generated draft's Changes/Tests sections based on actual work.
135
138
 
136
139
  ### 5. Create PR
137
140
 
@@ -47,7 +47,7 @@ npx lee-spec-kit github issue F001 --json
47
47
  - 본문 전체 초안 (`body` 기준)
48
48
  - 라벨
49
49
 
50
- 생성 전 목표/완료 기준 섹션의 `TODO` 항목을 모두 채우세요.
50
+ 생성 전 목표/완료 기준을 spec 기준으로 구체화하고 검토하세요.
51
51
 
52
52
  ### 3. 이슈 생성
53
53
 
@@ -26,6 +26,9 @@ npx lee-spec-kit docs get pr-template --json
26
26
 
27
27
  # 2) 초안 본문 생성 (원격 작업 아님)
28
28
  npx lee-spec-kit github pr F001 --json
29
+ # - 스크린샷 강제 포함: --screenshots on
30
+ # - Mermaid 강제 포함: --mermaid on
31
+ # - 자동 정책(기본): --screenshots auto --mermaid auto
29
32
  ```
30
33
 
31
34
  `docs get pr-template --json` 출력은 섹션 정책으로 보고,
@@ -131,7 +134,7 @@ PR 생성 전 다음 내용을 **코드블록으로** 사용자에게 공유하
131
134
  - 본문 전체 초안 (`body` 기준)
132
135
  - 라벨(최소 1개, 비워둘 수 없음)
133
136
 
134
- 승인/생성 전에 생성된 초안의 `TODO` 항목을 모두 채우세요.
137
+ 승인/생성 전에 생성된 초안의 변경 사항/테스트 섹션을 실제 작업 기준으로 보완하세요.
135
138
 
136
139
  ### 5. PR 생성
137
140