lee-spec-kit 0.5.0 → 0.5.2

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
@@ -128,10 +128,12 @@ var I18N = {
128
128
  "context.tipShowAll": "\uC804\uCCB4 \uBCF4\uAE30",
129
129
  "context.tipShowDone": "\uC644\uB8CC\uB9CC \uBCF4\uAE30",
130
130
  "context.checkRequired": "[\uD655\uC778 \uD544\uC694] ",
131
- "context.checkPolicyHint": "\u2139\uFE0F \uC0AC\uC6A9\uC790 \uD655\uC778 \uADDC\uCE59: /docs/agents/agents.md \uCC38\uACE0 (git push/merge/merge commit \uD3EC\uD568) \u2014 [\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)",
131
+ "context.checkPolicyHint": "\u2139\uFE0F \uC0AC\uC6A9\uC790 \uD655\uC778 \uC815\uCC45 \uC548\uB0B4(\uD604\uC7AC Next Action \uC544\uB2D8): /docs/agents/agents.md \uCC38\uACE0 (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)",
132
132
  "context.actionOptionHint": "\uC2B9\uC778 \uC751\uB2F5 \uD615\uC2DD: `<\uB77C\uBCA8>` \uB610\uB294 `<\uB77C\uBCA8> OK` (\uC608: `A`, `A OK`)",
133
+ "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.",
133
134
  "context.tipDocsCommitRules": "\uCEE4\uBC0B \uBA54\uC2DC\uC9C0 \uADDC\uCE59: /docs/agents/git-workflow.md \uCC38\uACE0",
134
135
  "context.list.docsCommitNeeded": "\uBB38\uC11C \uCEE4\uBC0B \uD544\uC694",
136
+ "context.list.projectCommitNeeded": "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uCEE4\uBC0B \uD544\uC694",
135
137
  "context.list.issueNumberNeeded": "\uC774\uC288 \uBC88\uD638 \uAE30\uB85D \uD544\uC694",
136
138
  "context.list.addPrMetadata": "PR \uBA54\uD0C0\uB370\uC774\uD130(PR/PR \uC0C1\uD0DC) \uCD94\uAC00",
137
139
  "context.list.recordPrLink": "PR \uB9C1\uD06C \uAE30\uB85D",
@@ -222,6 +224,8 @@ var I18N = {
222
224
  issueCreateAndWrite: "GitHub Issue\uB97C \uC0DD\uC131\uD55C \uB4A4, spec.md/tasks.md\uC758 \uC774\uC288 \uBC88\uD638\uB97C \uCC44\uC6B0\uACE0 \uBB38\uC11C \uCEE4\uBC0B\uC744 \uC900\uBE44\uD558\uC138\uC694. (skills/create-issue.md \uCC38\uACE0)",
223
225
  docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
224
226
  docsCommitUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs: {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
227
+ projectCommitIssueUpdate: 'cd "{projectGitCwd}" && git add -A && git commit -m "feat(#{issueNumber}): {folderName} \uAD6C\uD604 \uC5C5\uB370\uC774\uD2B8"',
228
+ projectCommitUpdate: 'cd "{projectGitCwd}" && git add -A && git commit -m "feat: {folderName} \uAD6C\uD604 \uC5C5\uB370\uC774\uD2B8"',
225
229
  standaloneNeedsProjectRoot: "standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)",
226
230
  createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
227
231
  tasksAllDoneButNoChecklist: '\uBAA8\uB4E0 \uD0DC\uC2A4\uD06C\uAC00 DONE\uC774\uC9C0\uB9CC \uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uC139\uC158\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. tasks.md\uC758 "\uC644\uB8CC \uC870\uAC74" \uC139\uC158\uC744 \uCD94\uAC00/\uD655\uC778\uD558\uC138\uC694.',
@@ -242,11 +246,13 @@ var I18N = {
242
246
  docsGitUnavailable: "docs \uB808\uD3EC\uC758 git \uC0C1\uD0DC\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (\uB808\uD3EC \uC704\uCE58 / git init \uD655\uC778)",
243
247
  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.)",
244
248
  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: /docs/agents/git-workflow.md \uCC38\uACE0",
249
+ 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)",
245
250
  legacyTasksDocStatusField: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. `\uBB38\uC11C \uC0C1\uD0DC` \uD544\uB4DC(Review/Approved)\uB97C \uCD94\uAC00\uD574 \uD0DC\uC2A4\uD06C \uC2B9\uC778 \uB2E8\uACC4\uB97C \uD65C\uC131\uD654\uD558\uC138\uC694.",
246
251
  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.",
247
252
  workflowSpecNotApproved: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC spec.md \uC0C1\uD0DC\uAC00 Approved\uAC00 \uC544\uB2D9\uB2C8\uB2E4. (spec.md\uC758 \uC0C1\uD0DC\uB97C Approved\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.)",
248
253
  workflowPlanNotApproved: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC plan.md \uC0C1\uD0DC\uAC00 Approved\uAC00 \uC544\uB2D9\uB2C8\uB2E4. (plan.md\uC758 \uC0C1\uD0DC\uB97C Approved\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.)",
249
254
  workflowIssueMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC \uC774\uC288 \uBC88\uD638\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. (spec.md/tasks.md\uC758 \uC774\uC288 \uBC88\uD638\uB97C \uCC44\uC6B0\uC138\uC694.)",
255
+ workflowProjectUncommittedChanges: "\uC644\uB8CC \uC870\uAC74 \uC774\uC804\uC5D0 \uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC \uBCC0\uACBD\uC0AC\uD56D\uC744 \uCEE4\uBC0B\uD574\uC57C \uD569\uB2C8\uB2E4. (\uD504\uB85C\uC81D\uD2B8 \uC6CC\uD06C\uD2B8\uB9AC \uBBF8\uCEE4\uBC0B \uBCC0\uACBD \uC874\uC7AC)",
250
256
  workflowPrLinkMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC PR \uB9C1\uD06C\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. (tasks.md\uC758 PR \uD544\uB4DC\uB97C \uCC44\uC6B0\uC138\uC694.)",
251
257
  workflowPrStatusMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC PR \uC0C1\uD0DC\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. (tasks.md\uC758 PR \uC0C1\uD0DC\uB97C Review/Approved \uC911 \uD558\uB098\uB85C \uC124\uC815\uD558\uC138\uC694.)",
252
258
  workflowPrStatusNotApproved: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC PR \uC0C1\uD0DC\uAC00 Approved\uAC00 \uC544\uB2D9\uB2C8\uB2E4. (merge \uD6C4 PR \uC0C1\uD0DC\uB97C Approved\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.)"
@@ -319,10 +325,12 @@ var I18N = {
319
325
  "context.tipShowAll": "Show all",
320
326
  "context.tipShowDone": "Show done only",
321
327
  "context.checkRequired": "[CHECK required] ",
322
- "context.checkPolicyHint": "\u2139\uFE0F User check policy: see /docs/agents/agents.md (includes git push/merge and merge commits) \u2014 if you see [CHECK required], wait for `<label>` or `<label> OK` (e.g. `A`, `A OK`) before proceeding (config: approval can override)",
328
+ "context.checkPolicyHint": "\u2139\uFE0F User check policy notice (not the current next action): see /docs/agents/agents.md (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)",
323
329
  "context.actionOptionHint": "Approval reply format: `<label>` or `<label> OK` (e.g. `A`, `A OK`)",
330
+ "context.actionExplainHint": "Before requesting approval, explain what each label will run/change with a one-line summary.",
324
331
  "context.tipDocsCommitRules": "Commit message rules: /docs/agents/git-workflow.md",
325
332
  "context.list.docsCommitNeeded": "Commit docs changes",
333
+ "context.list.projectCommitNeeded": "Commit project code changes",
326
334
  "context.list.issueNumberNeeded": "Fill issue number in docs",
327
335
  "context.list.addPrMetadata": "Add PR metadata (PR/PR Status)",
328
336
  "context.list.recordPrLink": "Record PR link",
@@ -413,6 +421,8 @@ var I18N = {
413
421
  issueCreateAndWrite: "Create a GitHub Issue, fill the issue number in spec.md/tasks.md, then prepare a docs commit. (See skills/create-issue.md)",
414
422
  docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} docs update"',
415
423
  docsCommitUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs: {folderName} docs update"',
424
+ projectCommitIssueUpdate: 'cd "{projectGitCwd}" && git add -A && git commit -m "feat(#{issueNumber}): {folderName} implementation update"',
425
+ projectCommitUpdate: 'cd "{projectGitCwd}" && git add -A && git commit -m "feat: {folderName} implementation update"',
416
426
  standaloneNeedsProjectRoot: "Standalone mode requires projectRoot. (npx lee-spec-kit config --project-root ...)",
417
427
  createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
418
428
  tasksAllDoneButNoChecklist: 'All tasks are DONE, but no completion checklist section was found. Add/verify the "Completion Criteria" section in tasks.md.',
@@ -433,11 +443,13 @@ var I18N = {
433
443
  docsGitUnavailable: "Cannot read git status for the docs repo. (Check repo location / git init.)",
434
444
  docsPathIgnored: "Current feature docs path is ignored by git: {path} (docs commit detection may be limited).",
435
445
  docsUncommittedChanges: "Docs changes are not committed. (Additional docs commit needed.) Commit message rules: /docs/agents/git-workflow.md",
446
+ projectUncommittedChanges: "Project code changes are not committed. (Additional code commit needed.)",
436
447
  legacyTasksDocStatusField: "Legacy tasks.md format detected. Add a `Doc Status` field (Review/Approved) to enable tasks approval.",
437
448
  legacyTasksPrFields: "Legacy tasks.md format detected. Add `PR` and `PR Status` fields before PR steps.",
438
449
  workflowSpecNotApproved: "Implementation is done but spec.md Status is not Approved. (Update spec.md Status to Approved.)",
439
450
  workflowPlanNotApproved: "Implementation is done but plan.md Status is not Approved. (Update plan.md Status to Approved.)",
440
451
  workflowIssueMissing: "Implementation is done but Issue Number is missing. (Fill Issue Number in spec.md/tasks.md.)",
452
+ workflowProjectUncommittedChanges: "Commit project code changes before completing workflow. (Project worktree has uncommitted changes.)",
441
453
  workflowPrLinkMissing: "Implementation is done but PR link is missing. (Fill the PR field in tasks.md.)",
442
454
  workflowPrStatusMissing: "Implementation is done but PR Status is missing. (Set PR Status to Review/Approved in tasks.md.)",
443
455
  workflowPrStatusNotApproved: "Implementation is done but PR Status is not Approved. (After merge, update PR Status to Approved in tasks.md.)"
@@ -1628,7 +1640,7 @@ Store ${component} feature specs here.
1628
1640
  lang,
1629
1641
  createdAt: getLocalDateString(),
1630
1642
  docsRepo,
1631
- workflow: { mode: workflowMode },
1643
+ workflow: { mode: workflowMode, codeDirtyScope: "auto" },
1632
1644
  pr: {
1633
1645
  screenshots: { upload: false }
1634
1646
  },
@@ -1858,13 +1870,39 @@ async function getConfig(cwd) {
1858
1870
 
1859
1871
  // src/commands/feature.ts
1860
1872
  function featureCommand(program2) {
1861
- program2.command("feature <name>").description("Create a new feature folder").option("-r, --repo <repo>", "Component name (multi only)").option("--component <component>", "Component name (multi only)").option("--id <id>", "Feature ID (default: auto)").option("-d, --desc <description>", "Feature description for spec.md").option("--non-interactive", "Fail instead of prompting for input").action(async (name, options) => {
1873
+ program2.command("feature <name>").description("Create a new feature folder").option("-r, --repo <repo>", "Component name (multi only)").option("--component <component>", "Component name (multi only)").option("--id <id>", "Feature ID (default: auto)").option("-d, --desc <description>", "Feature description for spec.md").option("--non-interactive", "Fail instead of prompting for input").option("--json", "Output in JSON format for agents").action(async (name, options) => {
1862
1874
  try {
1863
- await runFeature(name, options);
1875
+ const result = await runFeature(name, options);
1876
+ if (options.json) {
1877
+ console.log(
1878
+ JSON.stringify(
1879
+ {
1880
+ status: "ok",
1881
+ reasonCode: "FEATURE_CREATED",
1882
+ featureId: result.featureId,
1883
+ featureName: result.featureName,
1884
+ component: result.component,
1885
+ featurePath: result.featurePath,
1886
+ featurePathFromDocs: result.featurePathFromDocs
1887
+ },
1888
+ null,
1889
+ 2
1890
+ )
1891
+ );
1892
+ }
1864
1893
  } catch (error) {
1865
1894
  if (error instanceof Error && error.message === "canceled") {
1866
1895
  const config2 = await getConfig(process.cwd());
1867
1896
  const lang2 = config2?.lang ?? DEFAULT_LANG;
1897
+ if (options.json) {
1898
+ console.log(
1899
+ JSON.stringify({
1900
+ status: "canceled",
1901
+ reasonCode: "CANCELED"
1902
+ })
1903
+ );
1904
+ process.exit(0);
1905
+ }
1868
1906
  console.log(chalk6.yellow(`
1869
1907
  ${tr(lang2, "cli", "common.canceled")}`));
1870
1908
  process.exit(0);
@@ -1873,6 +1911,17 @@ ${tr(lang2, "cli", "common.canceled")}`));
1873
1911
  const lang = config?.lang ?? DEFAULT_LANG;
1874
1912
  const cliError = toCliError(error);
1875
1913
  const suggestions = getCliErrorSuggestions(cliError.code, lang);
1914
+ if (options.json) {
1915
+ console.log(
1916
+ JSON.stringify({
1917
+ status: "error",
1918
+ reasonCode: cliError.code,
1919
+ error: cliError.message,
1920
+ suggestions
1921
+ })
1922
+ );
1923
+ process.exit(1);
1924
+ }
1876
1925
  console.error(
1877
1926
  chalk6.red(tr(lang, "cli", "common.errorLabel")),
1878
1927
  chalk6.red(`[${cliError.code}] ${cliError.message}`)
@@ -1944,7 +1993,7 @@ async function runFeature(name, options) {
1944
1993
  assertAllowedComponent(component, configuredComponents);
1945
1994
  }
1946
1995
  const docsLockPath = getDocsLockPath(docsDir);
1947
- await withFileLock(
1996
+ return withFileLock(
1948
1997
  docsLockPath,
1949
1998
  async () => {
1950
1999
  let featureId;
@@ -1980,6 +2029,8 @@ async function runFeature(name, options) {
1980
2029
  // ko placeholders
1981
2030
  "{\uAE30\uB2A5\uBA85}": name,
1982
2031
  "{\uBC88\uD638}": idNumber,
2032
+ "{\uACB0\uC815 \uC81C\uBAA9}": `${name} \uACB0\uC815`,
2033
+ "{YYYY-MM-DD}": getLocalDateString(),
1983
2034
  "YYYY-MM-DD": getLocalDateString(),
1984
2035
  "{be|fe}": component || "",
1985
2036
  "{\uC774\uC288\uBC88\uD638}": "",
@@ -1987,6 +2038,7 @@ async function runFeature(name, options) {
1987
2038
  // en placeholders
1988
2039
  "{feature-name}": name,
1989
2040
  "{number}": idNumber,
2041
+ "{Decision Title}": `${name} design decision`,
1990
2042
  "{issue-number}": "",
1991
2043
  "{{projectName}}-{be|fe}": repoName
1992
2044
  };
@@ -2002,14 +2054,23 @@ async function runFeature(name, options) {
2002
2054
  if (config.workflow?.mode === "local") {
2003
2055
  await applyLocalWorkflowTemplateToFeatureDir(featureDir, lang);
2004
2056
  }
2005
- console.log();
2006
- console.log(chalk6.green(tr(lang, "cli", "feature.created", { path: featureDir })));
2007
- console.log();
2008
- console.log(chalk6.blue(tr(lang, "cli", "feature.nextStepsTitle")));
2009
- console.log(chalk6.gray(tr(lang, "cli", "feature.nextSteps1", { path: featureDir })));
2010
- console.log(chalk6.gray(tr(lang, "cli", "feature.nextSteps2")));
2011
- console.log(chalk6.gray(tr(lang, "cli", "feature.nextSteps3")));
2012
- console.log();
2057
+ if (!options.json) {
2058
+ console.log();
2059
+ console.log(chalk6.green(tr(lang, "cli", "feature.created", { path: featureDir })));
2060
+ console.log();
2061
+ console.log(chalk6.blue(tr(lang, "cli", "feature.nextStepsTitle")));
2062
+ console.log(chalk6.gray(tr(lang, "cli", "feature.nextSteps1", { path: featureDir })));
2063
+ console.log(chalk6.gray(tr(lang, "cli", "feature.nextSteps2")));
2064
+ console.log(chalk6.gray(tr(lang, "cli", "feature.nextSteps3")));
2065
+ console.log();
2066
+ }
2067
+ return {
2068
+ featureId,
2069
+ featureName: name,
2070
+ component: projectType === "multi" ? component : void 0,
2071
+ featurePath: featureDir,
2072
+ featurePathFromDocs: path13.relative(docsDir, featureDir)
2073
+ };
2013
2074
  },
2014
2075
  { owner: "feature" }
2015
2076
  );
@@ -2112,6 +2173,15 @@ function resolveWorkflowPolicy(workflow) {
2112
2173
  }
2113
2174
  return policy;
2114
2175
  }
2176
+ function resolveCodeDirtyScopePolicy(workflow, projectType) {
2177
+ const raw = workflow?.codeDirtyScope;
2178
+ if (!raw) return "repo";
2179
+ if (raw === "repo") return "repo";
2180
+ if (raw === "component") {
2181
+ return projectType === "multi" ? "component" : "repo";
2182
+ }
2183
+ return projectType === "multi" ? "component" : "repo";
2184
+ }
2115
2185
 
2116
2186
  // src/utils/context/steps.ts
2117
2187
  function isCompletionChecklistDone(feature) {
@@ -2127,7 +2197,7 @@ function isPrMetadataConfigured(feature) {
2127
2197
  return feature.docs.prFieldExists && feature.docs.prStatusFieldExists;
2128
2198
  }
2129
2199
  function isFeatureDone(feature, workflowPolicy) {
2130
- return feature.specStatus === "Approved" && feature.planStatus === "Approved" && feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature) && isTasksDocApproved(feature) && (!workflowPolicy.requireIssue || !!feature.issueNumber) && (!workflowPolicy.requirePr || isPrMetadataConfigured(feature) && !!feature.pr.link) && (!workflowPolicy.requireReview || feature.pr.status === "Approved");
2200
+ return feature.specStatus === "Approved" && feature.planStatus === "Approved" && !feature.git.docsHasUncommittedChanges && !feature.git.projectHasUncommittedChanges && feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature) && isTasksDocApproved(feature) && (!workflowPolicy.requireIssue || !!feature.issueNumber) && (!workflowPolicy.requirePr || isPrMetadataConfigured(feature) && !!feature.pr.link) && (!workflowPolicy.requireReview || feature.pr.status === "Approved");
2131
2201
  }
2132
2202
  function getStepDefinitions(lang, workflow) {
2133
2203
  const workflowPolicy = resolveWorkflowPolicy(workflow);
@@ -2460,29 +2530,59 @@ function getStepDefinitions(lang, workflow) {
2460
2530
  step: 11,
2461
2531
  name: tr(lang, "steps", "docsCommitSync"),
2462
2532
  checklist: {
2463
- done: (f) => !f.git.docsHasUncommittedChanges
2533
+ done: (f) => !f.git.docsHasUncommittedChanges && !f.git.projectHasUncommittedChanges
2464
2534
  },
2465
2535
  current: {
2466
- when: (f) => isImplementationDone(f) && f.git.docsHasUncommittedChanges,
2467
- actions: (f) => [
2468
- {
2469
- type: "command",
2470
- category: "docs_commit",
2471
- requiresUserCheck: true,
2472
- scope: "docs",
2473
- cwd: f.git.docsGitCwd,
2474
- cmd: f.issueNumber ? tr(lang, "messages", "docsCommitIssueUpdate", {
2475
- docsGitCwd: f.git.docsGitCwd,
2476
- featurePath: f.docs.featurePathFromDocs,
2477
- issueNumber: f.issueNumber,
2478
- folderName: f.folderName
2479
- }) : tr(lang, "messages", "docsCommitUpdate", {
2480
- docsGitCwd: f.git.docsGitCwd,
2481
- featurePath: f.docs.featurePathFromDocs,
2482
- folderName: f.folderName
2483
- })
2536
+ when: (f) => isImplementationDone(f) && (f.git.docsHasUncommittedChanges || f.git.projectHasUncommittedChanges),
2537
+ actions: (f) => {
2538
+ if (f.git.docsHasUncommittedChanges) {
2539
+ return [
2540
+ {
2541
+ type: "command",
2542
+ category: "docs_commit",
2543
+ requiresUserCheck: true,
2544
+ scope: "docs",
2545
+ cwd: f.git.docsGitCwd,
2546
+ cmd: f.issueNumber ? tr(lang, "messages", "docsCommitIssueUpdate", {
2547
+ docsGitCwd: f.git.docsGitCwd,
2548
+ featurePath: f.docs.featurePathFromDocs,
2549
+ issueNumber: f.issueNumber,
2550
+ folderName: f.folderName
2551
+ }) : tr(lang, "messages", "docsCommitUpdate", {
2552
+ docsGitCwd: f.git.docsGitCwd,
2553
+ featurePath: f.docs.featurePathFromDocs,
2554
+ folderName: f.folderName
2555
+ })
2556
+ }
2557
+ ];
2484
2558
  }
2485
- ]
2559
+ if (!f.git.projectGitCwd) {
2560
+ return [
2561
+ {
2562
+ type: "instruction",
2563
+ category: "task_execute",
2564
+ message: tr(lang, "messages", "standaloneNeedsProjectRoot")
2565
+ }
2566
+ ];
2567
+ }
2568
+ return [
2569
+ {
2570
+ type: "command",
2571
+ category: "task_execute",
2572
+ requiresUserCheck: true,
2573
+ scope: "project",
2574
+ cwd: f.git.projectGitCwd,
2575
+ cmd: f.issueNumber ? tr(lang, "messages", "projectCommitIssueUpdate", {
2576
+ projectGitCwd: f.git.projectGitCwd,
2577
+ issueNumber: f.issueNumber,
2578
+ folderName: f.folderName
2579
+ }) : tr(lang, "messages", "projectCommitUpdate", {
2580
+ projectGitCwd: f.git.projectGitCwd,
2581
+ folderName: f.folderName
2582
+ })
2583
+ }
2584
+ ];
2585
+ }
2486
2586
  }
2487
2587
  },
2488
2588
  {
@@ -2805,6 +2905,60 @@ function parsePrLink(value) {
2805
2905
  if (trimmed.includes("{") || trimmed.includes("}")) return void 0;
2806
2906
  return trimmed;
2807
2907
  }
2908
+ function normalizeGitPath(value) {
2909
+ return value.split(path13.sep).join("/");
2910
+ }
2911
+ function resolveProjectStatusPaths(projectGitCwd, docsDir) {
2912
+ const relativeDocsDir = path13.relative(projectGitCwd, docsDir);
2913
+ if (!relativeDocsDir) return [];
2914
+ if (path13.isAbsolute(relativeDocsDir)) return [];
2915
+ if (relativeDocsDir === ".." || relativeDocsDir.startsWith(`..${path13.sep}`)) {
2916
+ return [];
2917
+ }
2918
+ const normalizedDocsDir = normalizeGitPath(relativeDocsDir).replace(/\/+$/, "");
2919
+ if (!normalizedDocsDir) return [];
2920
+ return [".", `:(exclude)${normalizedDocsDir}/**`];
2921
+ }
2922
+ function uniqueNormalizedPaths(values) {
2923
+ const seen = /* @__PURE__ */ new Set();
2924
+ const out = [];
2925
+ for (const value of values) {
2926
+ const normalized = normalizeGitPath(value).replace(/^\.\/+/, "").replace(/\/+$/, "");
2927
+ if (!normalized || normalized === "." || normalized === "..") continue;
2928
+ if (seen.has(normalized)) continue;
2929
+ seen.add(normalized);
2930
+ out.push(normalized);
2931
+ }
2932
+ return out;
2933
+ }
2934
+ async function resolveComponentStatusPaths(projectGitCwd, component, workflow) {
2935
+ const configured = workflow?.componentPaths?.[component];
2936
+ const configuredCandidates = Array.isArray(configured) ? configured.map((value) => String(value).trim()).filter(Boolean) : [];
2937
+ const candidates = configuredCandidates.length > 0 ? configuredCandidates : [
2938
+ component,
2939
+ `apps/${component}`,
2940
+ `packages/${component}`,
2941
+ `services/${component}`,
2942
+ `modules/${component}`
2943
+ ];
2944
+ const normalizedCandidates = uniqueNormalizedPaths(
2945
+ candidates.map((candidate) => {
2946
+ if (!candidate) return "";
2947
+ if (!path13.isAbsolute(candidate)) return candidate;
2948
+ const relative = path13.relative(projectGitCwd, candidate);
2949
+ if (!relative) return "";
2950
+ if (relative === ".." || relative.startsWith(`..${path13.sep}`)) return "";
2951
+ return relative;
2952
+ }).filter(Boolean)
2953
+ );
2954
+ const existing = [];
2955
+ for (const candidate of normalizedCandidates) {
2956
+ if (await fs2.pathExists(path13.join(projectGitCwd, candidate))) {
2957
+ existing.push(candidate);
2958
+ }
2959
+ }
2960
+ return existing;
2961
+ }
2808
2962
  function parseTasks(content) {
2809
2963
  const summary = { total: 0, todo: 0, doing: 0, done: 0 };
2810
2964
  let activeTask;
@@ -2937,6 +3091,25 @@ async function parseFeature(featurePath, type, context, options) {
2937
3091
  );
2938
3092
  const docsStatus = getGitStatusPorcelain(context.docsGitCwd, [relativeFeaturePathFromDocs]);
2939
3093
  const docsHasUncommittedChanges = docsStatus === void 0 ? true : docsStatus.trim().length > 0;
3094
+ const dirtyScopePolicy = resolveCodeDirtyScopePolicy(options.workflow, options.projectType);
3095
+ let projectStatusPaths = [];
3096
+ if (context.projectGitCwd) {
3097
+ if (dirtyScopePolicy === "component" && type !== "single") {
3098
+ const componentStatusPaths = await resolveComponentStatusPaths(
3099
+ context.projectGitCwd,
3100
+ type,
3101
+ options.workflow
3102
+ );
3103
+ projectStatusPaths = componentStatusPaths.length > 0 ? componentStatusPaths : resolveProjectStatusPaths(context.projectGitCwd, context.docsDir);
3104
+ } else {
3105
+ projectStatusPaths = resolveProjectStatusPaths(
3106
+ context.projectGitCwd,
3107
+ context.docsDir
3108
+ );
3109
+ }
3110
+ }
3111
+ const projectStatus = context.projectGitCwd ? getGitStatusPorcelain(context.projectGitCwd, projectStatusPaths) : void 0;
3112
+ const projectHasUncommittedChanges = projectStatus === void 0 ? false : projectStatus.trim().length > 0;
2940
3113
  const docsLastCommit = getLastCommitForPath(
2941
3114
  context.docsGitCwd,
2942
3115
  relativeFeaturePathFromDocs
@@ -2961,9 +3134,12 @@ async function parseFeature(featurePath, type, context, options) {
2961
3134
  if (docsEverCommitted && docsHasUncommittedChanges) {
2962
3135
  warnings.push(tr(lang, "warnings", "docsUncommittedChanges"));
2963
3136
  }
3137
+ if (projectHasUncommittedChanges) {
3138
+ warnings.push(tr(lang, "warnings", "projectUncommittedChanges"));
3139
+ }
2964
3140
  const tasksDocApproved = !tasksDocStatusFieldExists || tasksDocStatus === "Approved";
2965
3141
  const implementationDone = tasksExists && tasksSummary.total > 0 && tasksSummary.total === tasksSummary.done && isCompletionChecklistDone2({ completionChecklist }) && tasksDocApproved;
2966
- const workflowDone = implementationDone && specStatus === "Approved" && planStatus === "Approved" && (!workflowPolicy.requireIssue || !!issueNumber) && (!workflowPolicy.requirePr || isPrMetadataConfigured2({ docs: { prFieldExists, prStatusFieldExists } }) && !!prLink) && (!workflowPolicy.requireReview || prStatus === "Approved");
3142
+ const workflowDone = implementationDone && !docsHasUncommittedChanges && !projectHasUncommittedChanges && specStatus === "Approved" && planStatus === "Approved" && (!workflowPolicy.requireIssue || !!issueNumber) && (!workflowPolicy.requirePr || isPrMetadataConfigured2({ docs: { prFieldExists, prStatusFieldExists } }) && !!prLink) && (!workflowPolicy.requireReview || prStatus === "Approved");
2967
3143
  if (implementationDone && !workflowDone) {
2968
3144
  if (specStatus !== "Approved") {
2969
3145
  warnings.push(tr(lang, "warnings", "workflowSpecNotApproved"));
@@ -2974,6 +3150,9 @@ async function parseFeature(featurePath, type, context, options) {
2974
3150
  if (workflowPolicy.requireIssue && !issueNumber) {
2975
3151
  warnings.push(tr(lang, "warnings", "workflowIssueMissing"));
2976
3152
  }
3153
+ if (projectHasUncommittedChanges) {
3154
+ warnings.push(tr(lang, "warnings", "workflowProjectUncommittedChanges"));
3155
+ }
2977
3156
  if (workflowPolicy.requirePr && prFieldExists && prStatusFieldExists) {
2978
3157
  if (!prLink) warnings.push(tr(lang, "warnings", "workflowPrLinkMissing"));
2979
3158
  if (workflowPolicy.requireReview) {
@@ -3012,6 +3191,7 @@ async function parseFeature(featurePath, type, context, options) {
3012
3191
  onExpectedBranch,
3013
3192
  docsEverCommitted,
3014
3193
  docsHasUncommittedChanges,
3194
+ projectHasUncommittedChanges,
3015
3195
  docsPathIgnored: docsPathIgnored === true
3016
3196
  },
3017
3197
  docs: {
@@ -3075,7 +3255,8 @@ async function scanFeatures(config) {
3075
3255
  lang: config.lang,
3076
3256
  stepDefinitions,
3077
3257
  approval: config.approval,
3078
- workflow: config.workflow
3258
+ workflow: config.workflow,
3259
+ projectType: config.projectType
3079
3260
  }
3080
3261
  )
3081
3262
  );
@@ -3107,7 +3288,8 @@ async function scanFeatures(config) {
3107
3288
  lang: config.lang,
3108
3289
  stepDefinitions,
3109
3290
  approval: config.approval,
3110
- workflow: config.workflow
3291
+ workflow: config.workflow,
3292
+ projectType: config.projectType
3111
3293
  }
3112
3294
  )
3113
3295
  );
@@ -3326,7 +3508,8 @@ async function runUpdate(options) {
3326
3508
  async () => {
3327
3509
  const templatesDir = getTemplatesDir();
3328
3510
  const sourceDir = path13.join(templatesDir, lang, toTemplateProjectType(projectType));
3329
- const forceOverwrite = !!options.force || await isDocsWorktreeCleanOrThrow(docsDir, lang);
3511
+ const docsLockPath = getDocsLockPath(docsDir);
3512
+ const forceOverwrite = !!options.force || await isDocsWorktreeCleanOrThrow(docsDir, lang, [docsLockPath]);
3330
3513
  const hasExplicitSelection = !!(options.agents || options.skills || options.templates);
3331
3514
  const updateAgents = options.agents || options.skills || !hasExplicitSelection;
3332
3515
  const updateTemplates = options.templates || !hasExplicitSelection;
@@ -3483,22 +3666,56 @@ function getGitTopLevel2(cwd) {
3483
3666
  return null;
3484
3667
  }
3485
3668
  }
3486
- function getDocsPorcelainStatus(docsDir) {
3669
+ function normalizeGitPath2(input) {
3670
+ return input.replace(/\\/g, "/").replace(/^\.\/+/, "");
3671
+ }
3672
+ function stripOuterQuotes(input) {
3673
+ const trimmed = input.trim();
3674
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
3675
+ return trimmed.slice(1, -1).replace(/\\"/g, '"');
3676
+ }
3677
+ return trimmed;
3678
+ }
3679
+ function extractPorcelainPaths(line) {
3680
+ if (line.length < 4) return [];
3681
+ const body = line.slice(3).trim();
3682
+ if (!body) return [];
3683
+ if (body.includes(" -> ")) {
3684
+ return body.split(" -> ").map((part) => normalizeGitPath2(stripOuterQuotes(part)));
3685
+ }
3686
+ return [normalizeGitPath2(stripOuterQuotes(body))];
3687
+ }
3688
+ function getDocsPorcelainStatus(docsDir, ignoredAbsPaths = []) {
3487
3689
  const top = getGitTopLevel2(docsDir);
3488
3690
  if (!top) return null;
3489
3691
  const rel = path13.relative(top, docsDir) || ".";
3490
3692
  try {
3491
- return execFileSync("git", ["status", "--porcelain=v1", "--", rel], {
3693
+ const output = execFileSync("git", ["status", "--porcelain=v1", "--", rel], {
3492
3694
  cwd: top,
3493
3695
  encoding: "utf-8",
3494
3696
  stdio: ["ignore", "pipe", "ignore"]
3495
3697
  });
3698
+ if (ignoredAbsPaths.length === 0) {
3699
+ return output;
3700
+ }
3701
+ const ignoredRelPaths = new Set(
3702
+ ignoredAbsPaths.map(
3703
+ (absPath) => normalizeGitPath2(path13.relative(top, absPath) || ".")
3704
+ )
3705
+ );
3706
+ const filtered = output.split("\n").filter((line) => {
3707
+ if (!line.trim()) return false;
3708
+ const touchedPaths = extractPorcelainPaths(line);
3709
+ if (touchedPaths.length === 0) return true;
3710
+ return touchedPaths.some((p) => !ignoredRelPaths.has(p));
3711
+ }).join("\n");
3712
+ return filtered;
3496
3713
  } catch {
3497
3714
  return null;
3498
3715
  }
3499
3716
  }
3500
- async function isDocsWorktreeCleanOrThrow(docsDir, lang) {
3501
- const status = getDocsPorcelainStatus(docsDir);
3717
+ async function isDocsWorktreeCleanOrThrow(docsDir, lang, ignoredAbsPaths = []) {
3718
+ const status = getDocsPorcelainStatus(docsDir, ignoredAbsPaths);
3502
3719
  if (status === null) {
3503
3720
  throw createCliError(
3504
3721
  "PRECONDITION_FAILED",
@@ -3652,6 +3869,22 @@ async function runConfig(options) {
3652
3869
  { owner: "config" }
3653
3870
  );
3654
3871
  }
3872
+ var REMOTE_ACTION_CATEGORIES = /* @__PURE__ */ new Set([
3873
+ "issue_create",
3874
+ "pr_create",
3875
+ "pr_status_update",
3876
+ "code_review"
3877
+ ]);
3878
+ var LOCAL_ACTION_CATEGORIES = /* @__PURE__ */ new Set([
3879
+ "docs_commit",
3880
+ "branch_create",
3881
+ "task_execute"
3882
+ ]);
3883
+ var REMOTE_COMMAND_PATTERN = /\b(?:git\s+push|git\s+merge|gh\s+(?:issue|pr)\b)/i;
3884
+ function resolveComponentOption(options) {
3885
+ const component = (options.component || options.repo || "").trim().toLowerCase();
3886
+ return component || void 0;
3887
+ }
3655
3888
  function getActionLabel(index) {
3656
3889
  let n = index + 1;
3657
3890
  let label = "";
@@ -3662,11 +3895,67 @@ function getActionLabel(index) {
3662
3895
  }
3663
3896
  return label;
3664
3897
  }
3898
+ function resolveActionOperationType(action) {
3899
+ if (action.operationType) return action.operationType;
3900
+ if (action.type === "command") {
3901
+ if (REMOTE_COMMAND_PATTERN.test(action.cmd)) return "remote";
3902
+ return "local";
3903
+ }
3904
+ if (action.category && REMOTE_ACTION_CATEGORIES.has(action.category)) {
3905
+ return "remote";
3906
+ }
3907
+ if (action.category && LOCAL_ACTION_CATEGORIES.has(action.category)) {
3908
+ return "local";
3909
+ }
3910
+ return "manual";
3911
+ }
3912
+ function annotateActionOperationType(action) {
3913
+ return {
3914
+ ...action,
3915
+ operationType: resolveActionOperationType(action)
3916
+ };
3917
+ }
3918
+ function annotateActions(actions) {
3919
+ return actions.map((action) => annotateActionOperationType(action));
3920
+ }
3921
+ function getActionSummary(action) {
3922
+ if (action.category === "docs_commit") return "Commit docs updates";
3923
+ if (action.category === "issue_create") return "Create and record issue";
3924
+ if (action.category === "branch_create") return "Create feature branch";
3925
+ if (action.category === "pr_create") return "Create PR and record link";
3926
+ if (action.category === "pr_status_update") return "Update PR status";
3927
+ if (action.category === "code_review") return "Process code review feedback";
3928
+ if (action.category === "task_execute") return "Proceed with task execution";
3929
+ if (action.category === "feature_done") return "Feature is complete";
3930
+ if (action.category === "spec_approve") return "Request spec approval";
3931
+ if (action.category === "plan_approve") return "Request plan approval";
3932
+ if (action.category === "tasks_approve") return "Request tasks approval";
3933
+ if (action.category === "pr_metadata_migrate") return "Update tasks.md to latest PR fields";
3934
+ if (action.category === "fallback") return "Re-check context and rerun";
3935
+ if (action.type === "command") {
3936
+ return action.scope === "docs" ? "Run docs command" : "Run project command";
3937
+ }
3938
+ return action.message;
3939
+ }
3940
+ function formatActionSummary(action) {
3941
+ if (action.type === "command") {
3942
+ return `(${action.scope}) ${action.cmd}`;
3943
+ }
3944
+ return action.message;
3945
+ }
3665
3946
  function toActionOptions(actions) {
3666
- return actions.map((action, index) => ({
3667
- label: getActionLabel(index),
3668
- action
3669
- }));
3947
+ return actions.map((action, index) => {
3948
+ const label = getActionLabel(index);
3949
+ const summary = getActionSummary(action);
3950
+ const detail = formatActionSummary(action);
3951
+ return {
3952
+ label,
3953
+ summary,
3954
+ detail,
3955
+ approvalPrompt: `${label}: ${summary}`,
3956
+ action
3957
+ };
3958
+ });
3670
3959
  }
3671
3960
  function buildActionSnapshot(actionOptions) {
3672
3961
  return actionOptions.map(({ label, action }) => {
@@ -3678,6 +3967,7 @@ function buildActionSnapshot(actionOptions) {
3678
3967
  cwd: action.cwd,
3679
3968
  cmd: action.cmd,
3680
3969
  category: action.category,
3970
+ operationType: action.operationType,
3681
3971
  requiresUserCheck: !!action.requiresUserCheck
3682
3972
  };
3683
3973
  }
@@ -3686,6 +3976,7 @@ function buildActionSnapshot(actionOptions) {
3686
3976
  type: action.type,
3687
3977
  message: action.message,
3688
3978
  category: action.category,
3979
+ operationType: action.operationType,
3689
3980
  requiresUserCheck: !!action.requiresUserCheck
3690
3981
  };
3691
3982
  });
@@ -3700,20 +3991,21 @@ function getContextVersion(feature, actionOptions) {
3700
3991
  });
3701
3992
  return createHash("sha256").update(payload).digest("hex").slice(0, 12);
3702
3993
  }
3703
- function parseApprovalLabel(input) {
3704
- const match = input.trim().match(/^([A-Z]+)(?:\s+OK)?$/i);
3705
- if (!match) return null;
3706
- return match[1].toUpperCase();
3707
- }
3708
- function listLabels(actionOptions) {
3709
- if (actionOptions.length === 0) return "-";
3710
- return actionOptions.map((o) => o.label).join(", ");
3994
+ function matchesFeatureSelector(f, selector) {
3995
+ const s = selector.trim();
3996
+ if (!s) return false;
3997
+ if (f.folderName.toLowerCase() === s.toLowerCase()) return true;
3998
+ if (f.slug.toLowerCase() === s.toLowerCase()) return true;
3999
+ if (f.id && f.id.toLowerCase() === s.toLowerCase()) return true;
4000
+ return false;
3711
4001
  }
3712
- function formatActionSummary(action) {
3713
- if (action.type === "command") {
3714
- return `(${action.scope}) ${action.cmd}`;
3715
- }
3716
- return action.message;
4002
+ function detectFromBranch(branchName, features) {
4003
+ const match = branchName.match(/^feat\/\d+-(.+)$/);
4004
+ if (!match) return [];
4005
+ const detected = match[1];
4006
+ return features.filter(
4007
+ (f) => f.slug.toLowerCase() === detected.toLowerCase() || f.folderName.toLowerCase() === detected.toLowerCase()
4008
+ );
3717
4009
  }
3718
4010
  function toSelectionStatus(features, selectionMode, openFeatures, targetFeatures) {
3719
4011
  const isNoOpen = selectionMode === "open" && features.length > 0 && openFeatures.length === 0;
@@ -3730,15 +4022,9 @@ function toReasonCode(status) {
3730
4022
  if (status === "multiple_active") return "MULTIPLE_ACTIVE_FEATURES";
3731
4023
  return "NO_MATCHED_FEATURES";
3732
4024
  }
3733
- async function resolveContextState(config, featureName, options) {
3734
- if (!config) {
3735
- throw createCliError(
3736
- "CONFIG_NOT_FOUND",
3737
- tr(DEFAULT_LANG, "cli", "common.configNotFound")
3738
- );
3739
- }
4025
+ async function resolveContextSelection(config, featureName, options) {
3740
4026
  const { features, branches, warnings } = await scanFeatures(config);
3741
- const selectedComponent = (options.component || options.repo || "").trim().toLowerCase();
4027
+ const selectedComponent = resolveComponentOption(options);
3742
4028
  const scopedFeatures = selectedComponent ? features.filter((f) => f.type === selectedComponent) : features;
3743
4029
  const doneFeatures = scopedFeatures.filter((f) => f.completion.workflowDone);
3744
4030
  const openFeatures = scopedFeatures.filter((f) => !f.completion.workflowDone);
@@ -3750,6 +4036,7 @@ async function resolveContextState(config, featureName, options) {
3750
4036
  );
3751
4037
  let targetFeatures = [];
3752
4038
  let selectionMode = "explicit";
4039
+ let selectionFallback = "none";
3753
4040
  if (featureName) {
3754
4041
  targetFeatures = scopedFeatures.filter(
3755
4042
  (f) => matchesFeatureSelector(f, featureName)
@@ -3782,15 +4069,19 @@ async function resolveContextState(config, featureName, options) {
3782
4069
  }
3783
4070
  if (targetFeatures.length > 0) {
3784
4071
  selectionMode = "branch";
4072
+ selectionFallback = "none";
3785
4073
  } else if (options.all) {
3786
4074
  targetFeatures = scopedFeatures;
3787
4075
  selectionMode = "all";
4076
+ selectionFallback = "all_features";
3788
4077
  } else if (options.done) {
3789
4078
  targetFeatures = doneFeatures;
3790
4079
  selectionMode = "done";
4080
+ selectionFallback = "done_features";
3791
4081
  } else {
3792
4082
  targetFeatures = openFeatures;
3793
4083
  selectionMode = "open";
4084
+ selectionFallback = "open_features";
3794
4085
  }
3795
4086
  }
3796
4087
  const status = toSelectionStatus(
@@ -3800,7 +4091,7 @@ async function resolveContextState(config, featureName, options) {
3800
4091
  targetFeatures
3801
4092
  );
3802
4093
  const matchedFeature = targetFeatures.length === 1 ? targetFeatures[0] : null;
3803
- const actions = matchedFeature?.actions ?? [];
4094
+ const actions = annotateActions(matchedFeature?.actions ?? []);
3804
4095
  const actionOptions = toActionOptions(actions);
3805
4096
  const contextVersion = getContextVersion(matchedFeature, actionOptions);
3806
4097
  return {
@@ -3812,6 +4103,7 @@ async function resolveContextState(config, featureName, options) {
3812
4103
  inProgressFeatures,
3813
4104
  readyToCloseFeatures,
3814
4105
  selectionMode,
4106
+ selectionFallback,
3815
4107
  targetFeatures,
3816
4108
  status,
3817
4109
  matchedFeature,
@@ -3820,6 +4112,32 @@ async function resolveContextState(config, featureName, options) {
3820
4112
  contextVersion
3821
4113
  };
3822
4114
  }
4115
+
4116
+ // src/commands/context.ts
4117
+ async function resolveContextState(config, featureName, options) {
4118
+ if (!config) {
4119
+ throw createCliError(
4120
+ "CONFIG_NOT_FOUND",
4121
+ tr(DEFAULT_LANG, "cli", "common.configNotFound")
4122
+ );
4123
+ }
4124
+ return resolveContextSelection(config, featureName, options);
4125
+ }
4126
+ function parseApprovalLabel(input) {
4127
+ const match = input.trim().match(/^([A-Z]+)(?:\s+OK)?$/i);
4128
+ if (!match) return null;
4129
+ return match[1].toUpperCase();
4130
+ }
4131
+ function listLabels(actionOptions) {
4132
+ if (actionOptions.length === 0) return "-";
4133
+ return actionOptions.map((o) => o.label).join(", ");
4134
+ }
4135
+ function formatActionSummary2(action) {
4136
+ if (action.type === "command") {
4137
+ return `(${action.scope}) ${action.cmd}`;
4138
+ }
4139
+ return action.message;
4140
+ }
3823
4141
  function executeCommandAction(cmd, jsonMode, cwd) {
3824
4142
  const shellPath = process.env.SHELL || (process.platform === "win32" ? process.env.ComSpec || "cmd.exe" : "/bin/sh");
3825
4143
  if (jsonMode) {
@@ -3878,27 +4196,14 @@ function contextCommand(program2) {
3878
4196
  }
3879
4197
  );
3880
4198
  }
3881
- function matchesFeatureSelector(f, selector) {
3882
- const s = selector.trim();
3883
- if (!s) return false;
3884
- if (f.folderName.toLowerCase() === s.toLowerCase()) return true;
3885
- if (f.slug.toLowerCase() === s.toLowerCase()) return true;
3886
- if (f.id && f.id.toLowerCase() === s.toLowerCase()) return true;
3887
- return false;
3888
- }
3889
- function detectFromBranch(branchName, features) {
3890
- const match = branchName.match(/^feat\/\d+-(.+)$/);
3891
- if (!match) return [];
3892
- const detected = match[1];
3893
- return features.filter(
3894
- (f) => f.slug.toLowerCase() === detected.toLowerCase() || f.folderName.toLowerCase() === detected.toLowerCase()
3895
- );
3896
- }
3897
4199
  function getListLabel(f, stepsMap, lang, workflowPolicy) {
3898
4200
  if (f.completion.implementationDone && !f.completion.workflowDone) {
3899
4201
  if (f.git.docsHasUncommittedChanges) {
3900
4202
  return tr(lang, "cli", "context.list.docsCommitNeeded");
3901
4203
  }
4204
+ if (f.git.projectHasUncommittedChanges) {
4205
+ return tr(lang, "cli", "context.list.projectCommitNeeded");
4206
+ }
3902
4207
  if (workflowPolicy.requireIssue && !f.issueNumber) {
3903
4208
  return tr(lang, "cli", "context.list.issueNumberNeeded");
3904
4209
  }
@@ -3984,10 +4289,12 @@ async function runContext(featureName, options) {
3984
4289
  return;
3985
4290
  }
3986
4291
  if (options.json) {
4292
+ const primaryAction = state.actionOptions[0] ?? null;
3987
4293
  const result = {
3988
4294
  status: state.status,
3989
4295
  reasonCode: toReasonCode(state.status),
3990
4296
  selectionMode: state.selectionMode,
4297
+ selectionFallback: state.selectionFallback,
3991
4298
  branches: state.branches,
3992
4299
  warnings: state.warnings,
3993
4300
  matchedFeature: state.matchedFeature,
@@ -3999,19 +4306,37 @@ async function runContext(featureName, options) {
3999
4306
  readyToCloseCandidates: state.selectionMode === "open" ? state.readyToCloseFeatures : [],
4000
4307
  actions: state.actions,
4001
4308
  actionOptions: state.actionOptions,
4309
+ primaryActionLabel: primaryAction?.label ?? null,
4310
+ primaryActionType: primaryAction?.action.type ?? null,
4311
+ primaryActionCategory: primaryAction?.action.category ?? null,
4312
+ primaryActionOperationType: primaryAction?.action.operationType ?? null,
4002
4313
  workflowPolicy,
4003
4314
  checkPolicy: {
4004
4315
  docPath: "/docs/agents/agents.md",
4005
4316
  hint: tr(lang, "cli", "context.checkPolicyHint"),
4317
+ policyOnly: true,
4006
4318
  token: "<LABEL>",
4007
4319
  acceptedTokens: ["<LABEL>", "<LABEL> OK"],
4008
4320
  tokenPattern: "^([A-Z]+)(?:\\s+OK)?$",
4009
4321
  validLabels: state.actionOptions.map((o) => o.label),
4322
+ requireExplanationBeforeApproval: true,
4323
+ requiredExplanationFields: ["actionOptions[].summary", "actionOptions[].approvalPrompt"],
4324
+ recommendation: "Before asking for approval, explain each label with summary and then ask for `<LABEL>` or `<LABEL> OK`.",
4010
4325
  oneApprovalPerAction: true,
4011
4326
  requireFreshContext: true,
4012
4327
  contextVersion: state.contextVersion,
4013
4328
  config: config.approval ?? { mode: "builtin" }
4014
4329
  },
4330
+ approvalRequest: {
4331
+ guidance: "Present each label with summary (e.g. `A: <summary>`) before asking for approval.",
4332
+ options: state.actionOptions.map((o) => ({
4333
+ label: o.label,
4334
+ summary: o.summary,
4335
+ approvalPrompt: o.approvalPrompt,
4336
+ requiresUserCheck: !!o.action.requiresUserCheck,
4337
+ operationType: o.action.operationType
4338
+ }))
4339
+ },
4015
4340
  prPolicy: {
4016
4341
  screenshots: {
4017
4342
  upload: config.pr?.screenshots?.upload ?? false
@@ -4217,7 +4542,7 @@ async function runContext(featureName, options) {
4217
4542
  console.log();
4218
4543
  return;
4219
4544
  }
4220
- const actionOptions = toActionOptions(f.actions);
4545
+ const actionOptions = state.actionOptions;
4221
4546
  console.log(chalk6.green(chalk6.bold("\u{1F449} Next Options (Atomic):")));
4222
4547
  let hasDocsCommand = false;
4223
4548
  actionOptions.forEach(({ label, action }) => {
@@ -4235,6 +4560,7 @@ async function runContext(featureName, options) {
4235
4560
  }
4236
4561
  if (hasCheckAction) {
4237
4562
  console.log(chalk6.gray(` \u21B3 ${tr(lang, "cli", "context.actionOptionHint")}`));
4563
+ console.log(chalk6.gray(` \u21B3 ${tr(lang, "cli", "context.actionExplainHint")}`));
4238
4564
  }
4239
4565
  console.log();
4240
4566
  }
@@ -4302,7 +4628,7 @@ async function runApprovedOption(state, config, lang, featureName, selectionOpti
4302
4628
  }
4303
4629
  console.log();
4304
4630
  console.log(chalk6.green(`\u2705 Approved option: ${parsedLabel}`));
4305
- console.log(chalk6.gray(` - Action: ${formatActionSummary(selectedAction)}`));
4631
+ console.log(chalk6.gray(` - Action: ${formatActionSummary2(selectedAction)}`));
4306
4632
  if (selectedAction.type === "command") {
4307
4633
  console.log(chalk6.gray(" - Run with: --execute"));
4308
4634
  } else {
@@ -4559,8 +4885,7 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
4559
4885
  const files = [
4560
4886
  "spec.md",
4561
4887
  "plan.md",
4562
- "tasks.md",
4563
- "decisions.md"
4888
+ "tasks.md"
4564
4889
  ];
4565
4890
  for (const file of files) {
4566
4891
  const fullPath = path13.join(f.path, file);
@@ -4629,7 +4954,7 @@ async function checkDocsStructure(config, cwd) {
4629
4954
  }
4630
4955
  return issues;
4631
4956
  }
4632
- async function checkFeatures(config, cwd, features) {
4957
+ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
4633
4958
  const issues = [];
4634
4959
  if (features.length === 0) {
4635
4960
  issues.push({
@@ -4647,7 +4972,7 @@ async function checkFeatures(config, cwd, features) {
4647
4972
  idMap.get(id).push(rel);
4648
4973
  const isInitialTemplateState = f.docs.specExists && f.docs.planExists && f.docs.tasksExists && !f.specStatus && !f.planStatus && f.tasks.total === 0 && (!f.docs.tasksDocStatusFieldExists || !f.tasksDocStatus || f.tasksDocStatus === "Draft");
4649
4974
  if (!isInitialTemplateState) {
4650
- const featureDocs = ["spec.md", "plan.md", "tasks.md", "decisions.md"];
4975
+ const featureDocs = ["spec.md", "plan.md", "tasks.md"];
4651
4976
  for (const file of featureDocs) {
4652
4977
  const p = path13.join(f.path, file);
4653
4978
  if (!await fs2.pathExists(p)) continue;
@@ -4663,6 +4988,23 @@ async function checkFeatures(config, cwd, features) {
4663
4988
  path: formatPath(cwd, p)
4664
4989
  });
4665
4990
  }
4991
+ if (decisionsPlaceholderMode !== "off") {
4992
+ const decisionsPath = path13.join(f.path, "decisions.md");
4993
+ if (await fs2.pathExists(decisionsPath)) {
4994
+ const content = await fs2.readFile(decisionsPath, "utf-8");
4995
+ const placeholders = detectPlaceholders(content);
4996
+ if (placeholders.length > 0) {
4997
+ issues.push({
4998
+ level: decisionsPlaceholderMode,
4999
+ code: "placeholder_left_decisions",
5000
+ message: tr(config.lang, "cli", "doctor.issue.placeholdersLeft", {
5001
+ placeholders: placeholders.join(", ")
5002
+ }),
5003
+ path: formatPath(cwd, decisionsPath)
5004
+ });
5005
+ }
5006
+ }
5007
+ }
4666
5008
  }
4667
5009
  if (!f.docs.specExists) {
4668
5010
  issues.push({
@@ -4741,7 +5083,11 @@ function hasFixableIssues(issues) {
4741
5083
  return issues.some((issue) => FIXABLE_ISSUE_CODES.has(issue.code));
4742
5084
  }
4743
5085
  function doctorCommand(program2) {
4744
- program2.command("doctor").description("Validate docs structure and feature metadata").option("--json", "Output in JSON format for agents").option("--fix", "Automatically apply safe fixes for common docs issues").option("--dry-run", "Show potential fixes without writing files (requires --fix)").option("-s, --strict", "Exit with non-zero code when issues are found").action(async (options) => {
5086
+ program2.command("doctor").description("Validate docs structure and feature metadata").option("--json", "Output in JSON format for agents").option("--fix", "Automatically apply safe fixes for common docs issues").option("--dry-run", "Show potential fixes without writing files (requires --fix)").option(
5087
+ "--decisions-placeholders <mode>",
5088
+ "decisions.md placeholder severity: off | info | warn (default: info)",
5089
+ "info"
5090
+ ).option("-s, --strict", "Exit with non-zero code when issues are found").action(async (options) => {
4745
5091
  try {
4746
5092
  const cwd = process.cwd();
4747
5093
  const config = await getConfig(cwd);
@@ -4757,6 +5103,14 @@ function doctorCommand(program2) {
4757
5103
  "`--dry-run` requires `--fix`."
4758
5104
  );
4759
5105
  }
5106
+ const rawDecisionsMode = (options.decisionsPlaceholders || "info").trim().toLowerCase();
5107
+ if (rawDecisionsMode !== "off" && rawDecisionsMode !== "info" && rawDecisionsMode !== "warn") {
5108
+ throw createCliError(
5109
+ "INVALID_ARGUMENT",
5110
+ "`--decisions-placeholders` must be one of: off, info, warn."
5111
+ );
5112
+ }
5113
+ const decisionsPlaceholderMode = rawDecisionsMode;
4760
5114
  const { docsDir, projectType, lang } = config;
4761
5115
  let scan = await scanFeatures(config);
4762
5116
  let features = scan.features;
@@ -4764,7 +5118,14 @@ function doctorCommand(program2) {
4764
5118
  let warnings = scan.warnings;
4765
5119
  let issues = [];
4766
5120
  issues.push(...await checkDocsStructure({ docsDir, projectType, lang }, cwd));
4767
- issues.push(...await checkFeatures({ docsDir, projectType, lang }, cwd, features));
5121
+ issues.push(
5122
+ ...await checkFeatures(
5123
+ { docsDir, projectType, lang },
5124
+ cwd,
5125
+ features,
5126
+ decisionsPlaceholderMode
5127
+ )
5128
+ );
4768
5129
  let fixResult = null;
4769
5130
  if (options.fix) {
4770
5131
  if (hasFixableIssues(issues)) {
@@ -4789,25 +5150,35 @@ function doctorCommand(program2) {
4789
5150
  warnings = scan.warnings;
4790
5151
  issues = [];
4791
5152
  issues.push(...await checkDocsStructure({ docsDir, projectType, lang }, cwd));
4792
- issues.push(...await checkFeatures({ docsDir, projectType, lang }, cwd, features));
5153
+ issues.push(
5154
+ ...await checkFeatures(
5155
+ { docsDir, projectType, lang },
5156
+ cwd,
5157
+ features,
5158
+ decisionsPlaceholderMode
5159
+ )
5160
+ );
4793
5161
  }
4794
5162
  }
4795
- const hasIssues = issues.length > 0;
5163
+ const nonInfoIssues = issues.filter((i) => i.level !== "info");
5164
+ const hasIssues = nonInfoIssues.length > 0;
4796
5165
  const hasErrors = issues.some((i) => i.level === "error");
5166
+ const hasWarns = issues.some((i) => i.level === "warn");
4797
5167
  const exitCode = options.strict && hasIssues ? 1 : 0;
4798
5168
  if (options.json) {
4799
5169
  console.log(
4800
5170
  JSON.stringify(
4801
5171
  {
4802
- status: hasErrors ? "error" : hasIssues ? "warn" : "ok",
5172
+ status: hasErrors ? "error" : hasWarns ? "warn" : "ok",
4803
5173
  meta: { docsDir, projectType, lang },
4804
5174
  branches,
4805
5175
  warnings,
4806
5176
  counts: {
4807
5177
  features: features.length,
4808
- issues: issues.length,
5178
+ issues: nonInfoIssues.length,
4809
5179
  errors: issues.filter((i) => i.level === "error").length,
4810
- warnings: issues.filter((i) => i.level === "warn").length
5180
+ warnings: issues.filter((i) => i.level === "warn").length,
5181
+ infos: issues.filter((i) => i.level === "info").length
4811
5182
  },
4812
5183
  fixes: fixResult ? {
4813
5184
  enabled: fixResult.enabled,
@@ -4844,13 +5215,18 @@ function doctorCommand(program2) {
4844
5215
  }
4845
5216
  console.log();
4846
5217
  }
4847
- if (!hasIssues) {
5218
+ const errors = issues.filter((i) => i.level === "error");
5219
+ const warns = issues.filter((i) => i.level === "warn");
5220
+ const infos = issues.filter((i) => i.level === "info");
5221
+ if (!hasIssues && infos.length === 0) {
4848
5222
  console.log(chalk6.green(tr(lang, "cli", "doctor.noIssues")));
4849
5223
  console.log();
4850
5224
  process.exit(0);
4851
5225
  }
4852
- const errors = issues.filter((i) => i.level === "error");
4853
- const warns = issues.filter((i) => i.level === "warn");
5226
+ if (!hasIssues && infos.length > 0) {
5227
+ console.log(chalk6.green(tr(lang, "cli", "doctor.noIssues")));
5228
+ console.log();
5229
+ }
4854
5230
  if (errors.length > 0) {
4855
5231
  console.log(
4856
5232
  chalk6.red(
@@ -4875,6 +5251,13 @@ function doctorCommand(program2) {
4875
5251
  );
4876
5252
  console.log();
4877
5253
  }
5254
+ if (infos.length > 0) {
5255
+ console.log(chalk6.blue(`\u2139\uFE0F Info (${infos.length})`));
5256
+ infos.forEach(
5257
+ (i) => console.log(chalk6.blue(` - ${i.message}${i.path ? ` (${i.path})` : ""}`))
5258
+ );
5259
+ console.log();
5260
+ }
4878
5261
  console.log(
4879
5262
  chalk6.gray(
4880
5263
  tr(lang, "cli", "doctor.tipJson", {
@@ -4913,182 +5296,13 @@ function doctorCommand(program2) {
4913
5296
  }
4914
5297
  });
4915
5298
  }
4916
- function resolveComponentOption(options) {
4917
- const component = (options.component || options.repo || "").trim().toLowerCase();
4918
- return component || void 0;
4919
- }
4920
- function getActionLabel2(index) {
4921
- let n = index + 1;
4922
- let label = "";
4923
- while (n > 0) {
4924
- const rem = (n - 1) % 26;
4925
- label = String.fromCharCode(65 + rem) + label;
4926
- n = Math.floor((n - 1) / 26);
4927
- }
4928
- return label;
4929
- }
4930
- function toActionOptions2(actions) {
4931
- return actions.map((action, index) => ({
4932
- label: getActionLabel2(index),
4933
- action
4934
- }));
4935
- }
4936
- function buildActionSnapshot2(actionOptions) {
4937
- return actionOptions.map(({ label, action }) => {
4938
- if (action.type === "command") {
4939
- return {
4940
- label,
4941
- type: action.type,
4942
- scope: action.scope,
4943
- cwd: action.cwd,
4944
- cmd: action.cmd,
4945
- category: action.category,
4946
- requiresUserCheck: !!action.requiresUserCheck
4947
- };
4948
- }
4949
- return {
4950
- label,
4951
- type: action.type,
4952
- message: action.message,
4953
- category: action.category,
4954
- requiresUserCheck: !!action.requiresUserCheck
4955
- };
4956
- });
4957
- }
4958
- function getContextVersion2(feature, actionOptions) {
4959
- if (!feature) return null;
4960
- const payload = JSON.stringify({
4961
- id: feature.id || "",
4962
- folderName: feature.folderName,
4963
- currentStep: feature.currentStep,
4964
- actionSnapshot: buildActionSnapshot2(actionOptions)
4965
- });
4966
- return createHash("sha256").update(payload).digest("hex").slice(0, 12);
4967
- }
4968
- function matchesFeatureSelector2(f, selector) {
4969
- const s = selector.trim();
4970
- if (!s) return false;
4971
- if (f.folderName.toLowerCase() === s.toLowerCase()) return true;
4972
- if (f.slug.toLowerCase() === s.toLowerCase()) return true;
4973
- if (f.id && f.id.toLowerCase() === s.toLowerCase()) return true;
4974
- return false;
4975
- }
4976
- function detectFromBranch2(branchName, features) {
4977
- const match = branchName.match(/^feat\/\d+-(.+)$/);
4978
- if (!match) return [];
4979
- const detected = match[1];
4980
- return features.filter(
4981
- (f) => f.slug.toLowerCase() === detected.toLowerCase() || f.folderName.toLowerCase() === detected.toLowerCase()
4982
- );
4983
- }
4984
- function toSelectionStatus2(features, selectionMode, openFeatures, targetFeatures) {
4985
- const isNoOpen = selectionMode === "open" && features.length > 0 && openFeatures.length === 0;
4986
- if (features.length === 0) return "no_features";
4987
- if (isNoOpen) return "no_open";
4988
- if (targetFeatures.length === 1) return "single_matched";
4989
- if (targetFeatures.length > 1) return "multiple_active";
4990
- return "no_match";
4991
- }
4992
- function toReasonCode2(status) {
4993
- if (status === "no_features") return "NO_FEATURES";
4994
- if (status === "no_open") return "NO_OPEN_FEATURES";
4995
- if (status === "single_matched") return "SINGLE_MATCHED";
4996
- if (status === "multiple_active") return "MULTIPLE_ACTIVE_FEATURES";
4997
- return "NO_MATCHED_FEATURES";
4998
- }
4999
- async function resolveContextSelection(config, featureName, options) {
5000
- const { features, branches, warnings } = await scanFeatures(config);
5001
- const selectedComponent = resolveComponentOption(options);
5002
- const scopedFeatures = selectedComponent ? features.filter((f) => f.type === selectedComponent) : features;
5003
- const doneFeatures = scopedFeatures.filter((f) => f.completion.workflowDone);
5004
- const openFeatures = scopedFeatures.filter((f) => !f.completion.workflowDone);
5005
- const inProgressFeatures = openFeatures.filter(
5006
- (f) => !f.completion.implementationDone
5007
- );
5008
- const readyToCloseFeatures = openFeatures.filter(
5009
- (f) => f.completion.implementationDone
5010
- );
5011
- let targetFeatures = [];
5012
- let selectionMode = "explicit";
5013
- if (featureName) {
5014
- targetFeatures = scopedFeatures.filter(
5015
- (f) => matchesFeatureSelector2(f, featureName)
5016
- );
5017
- selectionMode = "explicit";
5018
- } else {
5019
- if (config.projectType === "single") {
5020
- const branchName = branches.project.single || "";
5021
- targetFeatures = detectFromBranch2(branchName, scopedFeatures);
5022
- } else if (selectedComponent) {
5023
- const branchName = branches.project[selectedComponent] || "";
5024
- targetFeatures = detectFromBranch2(
5025
- branchName,
5026
- scopedFeatures
5027
- );
5028
- } else {
5029
- const matches = [];
5030
- const componentKeys = [...new Set(scopedFeatures.map((f) => f.type))].filter((key) => key !== "single");
5031
- for (const component of componentKeys) {
5032
- const branchName = branches.project[component] || "";
5033
- if (!branchName) continue;
5034
- matches.push(
5035
- ...detectFromBranch2(
5036
- branchName,
5037
- scopedFeatures.filter((f) => f.type === component)
5038
- )
5039
- );
5040
- }
5041
- targetFeatures = matches;
5042
- }
5043
- if (targetFeatures.length > 0) {
5044
- selectionMode = "branch";
5045
- } else if (options.all) {
5046
- targetFeatures = scopedFeatures;
5047
- selectionMode = "all";
5048
- } else if (options.done) {
5049
- targetFeatures = doneFeatures;
5050
- selectionMode = "done";
5051
- } else {
5052
- targetFeatures = openFeatures;
5053
- selectionMode = "open";
5054
- }
5055
- }
5056
- const status = toSelectionStatus2(
5057
- scopedFeatures,
5058
- selectionMode,
5059
- openFeatures,
5060
- targetFeatures
5061
- );
5062
- const matchedFeature = targetFeatures.length === 1 ? targetFeatures[0] : null;
5063
- const actions = matchedFeature?.actions ?? [];
5064
- const actionOptions = toActionOptions2(actions);
5065
- const contextVersion = getContextVersion2(matchedFeature, actionOptions);
5066
- return {
5067
- features: scopedFeatures,
5068
- branches,
5069
- warnings,
5070
- doneFeatures,
5071
- openFeatures,
5072
- inProgressFeatures,
5073
- readyToCloseFeatures,
5074
- selectionMode,
5075
- targetFeatures,
5076
- status,
5077
- matchedFeature,
5078
- actions,
5079
- actionOptions,
5080
- contextVersion
5081
- };
5082
- }
5083
-
5084
- // src/commands/view.ts
5085
- function resolveComponentOption2(options) {
5086
- if (options.repo && options.component && options.repo.trim().toLowerCase() !== options.component.trim().toLowerCase()) {
5087
- throw createCliError(
5088
- "INVALID_ARGUMENT",
5089
- "`--repo` and `--component` must reference the same value when both are provided."
5090
- );
5091
- }
5299
+ function resolveComponentOption2(options) {
5300
+ if (options.repo && options.component && options.repo.trim().toLowerCase() !== options.component.trim().toLowerCase()) {
5301
+ throw createCliError(
5302
+ "INVALID_ARGUMENT",
5303
+ "`--repo` and `--component` must reference the same value when both are provided."
5304
+ );
5305
+ }
5092
5306
  const component = (options.component || options.repo || "").trim().toLowerCase();
5093
5307
  return component || void 0;
5094
5308
  }
@@ -5139,8 +5353,9 @@ async function runView(featureName, options) {
5139
5353
  if (options.json) {
5140
5354
  const payload = {
5141
5355
  status: state.status,
5142
- reasonCode: toReasonCode2(state.status),
5356
+ reasonCode: toReasonCode(state.status),
5143
5357
  selectionMode: state.selectionMode,
5358
+ selectionFallback: state.selectionFallback,
5144
5359
  counts: {
5145
5360
  features: state.features.length,
5146
5361
  open: state.openFeatures.length,
@@ -5187,7 +5402,7 @@ async function runView(featureName, options) {
5187
5402
  }
5188
5403
  if (!state.matchedFeature) {
5189
5404
  console.log();
5190
- console.log(chalk6.blue(`Selection: ${state.status} (${toReasonCode2(state.status)})`));
5405
+ console.log(chalk6.blue(`Selection: ${state.status} (${toReasonCode(state.status)})`));
5191
5406
  const rows = state.targetFeatures.length > 0 ? state.targetFeatures : state.features;
5192
5407
  for (const f2 of rows) {
5193
5408
  const statusText = f2.completion.workflowDone ? chalk6.green("WORKFLOW_DONE") : f2.completion.implementationDone ? chalk6.cyan("DONE") : chalk6.yellow("IN_PROGRESS");
@@ -5375,16 +5590,18 @@ async function runFlow(featureName, options) {
5375
5590
  context: {
5376
5591
  before: {
5377
5592
  status: before.status,
5378
- reasonCode: toReasonCode2(before.status),
5593
+ reasonCode: toReasonCode(before.status),
5379
5594
  selectionMode: before.selectionMode,
5595
+ selectionFallback: before.selectionFallback,
5380
5596
  matchedFeature: before.matchedFeature,
5381
5597
  actionOptions: before.actionOptions,
5382
5598
  contextVersion: before.contextVersion
5383
5599
  },
5384
5600
  after: {
5385
5601
  status: after.status,
5386
- reasonCode: toReasonCode2(after.status),
5602
+ reasonCode: toReasonCode(after.status),
5387
5603
  selectionMode: after.selectionMode,
5604
+ selectionFallback: after.selectionFallback,
5388
5605
  matchedFeature: after.matchedFeature,
5389
5606
  actionOptions: after.actionOptions,
5390
5607
  contextVersion: after.contextVersion
@@ -5403,7 +5620,7 @@ async function runFlow(featureName, options) {
5403
5620
  console.log(chalk6.bold("\u{1F501} Flow Summary"));
5404
5621
  console.log(
5405
5622
  chalk6.gray(
5406
- `- Before: ${before.status} (${toReasonCode2(before.status)}) / After: ${after.status} (${toReasonCode2(after.status)})`
5623
+ `- Before: ${before.status} (${toReasonCode(before.status)}) / After: ${after.status} (${toReasonCode(after.status)})`
5407
5624
  )
5408
5625
  );
5409
5626
  if (approvalResult && typeof approvalResult === "object") {
@@ -5439,6 +5656,626 @@ async function runFlow(featureName, options) {
5439
5656
  console.log(chalk6.gray("Tip: add --approve <LABEL> [--execute] to run the selected atomic action."));
5440
5657
  console.log();
5441
5658
  }
5659
+ function resolveComponentOption4(options) {
5660
+ if (options.repo && options.component && options.repo.trim().toLowerCase() !== options.component.trim().toLowerCase()) {
5661
+ throw createCliError(
5662
+ "INVALID_ARGUMENT",
5663
+ "`--repo` and `--component` must reference the same value when both are provided."
5664
+ );
5665
+ }
5666
+ const component = (options.component || options.repo || "").trim().toLowerCase();
5667
+ return component || void 0;
5668
+ }
5669
+ function parseLabels(raw) {
5670
+ const labels = (raw || "enhancement").split(",").map((part) => part.trim()).filter(Boolean);
5671
+ if (labels.length === 0) {
5672
+ throw createCliError(
5673
+ "INVALID_ARGUMENT",
5674
+ "At least one label is required. Use `--labels enhancement`."
5675
+ );
5676
+ }
5677
+ return [...new Set(labels)];
5678
+ }
5679
+ function runProcess(bin, args, cwd) {
5680
+ const result = spawnSync(bin, args, {
5681
+ cwd,
5682
+ encoding: "utf-8",
5683
+ env: {
5684
+ ...process.env,
5685
+ LEE_SPEC_KIT_NO_UPDATE_CHECK: "1",
5686
+ LEE_SPEC_KIT_NO_BANNER: "1"
5687
+ }
5688
+ });
5689
+ return {
5690
+ code: result.status ?? 1,
5691
+ stdout: result.stdout || "",
5692
+ stderr: result.stderr || ""
5693
+ };
5694
+ }
5695
+ function runProcessOrThrow(bin, args, cwd, failureMessage) {
5696
+ const result = runProcess(bin, args, cwd);
5697
+ if (result.code !== 0) {
5698
+ const detail = (result.stderr || result.stdout || "").trim();
5699
+ throw createCliError(
5700
+ "EXECUTION_FAILED",
5701
+ `${failureMessage}${detail ? `: ${detail}` : ""}`
5702
+ );
5703
+ }
5704
+ return result;
5705
+ }
5706
+ function runGhJson(args, cwd) {
5707
+ const result = runProcessOrThrow("gh", args, cwd, "GitHub CLI command failed");
5708
+ const text = result.stdout.trim();
5709
+ if (!text) {
5710
+ throw createCliError("EXECUTION_FAILED", "GitHub CLI returned empty JSON output.");
5711
+ }
5712
+ try {
5713
+ return JSON.parse(text);
5714
+ } catch {
5715
+ throw createCliError(
5716
+ "EXECUTION_FAILED",
5717
+ `GitHub CLI returned invalid JSON: ${text.slice(0, 160)}`
5718
+ );
5719
+ }
5720
+ }
5721
+ function ensureSections(body, sections, kind) {
5722
+ const missing = sections.filter((section) => {
5723
+ const re = new RegExp(`^##\\s+${section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
5724
+ return !re.test(body);
5725
+ });
5726
+ if (missing.length > 0) {
5727
+ throw createCliError(
5728
+ "PRECONDITION_FAILED",
5729
+ `${kind} body is missing required sections: ${missing.join(", ")}`
5730
+ );
5731
+ }
5732
+ }
5733
+ function ensureDocsExist(docsDir, relativePaths) {
5734
+ const missing = relativePaths.filter(
5735
+ (relativePath) => !fs2.existsSync(path13.join(docsDir, relativePath))
5736
+ );
5737
+ if (missing.length > 0) {
5738
+ throw createCliError(
5739
+ "PRECONDITION_FAILED",
5740
+ `Related document paths do not exist: ${missing.join(", ")}`
5741
+ );
5742
+ }
5743
+ }
5744
+ function toBodyFilePath(raw, fallbackName) {
5745
+ const selected = raw?.trim() || path13.join(os.tmpdir(), fallbackName);
5746
+ return path13.resolve(selected);
5747
+ }
5748
+ async function resolveFeatureOrThrow(featureName, options) {
5749
+ const config = await getConfig(process.cwd());
5750
+ if (!config) {
5751
+ throw createCliError(
5752
+ "CONFIG_NOT_FOUND",
5753
+ tr(DEFAULT_LANG, "cli", "common.configNotFound")
5754
+ );
5755
+ }
5756
+ const state = await resolveContextSelection(config, featureName, options);
5757
+ if (!state.matchedFeature) {
5758
+ if (state.status === "no_features") {
5759
+ throw createCliError("PRECONDITION_FAILED", "No features found.");
5760
+ }
5761
+ if (state.status === "multiple_active") {
5762
+ throw createCliError(
5763
+ "CONTEXT_SELECTION_REQUIRED",
5764
+ "Multiple features matched. Specify feature name (slug | F001 | F001-slug)."
5765
+ );
5766
+ }
5767
+ throw createCliError(
5768
+ "CONTEXT_SELECTION_REQUIRED",
5769
+ "Failed to auto-select a feature. Specify feature name explicitly."
5770
+ );
5771
+ }
5772
+ return { config, feature: state.matchedFeature };
5773
+ }
5774
+ function getFeatureDocPaths(feature) {
5775
+ const featurePathFromDocs = feature.docs.featurePathFromDocs;
5776
+ return {
5777
+ featurePathFromDocs,
5778
+ specPath: `${featurePathFromDocs}/spec.md`,
5779
+ planPath: `${featurePathFromDocs}/plan.md`,
5780
+ tasksPath: `${featurePathFromDocs}/tasks.md`
5781
+ };
5782
+ }
5783
+ function buildIssueBody(feature, labels, paths) {
5784
+ return `## Overview
5785
+
5786
+ Implement feature \`${feature.folderName}\`.
5787
+
5788
+ ## Goals
5789
+
5790
+ - Finalize feature scope and implementation outcome
5791
+ - Keep spec/plan/tasks aligned with delivery
5792
+
5793
+ ## Completion Criteria
5794
+
5795
+ - [ ] Scope and approach are documented clearly
5796
+ - [ ] Tasks are complete and verifiable
5797
+ - [ ] Related docs are synchronized
5798
+
5799
+ ## Related Documents
5800
+
5801
+ - **Spec**: \`${paths.specPath}\`
5802
+ - **Plan**: \`${paths.planPath}\`
5803
+ - **Tasks**: \`${paths.tasksPath}\`
5804
+
5805
+ ## Labels
5806
+
5807
+ ${labels.map((label) => `- \`${label}\``).join("\n")}
5808
+ `;
5809
+ }
5810
+ function buildPrBody(feature, paths) {
5811
+ const closes = feature.issueNumber ? `
5812
+ Closes #${feature.issueNumber}
5813
+ ` : "\n";
5814
+ return `## Overview
5815
+
5816
+ Implement and document feature \`${feature.folderName}\`.
5817
+
5818
+ ## Changes
5819
+
5820
+ - Deliver implementation for the feature scope
5821
+ - Update docs to match implementation and workflow state
5822
+ - Keep PR metadata synchronized in tasks.md
5823
+
5824
+ ## Tests
5825
+
5826
+ ### Tests Run
5827
+
5828
+ - [x] \`<test command>\` \u2014 PASS
5829
+
5830
+ ## Related Documents
5831
+
5832
+ - **Spec**: \`${paths.specPath}\`
5833
+ - **Tasks**: \`${paths.tasksPath}\`${closes}`;
5834
+ }
5835
+ function replaceListField(content, keys, value) {
5836
+ for (const key of keys) {
5837
+ const re = new RegExp(
5838
+ `^(\\s*-\\s*\\*\\*${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\*\\*\\s*:\\s*).*$`,
5839
+ "m"
5840
+ );
5841
+ if (!re.test(content)) continue;
5842
+ const next = content.replace(re, `$1${value}`);
5843
+ return { content: next, changed: next !== content, found: true };
5844
+ }
5845
+ return { content, changed: false, found: false };
5846
+ }
5847
+ function insertFieldInGithubIssueSection(content, key, value) {
5848
+ const lines = content.split("\n");
5849
+ const headingIndex = lines.findIndex(
5850
+ (line) => /^\s*##\s+(GitHub Issue|로컬 추적 정보|Local Tracking)\s*$/.test(line)
5851
+ );
5852
+ if (headingIndex < 0) return { content, changed: false };
5853
+ let end = lines.length;
5854
+ for (let i = headingIndex + 1; i < lines.length; i++) {
5855
+ if (/^\s*##\s+/.test(lines[i])) {
5856
+ end = i;
5857
+ break;
5858
+ }
5859
+ }
5860
+ lines.splice(end, 0, `- **${key}**: ${value}`);
5861
+ return { content: lines.join("\n"), changed: true };
5862
+ }
5863
+ function syncTasksPrMetadata(tasksPath, prUrl, nextStatus) {
5864
+ if (!fs2.existsSync(tasksPath)) {
5865
+ throw createCliError("DOCS_NOT_FOUND", `tasks.md not found: ${tasksPath}`);
5866
+ }
5867
+ const original = fs2.readFileSync(tasksPath, "utf-8");
5868
+ let next = original;
5869
+ let changed = false;
5870
+ const prReplaced = replaceListField(next, ["PR", "Pull Request"], prUrl);
5871
+ next = prReplaced.content;
5872
+ changed = changed || prReplaced.changed;
5873
+ if (!prReplaced.found) {
5874
+ const inserted = insertFieldInGithubIssueSection(next, "PR", prUrl);
5875
+ next = inserted.content;
5876
+ changed = changed || inserted.changed;
5877
+ }
5878
+ const statusReplaced = replaceListField(
5879
+ next,
5880
+ ["PR Status", "PR \uC0C1\uD0DC"],
5881
+ nextStatus
5882
+ );
5883
+ next = statusReplaced.content;
5884
+ changed = changed || statusReplaced.changed;
5885
+ if (!statusReplaced.found) {
5886
+ const inserted = insertFieldInGithubIssueSection(next, "PR Status", nextStatus);
5887
+ next = inserted.content;
5888
+ changed = changed || inserted.changed;
5889
+ }
5890
+ if (changed) {
5891
+ fs2.writeFileSync(tasksPath, next, "utf-8");
5892
+ }
5893
+ return { changed, path: tasksPath };
5894
+ }
5895
+ function gitCurrentBranch(cwd) {
5896
+ const result = runProcessOrThrow(
5897
+ "git",
5898
+ ["rev-parse", "--abbrev-ref", "HEAD"],
5899
+ cwd,
5900
+ "Failed to detect current git branch"
5901
+ );
5902
+ return result.stdout.trim();
5903
+ }
5904
+ function ensureCleanWorktree(cwd) {
5905
+ const result = runProcessOrThrow(
5906
+ "git",
5907
+ ["status", "--porcelain=v1"],
5908
+ cwd,
5909
+ "Failed to inspect git worktree"
5910
+ );
5911
+ if (result.stdout.trim().length > 0) {
5912
+ throw createCliError(
5913
+ "PRECONDITION_FAILED",
5914
+ "Git worktree is not clean. Commit or stash changes before merge retry sync."
5915
+ );
5916
+ }
5917
+ }
5918
+ function commitAndPushPath(cwd, absPath, message) {
5919
+ const relativePath = path13.relative(cwd, absPath) || absPath;
5920
+ const status = runProcessOrThrow(
5921
+ "git",
5922
+ ["status", "--porcelain=v1", "--", relativePath],
5923
+ cwd,
5924
+ "Failed to inspect git file status"
5925
+ );
5926
+ if (status.stdout.trim().length === 0) return;
5927
+ runProcessOrThrow("git", ["add", "--", relativePath], cwd, "Failed to stage file");
5928
+ runProcessOrThrow("git", ["commit", "-m", message], cwd, "Failed to commit synced metadata");
5929
+ const branch = gitCurrentBranch(cwd);
5930
+ runProcessOrThrow(
5931
+ "git",
5932
+ ["push", "-u", "origin", branch],
5933
+ cwd,
5934
+ "Failed to push synced metadata commit"
5935
+ );
5936
+ }
5937
+ function shouldRefreshHeadBranch(stderr, stdout) {
5938
+ const text = `${stderr}
5939
+ ${stdout}`;
5940
+ return /out of date|not possible to fast-forward|must be up to date|not up to date/i.test(
5941
+ text
5942
+ );
5943
+ }
5944
+ function refreshPrHeadBranch(prRef, cwd) {
5945
+ ensureCleanWorktree(cwd);
5946
+ const meta = runGhJson(
5947
+ ["pr", "view", prRef, "--json", "url,headRefName,baseRefName"],
5948
+ cwd
5949
+ );
5950
+ const originalBranch = gitCurrentBranch(cwd);
5951
+ runProcessOrThrow(
5952
+ "git",
5953
+ ["fetch", "origin", meta.baseRefName, meta.headRefName],
5954
+ cwd,
5955
+ "Failed to fetch PR branches"
5956
+ );
5957
+ const hasLocalHead = runProcess(
5958
+ "git",
5959
+ ["show-ref", "--verify", "--quiet", `refs/heads/${meta.headRefName}`],
5960
+ cwd
5961
+ ).code === 0;
5962
+ if (hasLocalHead) {
5963
+ runProcessOrThrow(
5964
+ "git",
5965
+ ["checkout", meta.headRefName],
5966
+ cwd,
5967
+ "Failed to checkout PR head branch"
5968
+ );
5969
+ } else {
5970
+ runProcessOrThrow(
5971
+ "git",
5972
+ ["checkout", "-B", meta.headRefName, `origin/${meta.headRefName}`],
5973
+ cwd,
5974
+ "Failed to create local PR head branch"
5975
+ );
5976
+ }
5977
+ runProcessOrThrow(
5978
+ "git",
5979
+ ["rebase", `origin/${meta.baseRefName}`],
5980
+ cwd,
5981
+ "Failed to rebase PR head branch"
5982
+ );
5983
+ runProcessOrThrow(
5984
+ "git",
5985
+ ["push", "--force-with-lease", "origin", meta.headRefName],
5986
+ cwd,
5987
+ "Failed to push rebased PR head branch"
5988
+ );
5989
+ if (originalBranch !== meta.headRefName) {
5990
+ runProcessOrThrow(
5991
+ "git",
5992
+ ["checkout", originalBranch],
5993
+ cwd,
5994
+ "Failed to restore previous branch after PR refresh"
5995
+ );
5996
+ }
5997
+ }
5998
+ function mergePrWithRetry(prRef, cwd, retryCount) {
5999
+ const attempts = Number.isFinite(retryCount) ? Math.max(1, retryCount) : 3;
6000
+ let lastError = "";
6001
+ for (let attempt = 1; attempt <= attempts; attempt++) {
6002
+ const merged = runProcess(
6003
+ "gh",
6004
+ ["pr", "merge", prRef, "--squash", "--delete-branch"],
6005
+ cwd
6006
+ );
6007
+ if (merged.code === 0) {
6008
+ return { merged: true, attempts: attempt };
6009
+ }
6010
+ lastError = (merged.stderr || merged.stdout || "").trim();
6011
+ if (shouldRefreshHeadBranch(merged.stderr, merged.stdout)) {
6012
+ refreshPrHeadBranch(prRef, cwd);
6013
+ continue;
6014
+ }
6015
+ }
6016
+ throw createCliError(
6017
+ "EXECUTION_FAILED",
6018
+ `Failed to merge PR after retry attempts.${lastError ? ` Last error: ${lastError}` : ""}`
6019
+ );
6020
+ }
6021
+ function toRetryCount(raw) {
6022
+ if (!raw) return 3;
6023
+ const parsed = Number.parseInt(raw, 10);
6024
+ if (!Number.isFinite(parsed) || parsed <= 0) {
6025
+ throw createCliError("INVALID_ARGUMENT", "`--retry` must be a positive integer.");
6026
+ }
6027
+ return parsed;
6028
+ }
6029
+ function githubCommand(program2) {
6030
+ const github = program2.command("github").description("GitHub workflow helpers (issue/pr templates, validation, merge retry)");
6031
+ github.command("issue [feature-name]").description("Generate/create GitHub issue body from feature docs with validation").option("--json", "Output in JSON format for agents").option("--repo <repo>", "Component name for multi projects").option("--component <component>", "Component name for multi projects").option("--title <title>", "Issue title").option("--labels <labels>", "Comma-separated labels (default: enhancement)").option("--body-file <path>", "Issue body file output path").option("--assignee <assignee>", "Issue assignee (default: @me)").option("--create", "Create issue via gh CLI").action(async (featureName, options) => {
6032
+ try {
6033
+ const selectedComponent = resolveComponentOption4(options);
6034
+ const { config, feature } = await resolveFeatureOrThrow(featureName, {
6035
+ component: selectedComponent
6036
+ });
6037
+ const labels = parseLabels(options.labels);
6038
+ const paths = getFeatureDocPaths(feature);
6039
+ ensureDocsExist(config.docsDir, [paths.specPath, paths.planPath, paths.tasksPath]);
6040
+ const title = options.title?.trim() || `${feature.slug} (${feature.folderName} documentation update)`;
6041
+ const body = buildIssueBody(feature, labels, paths);
6042
+ ensureSections(body, ["Overview", "Goals", "Completion Criteria", "Related Documents", "Labels"], "Issue");
6043
+ const bodyFile = toBodyFilePath(
6044
+ options.bodyFile,
6045
+ `lee-spec-kit.issue.${feature.folderName}.md`
6046
+ );
6047
+ await fs2.ensureDir(path13.dirname(bodyFile));
6048
+ await fs2.writeFile(bodyFile, body, "utf-8");
6049
+ let issueUrl;
6050
+ if (options.create) {
6051
+ const args = [
6052
+ "issue",
6053
+ "create",
6054
+ "--title",
6055
+ title,
6056
+ "--body-file",
6057
+ bodyFile,
6058
+ "--assignee",
6059
+ options.assignee?.trim() || "@me"
6060
+ ];
6061
+ for (const label of labels) {
6062
+ args.push("--label", label);
6063
+ }
6064
+ const created = runProcessOrThrow(
6065
+ "gh",
6066
+ args,
6067
+ process.cwd(),
6068
+ "Failed to create GitHub issue"
6069
+ );
6070
+ issueUrl = created.stdout.trim() || void 0;
6071
+ }
6072
+ if (options.json) {
6073
+ console.log(
6074
+ JSON.stringify(
6075
+ {
6076
+ status: "ok",
6077
+ reasonCode: options.create ? "ISSUE_CREATED" : "ISSUE_TEMPLATE_GENERATED",
6078
+ feature: feature.folderName,
6079
+ component: feature.type,
6080
+ title,
6081
+ labels,
6082
+ bodyFile,
6083
+ issueUrl
6084
+ },
6085
+ null,
6086
+ 2
6087
+ )
6088
+ );
6089
+ return;
6090
+ }
6091
+ console.log();
6092
+ console.log(chalk6.bold("\u{1F9FE} GitHub Issue Helper"));
6093
+ console.log(chalk6.gray(`- Feature: ${feature.folderName}`));
6094
+ console.log(chalk6.gray(`- Body file: ${bodyFile}`));
6095
+ console.log(chalk6.gray(`- Labels: ${labels.join(", ")}`));
6096
+ if (issueUrl) {
6097
+ console.log(chalk6.green(`\u2705 Created: ${issueUrl}`));
6098
+ } else {
6099
+ console.log(chalk6.blue("Template generated. Add --create to open the issue automatically."));
6100
+ }
6101
+ console.log();
6102
+ } catch (error) {
6103
+ const config = await getConfig(process.cwd());
6104
+ const lang = config?.lang ?? DEFAULT_LANG;
6105
+ const cliError = toCliError(error);
6106
+ const suggestions = getCliErrorSuggestions(cliError.code, lang);
6107
+ if (options.json) {
6108
+ console.log(
6109
+ JSON.stringify({
6110
+ status: "error",
6111
+ reasonCode: cliError.code,
6112
+ error: cliError.message,
6113
+ suggestions
6114
+ })
6115
+ );
6116
+ } else {
6117
+ console.error(
6118
+ chalk6.red(tr(lang, "cli", "common.errorLabel")),
6119
+ chalk6.red(`[${cliError.code}] ${cliError.message}`)
6120
+ );
6121
+ printCliErrorSuggestions(suggestions, lang);
6122
+ }
6123
+ process.exit(1);
6124
+ }
6125
+ });
6126
+ github.command("pr [feature-name]").description(
6127
+ "Generate/create GitHub PR body with validation, tasks PR sync, and merge retry"
6128
+ ).option("--json", "Output in JSON format for agents").option("--repo <repo>", "Component name for multi projects").option("--component <component>", "Component name for multi projects").option("--title <title>", "PR title").option("--labels <labels>", "Comma-separated labels (default: enhancement)").option("--body-file <path>", "PR body file output path").option("--assignee <assignee>", "PR assignee (default: @me)").option("--base <branch>", "PR base branch (default: main)", "main").option("--create", "Create PR via gh CLI").option("--pr <ref>", "Existing PR URL/number (used by --merge)").option("--merge", "Merge PR with retry and head-branch refresh").option("--retry <count>", "Retry count for merge (default: 3)").option("--no-sync-tasks", "Do not sync PR URL/PR status into tasks.md").option("--commit-sync", "Commit and push tasks.md metadata sync automatically").action(async (featureName, options) => {
6129
+ try {
6130
+ const selectedComponent = resolveComponentOption4(options);
6131
+ const { config, feature } = await resolveFeatureOrThrow(featureName, {
6132
+ component: selectedComponent
6133
+ });
6134
+ const labels = parseLabels(options.labels);
6135
+ const paths = getFeatureDocPaths(feature);
6136
+ ensureDocsExist(config.docsDir, [paths.specPath, paths.tasksPath]);
6137
+ const defaultTitle = feature.issueNumber ? `feat(#${feature.issueNumber}): ${feature.slug} (implementation update)` : `feat: ${feature.slug} (implementation update)`;
6138
+ const title = options.title?.trim() || defaultTitle;
6139
+ const body = buildPrBody(feature, paths);
6140
+ ensureSections(body, ["Overview", "Changes", "Tests", "Related Documents"], "PR");
6141
+ const bodyFile = toBodyFilePath(
6142
+ options.bodyFile,
6143
+ `lee-spec-kit.pr.${feature.folderName}.md`
6144
+ );
6145
+ await fs2.ensureDir(path13.dirname(bodyFile));
6146
+ await fs2.writeFile(bodyFile, body, "utf-8");
6147
+ const retryCount = toRetryCount(options.retry);
6148
+ let prUrl = options.pr?.trim() || "";
6149
+ let mergedAttempts;
6150
+ let syncChanged = false;
6151
+ if (options.create) {
6152
+ const args = [
6153
+ "pr",
6154
+ "create",
6155
+ "--title",
6156
+ title,
6157
+ "--body-file",
6158
+ bodyFile,
6159
+ "--base",
6160
+ options.base || "main",
6161
+ "--assignee",
6162
+ options.assignee?.trim() || "@me"
6163
+ ];
6164
+ for (const label of labels) {
6165
+ args.push("--label", label);
6166
+ }
6167
+ const created = runProcessOrThrow(
6168
+ "gh",
6169
+ args,
6170
+ process.cwd(),
6171
+ "Failed to create GitHub PR"
6172
+ );
6173
+ prUrl = created.stdout.trim();
6174
+ }
6175
+ if (!prUrl && options.merge) {
6176
+ throw createCliError(
6177
+ "INVALID_ARGUMENT",
6178
+ "`--merge` requires `--create` or `--pr <url|number>`."
6179
+ );
6180
+ }
6181
+ if (prUrl && options.syncTasks !== false) {
6182
+ const synced = syncTasksPrMetadata(
6183
+ path13.join(config.docsDir, paths.tasksPath),
6184
+ prUrl,
6185
+ "Review"
6186
+ );
6187
+ syncChanged = synced.changed;
6188
+ const shouldCommitSync = !!options.commitSync || !!options.merge;
6189
+ if (syncChanged && shouldCommitSync) {
6190
+ const issueSuffix = feature.issueNumber ? `#${feature.issueNumber}` : feature.folderName;
6191
+ commitAndPushPath(
6192
+ process.cwd(),
6193
+ synced.path,
6194
+ `docs(${issueSuffix}): sync PR metadata for ${feature.folderName}`
6195
+ );
6196
+ }
6197
+ }
6198
+ if (options.merge) {
6199
+ const merged = mergePrWithRetry(prUrl, process.cwd(), retryCount);
6200
+ mergedAttempts = merged.attempts;
6201
+ const baseBranch = options.base || "main";
6202
+ runProcessOrThrow(
6203
+ "git",
6204
+ ["checkout", baseBranch],
6205
+ process.cwd(),
6206
+ `Failed to checkout ${baseBranch} after merge`
6207
+ );
6208
+ runProcessOrThrow(
6209
+ "git",
6210
+ ["pull", "--rebase", "origin", baseBranch],
6211
+ process.cwd(),
6212
+ `Failed to update ${baseBranch} after merge`
6213
+ );
6214
+ }
6215
+ if (options.json) {
6216
+ console.log(
6217
+ JSON.stringify(
6218
+ {
6219
+ status: "ok",
6220
+ reasonCode: options.merge ? "PR_CREATED_SYNCED_MERGED" : options.create ? "PR_CREATED_SYNCED" : "PR_TEMPLATE_GENERATED",
6221
+ feature: feature.folderName,
6222
+ component: feature.type,
6223
+ title,
6224
+ labels,
6225
+ bodyFile,
6226
+ prUrl: prUrl || void 0,
6227
+ syncChanged,
6228
+ merged: !!options.merge,
6229
+ mergeAttempts: mergedAttempts
6230
+ },
6231
+ null,
6232
+ 2
6233
+ )
6234
+ );
6235
+ return;
6236
+ }
6237
+ console.log();
6238
+ console.log(chalk6.bold("\u{1F500} GitHub PR Helper"));
6239
+ console.log(chalk6.gray(`- Feature: ${feature.folderName}`));
6240
+ console.log(chalk6.gray(`- Body file: ${bodyFile}`));
6241
+ console.log(chalk6.gray(`- Labels: ${labels.join(", ")}`));
6242
+ if (prUrl) {
6243
+ console.log(chalk6.gray(`- PR: ${prUrl}`));
6244
+ }
6245
+ if (syncChanged) {
6246
+ console.log(chalk6.green("\u2705 tasks.md PR metadata synced."));
6247
+ }
6248
+ if (options.merge) {
6249
+ console.log(chalk6.green(`\u2705 PR merged (attempts: ${mergedAttempts ?? 1}).`));
6250
+ } else if (!options.create) {
6251
+ console.log(chalk6.blue("Template generated. Add --create to open the PR automatically."));
6252
+ }
6253
+ console.log();
6254
+ } catch (error) {
6255
+ const config = await getConfig(process.cwd());
6256
+ const lang = config?.lang ?? DEFAULT_LANG;
6257
+ const cliError = toCliError(error);
6258
+ const suggestions = getCliErrorSuggestions(cliError.code, lang);
6259
+ if (options.json) {
6260
+ console.log(
6261
+ JSON.stringify({
6262
+ status: "error",
6263
+ reasonCode: cliError.code,
6264
+ error: cliError.message,
6265
+ suggestions
6266
+ })
6267
+ );
6268
+ } else {
6269
+ console.error(
6270
+ chalk6.red(tr(lang, "cli", "common.errorLabel")),
6271
+ chalk6.red(`[${cliError.code}] ${cliError.message}`)
6272
+ );
6273
+ printCliErrorSuggestions(suggestions, lang);
6274
+ }
6275
+ process.exit(1);
6276
+ }
6277
+ });
6278
+ }
5442
6279
  function isBannerDisabled() {
5443
6280
  const v = (process.env.LEE_SPEC_KIT_NO_BANNER || "").trim();
5444
6281
  return v === "1";
@@ -5607,4 +6444,5 @@ contextCommand(program);
5607
6444
  doctorCommand(program);
5608
6445
  viewCommand(program);
5609
6446
  flowCommand(program);
6447
+ githubCommand(program);
5610
6448
  await program.parseAsync();