lee-spec-kit 0.6.33 → 0.6.34

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
@@ -512,6 +512,10 @@ var koContext = {
512
512
  "context.actionDetail.codeReviewRemoteBlocked": "\uC6D0\uACA9 PR \uCC28\uB2E8 \uC0AC\uC720\uB97C \uD574\uC18C\uD55C \uB4A4 \uBA38\uC9C0\uB97C \uC9C4\uD589\uD558\uC138\uC694",
513
513
  "context.actionDetail.codeReviewMergeAfterOk": "\uC0AC\uC6A9\uC790 \uC2B9\uC778(OK) \uD6C4 PR\uC744 \uBA38\uC9C0\uD558\uC138\uC694",
514
514
  "context.actionDetail.codeReviewRequestReview": "\uB9AC\uBDF0 \uC694\uCCAD\uC744 \uC9C4\uD589\uD558\uACE0 PR \uC0C1\uD0DC\uB97C Review\uB85C \uC720\uC9C0\uD558\uC138\uC694",
515
+ "context.actionDetail.featureScopeSplit": "\uC774 Feature\uB97C \uB354 \uC791\uC740 \uC774\uC288 \uB2E8\uC704\uB85C \uBD84\uB9AC\uD560\uC9C0 \uAC80\uD1A0\uD558\uC138\uC694",
516
+ "context.actionDetail.featureScopeSplitKeep": "\uBD84\uD560 \uAC00\uC774\uB4DC\uB97C \uD655\uC778\uD55C \uB4A4 \uD604\uC7AC \uC774\uC288 \uBC94\uC704\uB97C \uC720\uC9C0\uD558\uACE0 \uC9C4\uD589\uD558\uC138\uC694",
517
+ "context.actionDetail.featureScopeSplitTwo": "\uACB0\uD569\uB3C4/\uD30C\uC77C\uACB9\uCE68/\uD14C\uC2A4\uD2B8/\uBC30\uD3EC \uAE30\uC900\uC73C\uB85C 2\uAC1C \uC774\uC288\uB85C \uBD84\uB9AC\uD558\uC138\uC694",
518
+ "context.actionDetail.featureScopeSplitFour": "\uAE30\uC900 \uAE30\uBC18\uC73C\uB85C 4\uAC1C \uC774\uC288\uB85C \uBD84\uB9AC\uD558\uACE0 \uC758\uC874 \uC21C\uC11C\uB300\uB85C PR\uC744 \uBA38\uC9C0\uD558\uC138\uC694",
515
519
  "context.actionDetail.worktreeCleanup": "\uC644\uB8CC\uB41C feature worktree\uB97C \uC815\uB9AC\uD558\uC138\uC694",
516
520
  "context.actionDetail.prMetadataMigrate": "tasks.md\uC758 PR \uD56D\uBAA9 \uD615\uC2DD\uC744 \uCD5C\uC2E0 \uD15C\uD50C\uB9BF\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694",
517
521
  "context.actionDetail.prMetadataMigratePrFields": "tasks.md\uC5D0 PR/PR \uC0C1\uD0DC \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694",
@@ -636,6 +640,9 @@ var koMessages = {
636
640
  prReviewMerge: "\uBA38\uC9C0 \uC900\uBE44\uAC00 \uB418\uBA74 \uBA85\uC2DC\uC801\uC778 \uC2B9\uC778(\uB77C\uBCA8)\uC744 \uBC1B\uC740 \uB4A4 \uBA38\uC9C0 \uC635\uC158\uC744 \uC2E4\uD589\uD558\uC138\uC694. (\uC131\uACF5 \uC2DC PR \uC0C1\uD0DC\uAC00 Approved\uB85C \uB3D9\uAE30\uD654\uB429\uB2C8\uB2E4.)",
637
641
  prReviewMergeCommand: "npx lee-spec-kit github pr {featureRef} --merge --confirm OK",
638
642
  prRequestReview: "\uB9AC\uBDF0\uC5B4\uC5D0\uAC8C \uB9AC\uBDF0\uB97C \uC694\uCCAD\uD558\uACE0 PR \uC0C1\uD0DC\uB97C Review\uB85C \uC124\uC815/\uC720\uC9C0\uD558\uC138\uC694.",
643
+ featureScopeSplitKeep: "Feature \uBC94\uC704\uAC00 \uD07D\uB2C8\uB2E4. (tasks: {taskCount}, decisions \uC904\uC218: {decisionsLineCount}; \uBD84\uD560 \uC81C\uC548 \uAE30\uC900: {taskThreshold}/{decisionsThreshold}) \uD604\uC7AC \uAD8C\uC7A5 \uBD84\uD560\uC740 {recommendedIssues}\uAC1C \uC774\uC288\uC785\uB2C8\uB2E4. \uBA3C\uC800 `{guideCommand}`\uB97C \uD655\uC778\uD558\uACE0 \uACB0\uD569\uB3C4, \uBCC0\uACBD \uD30C\uC77C \uACB9\uCE68, \uD14C\uC2A4\uD2B8 \uACBD\uACC4, \uBC30\uD3EC \uB3C5\uB9BD\uC131 \uAE30\uC900\uC73C\uB85C \uD310\uB2E8\uD558\uC138\uC694. \uC774\uC288\uB97C \uC720\uC9C0\uD560 \uACBD\uC6B0 tasks.md \uBC94\uC704\uB97C \uCD95\uC18C\uD558\uACE0 \uC800\uC6B0\uC120 TODO\uB294 \uD65C\uC131 \uBC30\uCE58\uC5D0\uC11C \uC81C\uC678\uD558\uC138\uC694.",
644
+ featureScopeSplitTwo: "Feature \uBC94\uC704\uAC00 \uD07D\uB2C8\uB2E4. (tasks: {taskCount}, decisions \uC904\uC218: {decisionsLineCount}) \uAD8C\uC7A5 \uADDC\uCE59\uC0C1 40~79 \uD0DC\uC2A4\uD06C\uC774\uBA74\uC11C \uD558\uB4DC \uAE30\uC900 \uBBF8\uB9CC\uC774\uBA74 2\uAC1C \uC774\uC288 \uBD84\uD560\uC774 \uAE30\uBCF8\uC785\uB2C8\uB2E4. `{guideCommand}`\uB97C \uB530\uB77C \uACB0\uD569\uB3C4/\uD30C\uC77C\uACB9\uCE68/\uD14C\uC2A4\uD2B8/\uBC30\uD3EC \uAE30\uC900\uC73C\uB85C \uBD84\uD560\uD558\uC138\uC694. \uAC01 \uC774\uC288\uC5D0\uB294 \uB2E4\uC74C \uD15C\uD50C\uB9BF\uC744 \uAE30\uB85D\uD558\uC138\uC694: \uBAA9\uD45C, \uD3EC\uD568 \uBC94\uC704, \uC81C\uC678 \uBC94\uC704, \uC120\uD589 \uC758\uC874\uC131, PR \uC644\uB8CC \uAE30\uC900.",
645
+ featureScopeSplitFour: "Feature \uBC94\uC704\uAC00 \uD07D\uB2C8\uB2E4. (tasks: {taskCount}, decisions \uC904\uC218: {decisionsLineCount}) tasks >= {recommendFourTaskThreshold} \uB610\uB294 decisions \uC904\uC218 >= {recommendFourDecisionsThreshold}\uC774\uBA74 4\uAC1C \uC774\uC288 \uBD84\uD560\uC744 \uAC15\uD558\uAC8C \uAD8C\uC7A5\uD569\uB2C8\uB2E4. `{guideCommand}`\uB97C \uB530\uB77C 4\uAC1C\uC758 \uC5F0\uAD00 \uC774\uC288\uB85C \uBD84\uB9AC\uD558\uACE0 \uC758\uC874 \uC21C\uC11C\uB97C \uBA85\uC2DC\uD55C \uB4A4 PR\uC744 \uC21C\uCC28 \uBA38\uC9C0\uD558\uC138\uC694. \uAC01 \uC774\uC288 \uD15C\uD50C\uB9BF: \uBAA9\uD45C, \uD3EC\uD568 \uBC94\uC704, \uC81C\uC678 \uBC94\uC704, \uC120\uD589 \uC758\uC874\uC131, PR \uC644\uB8CC \uAE30\uC900.",
639
646
  userRequestReplan: "\uD604\uC7AC \uB2E8\uACC4\uC640 \uBCC4\uAC1C\uB85C \uC0AC\uC6A9\uC790\uAC00 \uC81C\uC548\uD55C \uC0C8 \uC694\uAD6C\uB97C \uBA3C\uC800 \uBC18\uC601\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uC694\uAD6C\uC0AC\uD56D\uC744 \uC694\uC57D\uD574 tasks.md\uC5D0 \uCD94\uAC00\uD558\uAC70\uB098 \uBCC4\uB3C4 Feature\uB85C \uBD84\uB9AC\uD55C \uB4A4, \uBB38\uC11C \uC0C1\uD0DC\uB97C \uB9DE\uCD94\uACE0 context\uB97C \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
640
647
  featureDone: "\uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC694\uAD6C\uC0AC\uD56D\uACFC \uBAA8\uB4E0 \uD0DC\uC2A4\uD06C/\uC644\uB8CC \uC870\uAC74\uC774 \uCDA9\uC871\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC774 Feature\uB294 \uC644\uB8CC \uC0C1\uD0DC\uC785\uB2C8\uB2E4.",
641
648
  fallbackRerunContext: "\uC0C1\uD0DC\uB97C \uD310\uBCC4\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBB38\uC11C\uB97C \uD655\uC778\uD55C \uB4A4 \uB2E4\uC2DC context\uB97C \uC2E4\uD589\uD558\uC138\uC694."
@@ -648,6 +655,7 @@ var koWarnings = {
648
655
  docsPathIgnored: "\uD604\uC7AC Feature \uBB38\uC11C \uACBD\uB85C\uAC00 git ignore \uB300\uC0C1\uC785\uB2C8\uB2E4: {path} (docs \uCEE4\uBC0B \uAC10\uC9C0\uAC00 \uC81C\uD55C\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.)",
649
656
  docsUncommittedChanges: "\uBB38\uC11C \uBCC0\uACBD\uC0AC\uD56D\uC774 \uCEE4\uBC0B\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. (\uCD94\uAC00 \uBB38\uC11C \uCEE4\uBC0B \uD544\uC694) \uCEE4\uBC0B \uBA54\uC2DC\uC9C0 \uADDC\uCE59\uC740 git-workflow \uAC00\uC774\uB4DC\uB97C \uAE30\uC900\uC73C\uB85C \uD655\uC778\uD558\uC138\uC694.",
650
657
  projectUncommittedChanges: "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uBCC0\uACBD\uC0AC\uD56D\uC774 \uCEE4\uBC0B\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. (\uCD94\uAC00 \uCF54\uB4DC \uCEE4\uBC0B \uD544\uC694)",
658
+ featureScopeSplitSuggested: "Feature \uBC94\uC704\uAC00 \uB2E8\uC77C \uC774\uC288\uB85C \uCC98\uB9AC\uD558\uAE30\uC5D0 \uD07D\uB2C8\uB2E4. (tasks: {taskCount}, decisions \uC904\uC218: {decisionsLineCount}; \uBD84\uD560 \uC81C\uC548 \uAE30\uC900: tasks {taskThreshold}\uAC1C \uB610\uB294 decisions {decisionsThreshold}\uC904) \uD604\uC7AC \uAD8C\uC7A5 \uBD84\uD560: {recommendedIssues}\uAC1C \uC774\uC288 (4\uBD84\uD560 \uD558\uB4DC \uAE30\uC900: tasks >= {recommendFourTaskThreshold} \uB610\uB294 decisions \uC904\uC218 >= {recommendFourDecisionsThreshold}).",
651
659
  legacyTasksDocStatusField: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. `\uBB38\uC11C \uC0C1\uD0DC` \uD544\uB4DC(Draft/Review/Approved)\uB97C \uCD94\uAC00\uD574 \uD0DC\uC2A4\uD06C \uC2B9\uC778 \uB2E8\uACC4\uB97C \uD65C\uC131\uD654\uD558\uC138\uC694.",
652
660
  legacyTasksPrFields: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. PR \uB2E8\uACC4 \uC804\uC5D0 `PR` \uBC0F `PR \uC0C1\uD0DC` \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694.",
653
661
  legacyTasksPrePrReviewField: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. PR \uB2E8\uACC4 \uC804\uC5D0 `PR \uC804 \uB9AC\uBDF0` \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694. (`- **PR \uC804 \uB9AC\uBDF0**: Pending | Done`)",
@@ -1041,6 +1049,10 @@ var enContext = {
1041
1049
  "context.actionDetail.codeReviewRemoteBlocked": "Resolve remote PR blockers before merge",
1042
1050
  "context.actionDetail.codeReviewMergeAfterOk": "Merge PR after explicit OK",
1043
1051
  "context.actionDetail.codeReviewRequestReview": "Request review and keep PR Status as Review",
1052
+ "context.actionDetail.featureScopeSplit": "Review whether this feature should be split into smaller issue units",
1053
+ "context.actionDetail.featureScopeSplitKeep": "Keep current issue scope and continue (after split-guide check)",
1054
+ "context.actionDetail.featureScopeSplitTwo": "Split into 2 linked issues using coupling/file-overlap/test/deploy criteria",
1055
+ "context.actionDetail.featureScopeSplitFour": "Split into 4 linked issues (criteria-based) and merge PRs in dependency order",
1044
1056
  "context.actionDetail.worktreeCleanup": "Clean up the completed feature worktree",
1045
1057
  "context.actionDetail.prMetadataMigrate": "Update tasks.md PR fields to the latest template format",
1046
1058
  "context.actionDetail.prMetadataMigratePrFields": "Update tasks.md with PR/PR Status fields",
@@ -1165,6 +1177,9 @@ var enMessages = {
1165
1177
  prReviewMerge: "When ready to merge, get explicit approval (label) and run the merge option. (On success, PR Status is synced to Approved.)",
1166
1178
  prReviewMergeCommand: "npx lee-spec-kit github pr {featureRef} --merge --confirm OK",
1167
1179
  prRequestReview: "Request review and set/keep PR Status as Review.",
1180
+ featureScopeSplitKeep: "Feature scope is large (tasks: {taskCount}, decisions lines: {decisionsLineCount}; split suggestion threshold: {taskThreshold}/{decisionsThreshold}). Current recommendation is {recommendedIssues} issues. Before deciding, read `{guideCommand}` and evaluate coupling, changed-file overlap, test boundary, and deployment independence. If you keep one issue, tighten scope in tasks.md and move low-priority TODO items out of the active batch.",
1181
+ featureScopeSplitTwo: "Feature scope is large (tasks: {taskCount}, decisions lines: {decisionsLineCount}). Recommendation rule: 40~79 tasks and below the hard threshold usually maps to 2 issues. Follow `{guideCommand}` and split by coupling/file-overlap/test/deploy criteria. For each new issue, document this template: Goal, Included Scope, Excluded Scope, Depends On, PR Done Criteria.",
1182
+ featureScopeSplitFour: "Feature scope is large (tasks: {taskCount}, decisions lines: {decisionsLineCount}). Hard recommendation is 4 issues when tasks >= {recommendFourTaskThreshold} or decisions lines >= {recommendFourDecisionsThreshold}. Follow `{guideCommand}` and split into 4 linked issues with explicit dependency order and sequential PR merges. Use the per-issue template: Goal, Included Scope, Excluded Scope, Depends On, PR Done Criteria.",
1168
1183
  userRequestReplan: "You can pause this step and handle a newly requested user requirement first. Summarize it, add it to tasks.md or split it into a separate Feature, then align document statuses and rerun context.",
1169
1184
  featureDone: "Workflow requirements and all tasks/completion criteria are satisfied. This feature is done.",
1170
1185
  fallbackRerunContext: "Cannot determine status. Check the docs and run context again."
@@ -1177,6 +1192,7 @@ var enWarnings = {
1177
1192
  docsPathIgnored: "Current feature docs path is ignored by git: {path} (docs commit detection may be limited).",
1178
1193
  docsUncommittedChanges: "Docs changes are not committed. (Additional docs commit needed.) Check commit message rules against the git-workflow guide.",
1179
1194
  projectUncommittedChanges: "Project code changes are not committed. (Additional code commit needed.)",
1195
+ featureScopeSplitSuggested: "Feature scope may be too large for one issue (tasks: {taskCount}, decisions lines: {decisionsLineCount}; suggest split at {taskThreshold} tasks or {decisionsThreshold} decision lines). Current recommendation: split into {recommendedIssues} issue units (hard 4-way rule: tasks >= {recommendFourTaskThreshold} or decisions lines >= {recommendFourDecisionsThreshold}).",
1180
1196
  legacyTasksDocStatusField: "Legacy tasks.md format detected. Add a `Doc Status` field (Draft/Review/Approved) to enable tasks approval.",
1181
1197
  legacyTasksPrFields: "Legacy tasks.md format detected. Add `PR` and `PR Status` fields before PR steps.",
1182
1198
  legacyTasksPrePrReviewField: "Legacy tasks.md format detected. Add `Pre-PR Review` before PR steps. (`- **Pre-PR Review**: Pending | Done`)",
@@ -3163,6 +3179,7 @@ var ACTION_CATEGORIES = [
3163
3179
  "pre_pr_review",
3164
3180
  "pr_status_update",
3165
3181
  "code_review",
3182
+ "feature_scope_split",
3166
3183
  "worktree_cleanup",
3167
3184
  "user_request_replan",
3168
3185
  "feature_done",
@@ -4575,6 +4592,52 @@ function applyTaskExecutePhaseCheck(action, requiresUserCheck, policy, explicitl
4575
4592
  if (explicitlyRequired) return requiresUserCheck;
4576
4593
  return false;
4577
4594
  }
4595
+ function withFeatureScopeSplitOptions(actions, feature, lang) {
4596
+ if (!feature.scopeSplit.suggested) return actions;
4597
+ if (feature.tasks.total === 0 || feature.tasks.done >= feature.tasks.total) {
4598
+ return actions;
4599
+ }
4600
+ if (actions.some((action) => action.category === "feature_scope_split")) {
4601
+ return actions;
4602
+ }
4603
+ const recommendedIssues = feature.scopeSplit.recommendation === "split_4" ? 4 : 2;
4604
+ const recommendationLabel = feature.scopeSplit.recommendation === "split_4" ? "split_4" : feature.scopeSplit.recommendation === "split_2" ? "split_2" : "none";
4605
+ const vars = {
4606
+ taskCount: feature.scopeSplit.taskCount,
4607
+ decisionsLineCount: feature.scopeSplit.decisionsLineCount,
4608
+ taskThreshold: feature.scopeSplit.suggestTaskCountThreshold,
4609
+ decisionsThreshold: feature.scopeSplit.suggestDecisionsLineCountThreshold,
4610
+ recommendFourTaskThreshold: feature.scopeSplit.recommendSplitFourTaskCountThreshold,
4611
+ recommendFourDecisionsThreshold: feature.scopeSplit.recommendSplitFourDecisionsLineCountThreshold,
4612
+ recommendedIssues,
4613
+ recommendationLabel,
4614
+ guideCommand: "npx lee-spec-kit docs get split-feature --json"
4615
+ };
4616
+ return [
4617
+ ...actions,
4618
+ {
4619
+ type: "instruction",
4620
+ category: "feature_scope_split",
4621
+ requiresUserCheck: true,
4622
+ uiDetailKey: "context.actionDetail.featureScopeSplitKeep",
4623
+ message: tr(lang, "messages", "featureScopeSplitKeep", vars)
4624
+ },
4625
+ {
4626
+ type: "instruction",
4627
+ category: "feature_scope_split",
4628
+ requiresUserCheck: true,
4629
+ uiDetailKey: "context.actionDetail.featureScopeSplitTwo",
4630
+ message: tr(lang, "messages", "featureScopeSplitTwo", vars)
4631
+ },
4632
+ {
4633
+ type: "instruction",
4634
+ category: "feature_scope_split",
4635
+ requiresUserCheck: true,
4636
+ uiDetailKey: "context.actionDetail.featureScopeSplitFour",
4637
+ message: tr(lang, "messages", "featureScopeSplitFour", vars)
4638
+ }
4639
+ ];
4640
+ }
4578
4641
  function withUserRequestReplanOption(actions, lang) {
4579
4642
  if (actions.some((action) => action.category === "user_request_replan")) {
4580
4643
  return actions;
@@ -4594,8 +4657,13 @@ function resolveFeatureProgress(feature, stepDefinitions, lang, approval) {
4594
4657
  for (const definition of ordered) {
4595
4658
  if (!definition.current) continue;
4596
4659
  if (definition.current.when(feature)) {
4597
- const currentActions = withUserRequestReplanOption(
4660
+ const actionsWithScopeSplit = withFeatureScopeSplitOptions(
4598
4661
  definition.current.actions(feature),
4662
+ feature,
4663
+ lang
4664
+ );
4665
+ const currentActions = withUserRequestReplanOption(
4666
+ actionsWithScopeSplit,
4599
4667
  lang
4600
4668
  );
4601
4669
  const actions = applyApprovalPolicy(
@@ -4843,6 +4911,17 @@ function isExpectedFeatureBranch(branchName, issueNumber, slug, folderName) {
4843
4911
  const rest = match[1];
4844
4912
  return rest === slug || rest === folderName;
4845
4913
  }
4914
+ var FEATURE_SCOPE_SPLIT_TASK_THRESHOLD = 40;
4915
+ var FEATURE_SCOPE_SPLIT_DECISIONS_LINE_THRESHOLD = 1200;
4916
+ var FEATURE_SCOPE_SPLIT_RECOMMEND_FOUR_TASK_THRESHOLD = 80;
4917
+ var FEATURE_SCOPE_SPLIT_RECOMMEND_FOUR_DECISIONS_LINE_THRESHOLD = 2500;
4918
+ function countDocumentLines(content) {
4919
+ if (!content) return 0;
4920
+ const lines = content.split(/\r?\n/);
4921
+ if (lines.length === 0) return 0;
4922
+ if (lines[lines.length - 1] === "") return lines.length - 1;
4923
+ return lines.length;
4924
+ }
4846
4925
  function escapeRegExp(value) {
4847
4926
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4848
4927
  }
@@ -5466,6 +5545,7 @@ async function parseFeature(ctx, featurePath, type, context, options) {
5466
5545
  const specPath = path12.join(featurePath, "spec.md");
5467
5546
  const planPath = path12.join(featurePath, "plan.md");
5468
5547
  const tasksPath = path12.join(featurePath, "tasks.md");
5548
+ const decisionsPath = path12.join(featurePath, "decisions.md");
5469
5549
  const issueDocPath = path12.join(featurePath, "issue.md");
5470
5550
  const prDocPath = path12.join(featurePath, "pr.md");
5471
5551
  let specStatus;
@@ -5514,6 +5594,12 @@ async function parseFeature(ctx, featurePath, type, context, options) {
5514
5594
  const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
5515
5595
  planStatus = parseDocStatus(statusValue);
5516
5596
  }
5597
+ const decisionsExists = await ctx.fs.pathExists(decisionsPath);
5598
+ let decisionsLineCount = 0;
5599
+ if (decisionsExists) {
5600
+ const content = await ctx.fs.readFile(decisionsPath, "utf-8");
5601
+ decisionsLineCount = countDocumentLines(content);
5602
+ }
5517
5603
  const tasksExists = await ctx.fs.pathExists(tasksPath);
5518
5604
  const tasksSummary = { total: 0, todo: 0, doing: 0, done: 0 };
5519
5605
  let activeTask;
@@ -5701,6 +5787,16 @@ async function parseFeature(ctx, featurePath, type, context, options) {
5701
5787
  if (workflowPolicy.requireMerge && prStatus === "Review" && prLink && effectiveProjectGitCwd) {
5702
5788
  prRemote = resolvePrRemoteStatus(ctx, prLink, effectiveProjectGitCwd) || void 0;
5703
5789
  }
5790
+ const scopeSplitReasons = [];
5791
+ if (tasksSummary.total >= FEATURE_SCOPE_SPLIT_TASK_THRESHOLD) {
5792
+ scopeSplitReasons.push("task_count");
5793
+ }
5794
+ if (decisionsLineCount >= FEATURE_SCOPE_SPLIT_DECISIONS_LINE_THRESHOLD) {
5795
+ scopeSplitReasons.push("decisions_lines");
5796
+ }
5797
+ const scopeSplitSuggested = scopeSplitReasons.length > 0;
5798
+ const scopeSplitRecommendFour = tasksSummary.total >= FEATURE_SCOPE_SPLIT_RECOMMEND_FOUR_TASK_THRESHOLD || decisionsLineCount >= FEATURE_SCOPE_SPLIT_RECOMMEND_FOUR_DECISIONS_LINE_THRESHOLD;
5799
+ const scopeSplitRecommendation = !scopeSplitSuggested ? "none" : scopeSplitRecommendFour ? "split_4" : "split_2";
5704
5800
  const warnings = [];
5705
5801
  if (effectiveProjectBranchAvailable === false) {
5706
5802
  warnings.push(tr(lang, "warnings", "projectBranchUnavailable"));
@@ -5835,6 +5931,19 @@ async function parseFeature(ctx, featurePath, type, context, options) {
5835
5931
  if (tasksExists && !tasksDocStatusFieldExists) {
5836
5932
  warnings.push(tr(lang, "warnings", "legacyTasksDocStatusField"));
5837
5933
  }
5934
+ if (scopeSplitSuggested && tasksSummary.total > tasksSummary.done) {
5935
+ warnings.push(
5936
+ tr(lang, "warnings", "featureScopeSplitSuggested", {
5937
+ taskCount: tasksSummary.total,
5938
+ decisionsLineCount,
5939
+ taskThreshold: FEATURE_SCOPE_SPLIT_TASK_THRESHOLD,
5940
+ decisionsThreshold: FEATURE_SCOPE_SPLIT_DECISIONS_LINE_THRESHOLD,
5941
+ recommendFourTaskThreshold: FEATURE_SCOPE_SPLIT_RECOMMEND_FOUR_TASK_THRESHOLD,
5942
+ recommendFourDecisionsThreshold: FEATURE_SCOPE_SPLIT_RECOMMEND_FOUR_DECISIONS_LINE_THRESHOLD,
5943
+ recommendedIssues: scopeSplitRecommendation === "split_4" ? 4 : 2
5944
+ })
5945
+ );
5946
+ }
5838
5947
  if (docsEverCommitted && docsHasUncommittedChanges) {
5839
5948
  warnings.push(tr(lang, "warnings", "docsUncommittedChanges"));
5840
5949
  }
@@ -5946,6 +6055,17 @@ async function parseFeature(ctx, featurePath, type, context, options) {
5946
6055
  planStatus,
5947
6056
  tasksDocStatus,
5948
6057
  tasks: tasksSummary,
6058
+ scopeSplit: {
6059
+ suggested: scopeSplitSuggested,
6060
+ reasons: scopeSplitReasons,
6061
+ recommendation: scopeSplitRecommendation,
6062
+ taskCount: tasksSummary.total,
6063
+ decisionsLineCount,
6064
+ suggestTaskCountThreshold: FEATURE_SCOPE_SPLIT_TASK_THRESHOLD,
6065
+ suggestDecisionsLineCountThreshold: FEATURE_SCOPE_SPLIT_DECISIONS_LINE_THRESHOLD,
6066
+ recommendSplitFourTaskCountThreshold: FEATURE_SCOPE_SPLIT_RECOMMEND_FOUR_TASK_THRESHOLD,
6067
+ recommendSplitFourDecisionsLineCountThreshold: FEATURE_SCOPE_SPLIT_RECOMMEND_FOUR_DECISIONS_LINE_THRESHOLD
6068
+ },
5949
6069
  activeTask,
5950
6070
  lastDoneTask,
5951
6071
  nextTodoTask,
@@ -6966,12 +7086,18 @@ var BUILTIN_DOC_DEFINITIONS = [
6966
7086
  id: "create-pr",
6967
7087
  title: { ko: "create-pr \uC2A4\uD0AC", en: "create-pr skill" },
6968
7088
  relativePath: (_, lang) => path12.join(lang, "common", "agents", "skills", "create-pr.md")
7089
+ },
7090
+ {
7091
+ id: "split-feature",
7092
+ title: { ko: "feature \uBD84\uD560 \uAC00\uC774\uB4DC", en: "feature split guide" },
7093
+ relativePath: (_, lang) => path12.join(lang, "common", "agents", "skills", "split-feature.md")
6969
7094
  }
6970
7095
  ];
6971
7096
  var DOC_FOLLOWUPS = {
6972
7097
  agents: [
6973
7098
  "create-feature",
6974
7099
  "execute-task",
7100
+ "split-feature",
6975
7101
  "git-workflow",
6976
7102
  "create-issue",
6977
7103
  "issue-doc",
@@ -6982,9 +7108,10 @@ var DOC_FOLLOWUPS = {
6982
7108
  "issue-doc": [],
6983
7109
  "pr-doc": [],
6984
7110
  "create-feature": ["execute-task"],
6985
- "execute-task": ["git-workflow"],
7111
+ "execute-task": ["git-workflow", "split-feature"],
6986
7112
  "create-issue": ["issue-doc"],
6987
- "create-pr": ["pr-doc"]
7113
+ "create-pr": ["pr-doc"],
7114
+ "split-feature": []
6988
7115
  };
6989
7116
  var CATEGORY_DOC_MAP = {
6990
7117
  spec_write: ["agents"],
@@ -7002,6 +7129,7 @@ var CATEGORY_DOC_MAP = {
7002
7129
  pr_create: ["create-pr", "pr-doc", "git-workflow"],
7003
7130
  pr_status_update: ["create-pr"],
7004
7131
  code_review: ["create-pr"],
7132
+ feature_scope_split: ["split-feature", "execute-task"],
7005
7133
  worktree_cleanup: ["git-workflow"],
7006
7134
  user_request_replan: ["agents", "execute-task"]
7007
7135
  };
@@ -7019,6 +7147,9 @@ function normalizeBuiltinDocId(input) {
7019
7147
  if (normalized === "execute-task") return "execute-task";
7020
7148
  if (normalized === "create-issue") return "create-issue";
7021
7149
  if (normalized === "create-pr") return "create-pr";
7150
+ if (normalized === "split-feature" || normalized === "feature-split") {
7151
+ return "split-feature";
7152
+ }
7022
7153
  if (normalized === "agents") return "agents";
7023
7154
  return null;
7024
7155
  }
@@ -7107,6 +7238,7 @@ var ACTION_DETAIL_KEY_BY_CATEGORY = {
7107
7238
  pr_create: "context.actionDetail.prCreate",
7108
7239
  pr_status_update: "context.actionDetail.prStatusUpdate",
7109
7240
  code_review: "context.actionDetail.codeReview",
7241
+ feature_scope_split: "context.actionDetail.featureScopeSplit",
7110
7242
  worktree_cleanup: "context.actionDetail.worktreeCleanup",
7111
7243
  pr_metadata_migrate: "context.actionDetail.prMetadataMigrate",
7112
7244
  user_request_replan: "context.actionDetail.userRequestReplan",
@@ -13877,6 +14009,22 @@ function normalizeCommandsExecuted(value) {
13877
14009
  if (!Array.isArray(value)) return [];
13878
14010
  return value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
13879
14011
  }
14012
+ function normalizeGitPath3(value) {
14013
+ return value.trim().replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
14014
+ }
14015
+ function parseGitPathList(stdout) {
14016
+ return stdout.split("\n").map((entry) => normalizeGitPath3(entry)).filter(Boolean);
14017
+ }
14018
+ function uniquePaths(values) {
14019
+ const seen = /* @__PURE__ */ new Set();
14020
+ const out = [];
14021
+ for (const value of values) {
14022
+ if (!value || seen.has(value)) continue;
14023
+ seen.add(value);
14024
+ out.push(value);
14025
+ }
14026
+ return out;
14027
+ }
13880
14028
  function normalizeEvidenceFiles(value) {
13881
14029
  if (!Array.isArray(value)) {
13882
14030
  throw createCliError(
@@ -13920,6 +14068,10 @@ var PrePrReviewValidator = class {
13920
14068
  this.ctx = ctx;
13921
14069
  }
13922
14070
  async validateEvidence(evidencePath, projectRoot) {
14071
+ const result = await this.validateEvidenceWithScope(evidencePath, projectRoot);
14072
+ return result.evidence;
14073
+ }
14074
+ async validateEvidenceWithScope(evidencePath, projectRoot) {
13923
14075
  const fullPath = path12.resolve(evidencePath);
13924
14076
  if (!await fs.pathExists(fullPath)) {
13925
14077
  throw createCliError(
@@ -13958,14 +14110,18 @@ var PrePrReviewValidator = class {
13958
14110
  );
13959
14111
  }
13960
14112
  }
13961
- const changedFiles = await this.getChangedFiles(projectRoot);
14113
+ const scope = await this.collectReviewScope(projectRoot);
14114
+ const changedFiles = uniquePaths([
14115
+ ...scope.mainChangedFiles,
14116
+ ...scope.worktreeChangedFiles
14117
+ ]);
13962
14118
  const reviewedFiles = new Set(
13963
14119
  normalizedEvidence.files.map(
13964
14120
  (f) => path12.relative(
13965
14121
  projectRoot,
13966
14122
  path12.resolve(projectRoot, f.path)
13967
14123
  )
13968
- )
14124
+ ).map((entry) => normalizeGitPath3(entry)).filter(Boolean)
13969
14125
  );
13970
14126
  const missingFiles = changedFiles.filter((f) => !reviewedFiles.has(f));
13971
14127
  if (missingFiles.length > 0) {
@@ -13975,40 +14131,120 @@ var PrePrReviewValidator = class {
13975
14131
  ${missingFiles.map((f) => `- ${f}`).join("\n")}`
13976
14132
  );
13977
14133
  }
13978
- return normalizedEvidence;
14134
+ return {
14135
+ evidence: normalizedEvidence,
14136
+ scope
14137
+ };
13979
14138
  }
13980
- async getChangedFiles(cwd) {
13981
- const branchResult = await this.ctx.cmd.runAsync("git", ["rev-parse", "--abbrev-ref", "origin/HEAD"], { cwd }).catch(() => null);
13982
- let baseBranch = "origin/main";
13983
- if (branchResult && branchResult.code === 0 && branchResult.stdout.trim()) {
13984
- baseBranch = branchResult.stdout.trim().replace(/^origin\//, "");
14139
+ async collectReviewScope(cwd) {
14140
+ const baseRef = await this.resolveBaseRef(cwd);
14141
+ const mergeBase = await this.resolveMergeBase(cwd, baseRef);
14142
+ const mainDiff = await this.getMainChangedFiles(cwd, mergeBase);
14143
+ const worktreeDiff = await this.getWorktreeChangedFiles(cwd);
14144
+ if (!mainDiff.ok && !worktreeDiff.ok) {
14145
+ throw createCliError(
14146
+ "VALIDATION_FAILED",
14147
+ "Unable to determine changed files from git diff. Ensure this is a git repository with accessible history."
14148
+ );
13985
14149
  }
13986
- let diffTarget = "HEAD~1";
14150
+ return {
14151
+ baseRef,
14152
+ mergeBase,
14153
+ mainRange: mainDiff.rangeLabel,
14154
+ mainChangedFiles: mainDiff.files,
14155
+ worktreeChangedFiles: worktreeDiff.files
14156
+ };
14157
+ }
14158
+ async resolveBaseRef(cwd) {
13987
14159
  try {
13988
- const mergeBaseRes = await this.ctx.cmd.runAsync(
14160
+ const result = await this.ctx.cmd.runAsync(
13989
14161
  "git",
13990
- ["merge-base", "HEAD", baseBranch],
14162
+ ["rev-parse", "--abbrev-ref", "origin/HEAD"],
13991
14163
  { cwd }
13992
14164
  );
13993
- if (mergeBaseRes.code === 0 && mergeBaseRes.stdout.trim()) {
13994
- diffTarget = mergeBaseRes.stdout.trim();
14165
+ if (result.code === 0) {
14166
+ const value = result.stdout.trim();
14167
+ if (value && value !== "origin/HEAD") {
14168
+ return value;
14169
+ }
13995
14170
  }
13996
14171
  } catch {
13997
14172
  }
13998
- const targets = [diffTarget, "HEAD~1", ""];
13999
- const seen = /* @__PURE__ */ new Set();
14000
- for (const target of targets) {
14001
- if (seen.has(target)) continue;
14002
- seen.add(target);
14003
- const args = target ? ["diff", "--name-only", target] : ["diff", "--name-only"];
14004
- const diffResult = await this.ctx.cmd.runAsync("git", args, { cwd });
14005
- if (diffResult.code !== 0) continue;
14006
- return diffResult.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
14173
+ return "origin/main";
14174
+ }
14175
+ async resolveMergeBase(cwd, baseRef) {
14176
+ const candidates = uniquePaths([
14177
+ baseRef,
14178
+ baseRef.replace(/^origin\//, ""),
14179
+ "origin/main",
14180
+ "main"
14181
+ ]);
14182
+ for (const candidate of candidates) {
14183
+ if (!candidate) continue;
14184
+ try {
14185
+ const result = await this.ctx.cmd.runAsync(
14186
+ "git",
14187
+ ["merge-base", "HEAD", candidate],
14188
+ { cwd }
14189
+ );
14190
+ if (result.code !== 0) continue;
14191
+ const mergeBase = normalizeGitPath3(result.stdout);
14192
+ if (mergeBase) return mergeBase;
14193
+ } catch {
14194
+ }
14007
14195
  }
14008
- throw createCliError(
14009
- "VALIDATION_FAILED",
14010
- "Unable to determine changed files from git diff. Ensure this is a git repository with accessible history."
14011
- );
14196
+ return null;
14197
+ }
14198
+ async getMainChangedFiles(cwd, mergeBase) {
14199
+ const ranges = uniquePaths([
14200
+ mergeBase ? `${mergeBase}..HEAD` : "",
14201
+ "HEAD~1..HEAD",
14202
+ "HEAD^..HEAD"
14203
+ ]);
14204
+ for (const range of ranges) {
14205
+ if (!range) continue;
14206
+ try {
14207
+ const result = await this.ctx.cmd.runAsync(
14208
+ "git",
14209
+ ["diff", "--name-only", range],
14210
+ { cwd }
14211
+ );
14212
+ if (result.code !== 0) continue;
14213
+ return {
14214
+ ok: true,
14215
+ rangeLabel: range,
14216
+ files: uniquePaths(parseGitPathList(result.stdout))
14217
+ };
14218
+ } catch {
14219
+ }
14220
+ }
14221
+ return {
14222
+ ok: false,
14223
+ rangeLabel: mergeBase ? `${mergeBase}..HEAD` : "HEAD~1..HEAD",
14224
+ files: []
14225
+ };
14226
+ }
14227
+ async getWorktreeChangedFiles(cwd) {
14228
+ let hasSuccessfulCommand = false;
14229
+ const files = [];
14230
+ const commands = [
14231
+ ["diff", "--name-only"],
14232
+ ["diff", "--name-only", "--cached"],
14233
+ ["ls-files", "--others", "--exclude-standard"]
14234
+ ];
14235
+ for (const args of commands) {
14236
+ try {
14237
+ const result = await this.ctx.cmd.runAsync("git", args, { cwd });
14238
+ if (result.code !== 0) continue;
14239
+ hasSuccessfulCommand = true;
14240
+ files.push(...parseGitPathList(result.stdout));
14241
+ } catch {
14242
+ }
14243
+ }
14244
+ return {
14245
+ ok: hasSuccessfulCommand,
14246
+ files: uniquePaths(files)
14247
+ };
14012
14248
  }
14013
14249
  };
14014
14250
 
@@ -14106,6 +14342,8 @@ function buildReportContent(input) {
14106
14342
  - Maintainability: ${f.review.maintainability}`;
14107
14343
  }).join("\n");
14108
14344
  }
14345
+ const mainScopeFiles = input.scope.mainChangedFiles.length > 0 ? input.scope.mainChangedFiles.map((entry) => ` - ${entry}`).join("\n") : " - (none)";
14346
+ const worktreeScopeFiles = input.scope.worktreeChangedFiles.length > 0 ? input.scope.worktreeChangedFiles.map((entry) => ` - ${entry}`).join("\n") : " - (none)";
14109
14347
  return `## Pre-PR Review Log (${input.date})
14110
14348
 
14111
14349
  - **Feature**: ${input.folderName}
@@ -14119,12 +14357,30 @@ ${commandsRun}
14119
14357
  - **Residual Risks**:
14120
14358
  - ${input.evidence.residualRisks}
14121
14359
 
14360
+ - **Review Scope**:
14361
+ - **Main Base Ref**: ${input.scope.baseRef}
14362
+ - **Main Merge Base**: ${input.scope.mergeBase ?? "unresolved"}
14363
+ - **Main Range**: ${input.scope.mainRange}
14364
+ - **Main Changed Files**:
14365
+ ${mainScopeFiles}
14366
+ - **Worktree Changed Files**:
14367
+ ${worktreeScopeFiles}
14368
+
14122
14369
  - **Findings (Changed Files)**:
14123
14370
  ${filesSection}
14124
14371
 
14125
14372
  - **Trace**: pre-pr-review command executed and synced with tasks.md
14126
14373
  `;
14127
14374
  }
14375
+ function createFallbackReviewScope() {
14376
+ return {
14377
+ baseRef: "origin/main",
14378
+ mergeBase: null,
14379
+ mainRange: "HEAD~1..HEAD",
14380
+ mainChangedFiles: [],
14381
+ worktreeChangedFiles: []
14382
+ };
14383
+ }
14128
14384
  function appendDecisionLog(content, entry) {
14129
14385
  const normalized = content.trimEnd();
14130
14386
  if (!normalized) return `${entry.trim()}
@@ -14216,12 +14472,15 @@ async function runPrePrReview(featureName, options) {
14216
14472
  }
14217
14473
  const note = options.note?.trim() || (decision === "approve" ? "baseline review completed" : decision === "changes_requested" ? "follow-up changes are required before PR creation" : "blocked until prerequisite risk is resolved");
14218
14474
  let evidenceObj;
14475
+ let reviewScope = createFallbackReviewScope();
14219
14476
  if (options.evidence) {
14220
14477
  const validator = new PrePrReviewValidator(ctx);
14221
- evidenceObj = await validator.validateEvidence(
14478
+ const validationResult = await validator.validateEvidenceWithScope(
14222
14479
  options.evidence,
14223
14480
  process.cwd()
14224
14481
  );
14482
+ evidenceObj = validationResult.evidence;
14483
+ reviewScope = validationResult.scope;
14225
14484
  } else if (policy.evidenceMode === "path_required") {
14226
14485
  throw createCliError(
14227
14486
  "INVALID_ARGUMENT",
@@ -14229,6 +14488,12 @@ async function runPrePrReview(featureName, options) {
14229
14488
  );
14230
14489
  } else {
14231
14490
  evidenceObj = DEFAULT_EVIDENCE_FOR_ANY_MODE;
14491
+ const validator = new PrePrReviewValidator(ctx);
14492
+ try {
14493
+ reviewScope = await validator.collectReviewScope(process.cwd());
14494
+ } catch {
14495
+ reviewScope = createFallbackReviewScope();
14496
+ }
14232
14497
  }
14233
14498
  const decisionsPath = path12.join(feature.path, "decisions.md");
14234
14499
  const decisionLogEntry = buildReportContent({
@@ -14238,7 +14503,8 @@ async function runPrePrReview(featureName, options) {
14238
14503
  note,
14239
14504
  fallback: policy.fallback,
14240
14505
  skills: policy.skills,
14241
- evidence: evidenceObj
14506
+ evidence: evidenceObj,
14507
+ scope: reviewScope
14242
14508
  });
14243
14509
  const decisionsContent = await fs.pathExists(decisionsPath) ? await fs.readFile(decisionsPath, "utf-8") : "";
14244
14510
  const nextDecisions = appendDecisionLog(decisionsContent, decisionLogEntry);
@@ -14285,6 +14551,7 @@ async function runPrePrReview(featureName, options) {
14285
14551
  decisionsPath: normalizePathForDoc(decisionsPath),
14286
14552
  evidencePath,
14287
14553
  decision,
14554
+ reviewScope,
14288
14555
  tasksUpdated: nextTasks !== tasksContent
14289
14556
  },
14290
14557
  null,