@planu/cli 0.78.0 → 0.79.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/cli/commands/serve.d.ts.map +1 -1
  2. package/dist/cli/commands/serve.js +1 -1
  3. package/dist/cli/commands/serve.js.map +1 -1
  4. package/dist/config/contradiction-patterns.json +83 -0
  5. package/dist/config/estimation-tables.json +49 -0
  6. package/dist/config/license-plans.json +5 -1
  7. package/dist/engine/ci-generator/local-script.d.ts +8 -0
  8. package/dist/engine/ci-generator/local-script.d.ts.map +1 -0
  9. package/dist/engine/ci-generator/local-script.js +177 -0
  10. package/dist/engine/ci-generator/local-script.js.map +1 -0
  11. package/dist/engine/ci-generator/planu-steps.d.ts.map +1 -1
  12. package/dist/engine/ci-generator/planu-steps.js +37 -22
  13. package/dist/engine/ci-generator/planu-steps.js.map +1 -1
  14. package/dist/engine/config-loader.d.ts +73 -0
  15. package/dist/engine/config-loader.d.ts.map +1 -0
  16. package/dist/engine/config-loader.js +246 -0
  17. package/dist/engine/config-loader.js.map +1 -0
  18. package/dist/engine/config-schemas.d.ts +64 -0
  19. package/dist/engine/config-schemas.d.ts.map +1 -0
  20. package/dist/engine/config-schemas.js +55 -0
  21. package/dist/engine/config-schemas.js.map +1 -0
  22. package/dist/engine/spec-summary-html.d.ts.map +1 -1
  23. package/dist/engine/spec-summary-html.js +5 -0
  24. package/dist/engine/spec-summary-html.js.map +1 -1
  25. package/dist/index.js +10 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/storage/ideas-store.d.ts +15 -0
  28. package/dist/storage/ideas-store.d.ts.map +1 -0
  29. package/dist/storage/ideas-store.js +37 -0
  30. package/dist/storage/ideas-store.js.map +1 -0
  31. package/dist/tools/capture-idea-handler.d.ts +4 -0
  32. package/dist/tools/capture-idea-handler.d.ts.map +1 -0
  33. package/dist/tools/capture-idea-handler.js +39 -0
  34. package/dist/tools/capture-idea-handler.js.map +1 -0
  35. package/dist/tools/ci-planu-handler.d.ts +2 -2
  36. package/dist/tools/ci-planu-handler.d.ts.map +1 -1
  37. package/dist/tools/ci-planu-handler.js +55 -22
  38. package/dist/tools/ci-planu-handler.js.map +1 -1
  39. package/dist/tools/discard-idea-handler.d.ts +4 -0
  40. package/dist/tools/discard-idea-handler.d.ts.map +1 -0
  41. package/dist/tools/discard-idea-handler.js +27 -0
  42. package/dist/tools/discard-idea-handler.js.map +1 -0
  43. package/dist/tools/git/cleanup-ops.d.ts +15 -0
  44. package/dist/tools/git/cleanup-ops.d.ts.map +1 -0
  45. package/dist/tools/git/cleanup-ops.js +417 -0
  46. package/dist/tools/git/cleanup-ops.js.map +1 -0
  47. package/dist/tools/license-gate.d.ts +6 -0
  48. package/dist/tools/license-gate.d.ts.map +1 -1
  49. package/dist/tools/license-gate.js +14 -0
  50. package/dist/tools/license-gate.js.map +1 -1
  51. package/dist/tools/list-backlog-handler.d.ts +4 -0
  52. package/dist/tools/list-backlog-handler.d.ts.map +1 -0
  53. package/dist/tools/list-backlog-handler.js +60 -0
  54. package/dist/tools/list-backlog-handler.js.map +1 -0
  55. package/dist/tools/manage-git.d.ts.map +1 -1
  56. package/dist/tools/manage-git.js +3 -0
  57. package/dist/tools/manage-git.js.map +1 -1
  58. package/dist/tools/promote-idea-handler.d.ts +4 -0
  59. package/dist/tools/promote-idea-handler.d.ts.map +1 -0
  60. package/dist/tools/promote-idea-handler.js +51 -0
  61. package/dist/tools/promote-idea-handler.js.map +1 -0
  62. package/dist/tools/register-backlog-tools.d.ts +3 -0
  63. package/dist/tools/register-backlog-tools.d.ts.map +1 -0
  64. package/dist/tools/register-backlog-tools.js +81 -0
  65. package/dist/tools/register-backlog-tools.js.map +1 -0
  66. package/dist/tools/register-ci-tools.d.ts.map +1 -1
  67. package/dist/tools/register-ci-tools.js +8 -0
  68. package/dist/tools/register-ci-tools.js.map +1 -1
  69. package/dist/tools/schemas/infra.d.ts +1 -1
  70. package/dist/tools/schemas/lifecycle.d.ts +1 -0
  71. package/dist/tools/schemas/lifecycle.d.ts.map +1 -1
  72. package/dist/tools/schemas/lifecycle.js +2 -1
  73. package/dist/tools/schemas/lifecycle.js.map +1 -1
  74. package/dist/tools/update-status-actions.d.ts.map +1 -1
  75. package/dist/tools/update-status-actions.js +12 -0
  76. package/dist/tools/update-status-actions.js.map +1 -1
  77. package/dist/types/ci.d.ts +16 -1
  78. package/dist/types/ci.d.ts.map +1 -1
  79. package/dist/types/common/tech-enums.d.ts +1 -1
  80. package/dist/types/common/tech-enums.d.ts.map +1 -1
  81. package/dist/types/estimation.d.ts +34 -0
  82. package/dist/types/estimation.d.ts.map +1 -1
  83. package/dist/types/git.d.ts +24 -0
  84. package/dist/types/git.d.ts.map +1 -1
  85. package/dist/types/ideas.d.ts +37 -0
  86. package/dist/types/ideas.d.ts.map +1 -0
  87. package/dist/types/ideas.js +3 -0
  88. package/dist/types/ideas.js.map +1 -0
  89. package/dist/types/index.d.ts +1 -0
  90. package/dist/types/index.d.ts.map +1 -1
  91. package/dist/types/index.js +1 -0
  92. package/dist/types/index.js.map +1 -1
  93. package/package.json +1 -1
  94. package/src/config/contradiction-patterns.json +83 -0
  95. package/src/config/estimation-tables.json +49 -0
  96. package/src/config/license-plans.json +5 -1
@@ -0,0 +1,39 @@
1
+ import { loadIdeas, saveIdea } from '../storage/ideas-store.js';
2
+ const EXPIRY_DAYS = 90;
3
+ /** Generate a unique idea ID based on date and sequence number. */
4
+ async function generateIdeaId(projectPath) {
5
+ const ideas = await loadIdeas(projectPath);
6
+ const today = new Date();
7
+ const dateStr = today.toISOString().slice(0, 10).replace(/-/g, '');
8
+ const todayPrefix = `idea-${dateStr}-`;
9
+ const todayCount = ideas.filter((i) => i.id.startsWith(todayPrefix)).length;
10
+ const seq = String(todayCount + 1).padStart(3, '0');
11
+ return `${todayPrefix}${seq}`;
12
+ }
13
+ /** Calculate expiry date 90 days from now. */
14
+ function calculateExpiry() {
15
+ const expiry = new Date();
16
+ expiry.setDate(expiry.getDate() + EXPIRY_DAYS);
17
+ return expiry.toISOString();
18
+ }
19
+ /** Handle capture_idea tool call. */
20
+ export async function handleCaptureIdea(input) {
21
+ const { projectPath, title, description, tags = [] } = input;
22
+ const id = await generateIdeaId(projectPath);
23
+ const now = new Date().toISOString();
24
+ const idea = {
25
+ id,
26
+ title,
27
+ description,
28
+ tags,
29
+ capturedAt: now,
30
+ capturedFrom: 'tool',
31
+ status: 'new',
32
+ expiresAt: calculateExpiry(),
33
+ };
34
+ await saveIdea(projectPath, idea);
35
+ return {
36
+ content: [{ type: 'text', text: `Idea guardada: [${id}] ${title}` }],
37
+ };
38
+ }
39
+ //# sourceMappingURL=capture-idea-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-idea-handler.js","sourceRoot":"","sources":["../../src/tools/capture-idea-handler.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAEhE,MAAM,WAAW,GAAG,EAAE,CAAC;AAEvB,mEAAmE;AACnE,KAAK,UAAU,cAAc,CAAC,WAAmB;IAC/C,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,WAAW,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC;IACzB,MAAM,OAAO,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACnE,MAAM,WAAW,GAAG,QAAQ,OAAO,GAAG,CAAC;IACvC,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC;IAC5E,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACpD,OAAO,GAAG,WAAW,GAAG,GAAG,EAAE,CAAC;AAChC,CAAC;AAED,8CAA8C;AAC9C,SAAS,eAAe;IACtB,MAAM,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;IAC1B,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,WAAW,CAAC,CAAC;IAC/C,OAAO,MAAM,CAAC,WAAW,EAAE,CAAC;AAC9B,CAAC;AAED,qCAAqC;AACrC,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAAuB;IAC7D,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,GAAG,EAAE,EAAE,GAAG,KAAK,CAAC;IAE7D,MAAM,EAAE,GAAG,MAAM,cAAc,CAAC,WAAW,CAAC,CAAC;IAC7C,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAErC,MAAM,IAAI,GAAa;QACrB,EAAE;QACF,KAAK;QACL,WAAW;QACX,IAAI;QACJ,UAAU,EAAE,GAAG;QACf,YAAY,EAAE,MAAM;QACpB,MAAM,EAAE,KAAK;QACb,SAAS,EAAE,eAAe,EAAE;KAC7B,CAAC;IAEF,MAAM,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;IAElC,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,EAAE,KAAK,KAAK,EAAE,EAAE,CAAC;KACrE,CAAC;AACJ,CAAC"}
@@ -1,8 +1,8 @@
1
1
  import type { GeneratePlanuCIInput, ToolResult } from '../types/index.js';
2
2
  /**
3
3
  * Handle the generate_planu_ci tool.
4
- * Generates a GitHub Actions workflow YAML for Planu spec validation in CI,
5
- * including drift detection, PR comments, badge, and .planu.yml config.
4
+ * Always generates a local planu-check.sh script (no GitHub billing required).
5
+ * Optionally generates a GitHub Actions workflow YAML for cloud CI.
6
6
  */
7
7
  export declare function handleGeneratePlanuCI(args: GeneratePlanuCIInput): ToolResult;
8
8
  //# sourceMappingURL=ci-planu-handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ci-planu-handler.d.ts","sourceRoot":"","sources":["../../src/tools/ci-planu-handler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAG1E;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,oBAAoB,GAAG,UAAU,CA0C5E"}
1
+ {"version":3,"file":"ci-planu-handler.d.ts","sourceRoot":"","sources":["../../src/tools/ci-planu-handler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAG1E;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,oBAAoB,GAAG,UAAU,CAqF5E"}
@@ -1,42 +1,75 @@
1
- // tools/ci-planu-handler.ts — Handle generate_planu_ci tool calls (SPEC-066 AC-02..07)
1
+ // tools/ci-planu-handler.ts — Handle generate_planu_ci tool calls (SPEC-066 AC-02..07, SPEC-201)
2
2
  import { generatePlanuCI } from '../engine/ci-generator/planu-steps.js';
3
3
  /**
4
4
  * Handle the generate_planu_ci tool.
5
- * Generates a GitHub Actions workflow YAML for Planu spec validation in CI,
6
- * including drift detection, PR comments, badge, and .planu.yml config.
5
+ * Always generates a local planu-check.sh script (no GitHub billing required).
6
+ * Optionally generates a GitHub Actions workflow YAML for cloud CI.
7
7
  */
8
8
  export function handleGeneratePlanuCI(args) {
9
+ const includeGithubActions = args.includeGithubActions ?? true;
9
10
  const result = generatePlanuCI(args);
10
11
  const lines = [
11
12
  '## Planu CI Workflow Generated',
12
13
  '',
13
14
  `**Features enabled**: ${result.features.join(', ')}`,
14
- `**Workflow path**: \`${result.workflowPath}\``,
15
15
  '',
16
- '### Generated Workflow YAML',
16
+ '---',
17
17
  '',
18
- '```yaml',
19
- result.workflowYaml.trimEnd(),
18
+ '### Option 1 — Local script (no GitHub billing required)',
19
+ '',
20
+ 'Save `planu-check.sh` to your project root and run it directly:',
21
+ '',
22
+ '```bash',
23
+ 'chmod +x planu-check.sh',
24
+ './planu-check.sh',
25
+ '```',
26
+ '',
27
+ '```bash',
28
+ result.localScript.trimEnd(),
29
+ '```',
30
+ '',
31
+ '**Optional: run as git pre-push hook** (catches issues before pushing):',
32
+ '',
33
+ '```bash',
34
+ 'ln -s ../../planu-check.sh .git/hooks/pre-push',
35
+ '```',
36
+ '',
37
+ '---',
38
+ '',
39
+ '### Option 2 — Run GitHub Actions locally with act (no billing)',
40
+ '',
41
+ '[act](https://github.com/nektos/act) runs GitHub Actions workflows on your machine.',
42
+ 'The generated workflow is fully compatible with act:',
43
+ '',
44
+ '```bash',
45
+ '# Install act (macOS)',
46
+ 'brew install act',
47
+ '',
48
+ '# Run the Planu check locally',
49
+ 'act -j planu-check',
20
50
  '```',
21
51
  '',
22
52
  ];
53
+ if (includeGithubActions && result.workflowYaml) {
54
+ lines.push('---', '', `### Option 3 — GitHub Actions (requires billing for private repos)`, '', `**Workflow path**: \`${result.workflowPath}\``, '', '```yaml', result.workflowYaml.trimEnd(), '```', '');
55
+ }
23
56
  // .planu.yml config
24
- lines.push('### Generated .planu.yml Configuration');
25
- lines.push('');
26
- lines.push('```yaml');
57
+ lines.push('---', '', '### Generated .planu.yml Configuration', '', '```yaml');
27
58
  lines.push(result.planuConfigYaml.trimEnd());
28
- lines.push('```');
29
- lines.push('');
30
- // Badge
31
- lines.push('### Badge for README');
32
- lines.push('');
33
- lines.push('Add this to your README.md (replace `{owner}` and `{repo}` with your values):');
34
- lines.push('');
35
- lines.push('```markdown');
36
- lines.push(result.badgeMarkdown);
37
- lines.push('```');
38
- lines.push('');
39
- lines.push(`> Save the workflow to \`${result.workflowPath}\` and the config to \`.planu.yml\` in your project root.`);
59
+ lines.push('```', '');
60
+ // Badge (only if GitHub Actions enabled)
61
+ if (includeGithubActions && result.badgeMarkdown) {
62
+ lines.push('### Badge for README', '');
63
+ lines.push('Add this to your README.md (replace `{owner}` and `{repo}` with your values):');
64
+ lines.push('', '```markdown');
65
+ lines.push(result.badgeMarkdown);
66
+ lines.push('```', '');
67
+ }
68
+ lines.push(`> **Save**: \`${result.localScriptPath}\` (local script, always)`);
69
+ if (includeGithubActions && result.workflowPath) {
70
+ lines.push(`> **Save**: \`${result.workflowPath}\` (GitHub Actions, optional)`);
71
+ }
72
+ lines.push('> **Save**: `.planu.yml` (shared config for both modes)');
40
73
  return {
41
74
  content: [{ type: 'text', text: lines.join('\n') }],
42
75
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ci-planu-handler.js","sourceRoot":"","sources":["../../src/tools/ci-planu-handler.ts"],"names":[],"mappings":"AAAA,uFAAuF;AAGvF,OAAO,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAC;AAExE;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAA0B;IAC9D,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IAErC,MAAM,KAAK,GAAa;QACtB,gCAAgC;QAChC,EAAE;QACF,yBAAyB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QACrD,wBAAwB,MAAM,CAAC,YAAY,IAAI;QAC/C,EAAE;QACF,6BAA6B;QAC7B,EAAE;QACF,SAAS;QACT,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE;QAC7B,KAAK;QACL,EAAE;KACH,CAAC;IAEF,oBAAoB;IACpB,KAAK,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAC;IACrD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACtB,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE,CAAC,CAAC;IAC7C,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,QAAQ;IACR,KAAK,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;IACnC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,+EAA+E,CAAC,CAAC;IAC5F,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC1B,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IACjC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,KAAK,CAAC,IAAI,CACR,4BAA4B,MAAM,CAAC,YAAY,2DAA2D,CAC3G,CAAC;IAEF,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;KAC7D,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"ci-planu-handler.js","sourceRoot":"","sources":["../../src/tools/ci-planu-handler.ts"],"names":[],"mappings":"AAAA,iGAAiG;AAGjG,OAAO,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAC;AAExE;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAA0B;IAC9D,MAAM,oBAAoB,GAAG,IAAI,CAAC,oBAAoB,IAAI,IAAI,CAAC;IAC/D,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IAErC,MAAM,KAAK,GAAa;QACtB,gCAAgC;QAChC,EAAE;QACF,yBAAyB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QACrD,EAAE;QACF,KAAK;QACL,EAAE;QACF,0DAA0D;QAC1D,EAAE;QACF,iEAAiE;QACjE,EAAE;QACF,SAAS;QACT,yBAAyB;QACzB,kBAAkB;QAClB,KAAK;QACL,EAAE;QACF,SAAS;QACT,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE;QAC5B,KAAK;QACL,EAAE;QACF,yEAAyE;QACzE,EAAE;QACF,SAAS;QACT,gDAAgD;QAChD,KAAK;QACL,EAAE;QACF,KAAK;QACL,EAAE;QACF,iEAAiE;QACjE,EAAE;QACF,qFAAqF;QACrF,sDAAsD;QACtD,EAAE;QACF,SAAS;QACT,uBAAuB;QACvB,kBAAkB;QAClB,EAAE;QACF,+BAA+B;QAC/B,oBAAoB;QACpB,KAAK;QACL,EAAE;KACH,CAAC;IAEF,IAAI,oBAAoB,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QAChD,KAAK,CAAC,IAAI,CACR,KAAK,EACL,EAAE,EACF,oEAAoE,EACpE,EAAE,EACF,wBAAwB,MAAM,CAAC,YAAY,IAAI,EAC/C,EAAE,EACF,SAAS,EACT,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,EAC7B,KAAK,EACL,EAAE,CACH,CAAC;IACJ,CAAC;IAED,oBAAoB;IACpB,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,EAAE,wCAAwC,EAAE,EAAE,EAAE,SAAS,CAAC,CAAC;IAC/E,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE,CAAC,CAAC;IAC7C,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAEtB,yCAAyC;IACzC,IAAI,oBAAoB,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;QACjD,KAAK,CAAC,IAAI,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAC;QACvC,KAAK,CAAC,IAAI,CAAC,+EAA+E,CAAC,CAAC;QAC5F,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,aAAa,CAAC,CAAC;QAC9B,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QACjC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACxB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,iBAAiB,MAAM,CAAC,eAAe,2BAA2B,CAAC,CAAC;IAC/E,IAAI,oBAAoB,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QAChD,KAAK,CAAC,IAAI,CAAC,iBAAiB,MAAM,CAAC,YAAY,+BAA+B,CAAC,CAAC;IAClF,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,yDAAyD,CAAC,CAAC;IAEtE,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;KAC7D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { ToolResult, DiscardIdeaInput } from '../types/index.js';
2
+ /** Handle discard_idea tool call. Idempotent if already discarded. */
3
+ export declare function handleDiscardIdea(input: DiscardIdeaInput): Promise<ToolResult>;
4
+ //# sourceMappingURL=discard-idea-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discard-idea-handler.d.ts","sourceRoot":"","sources":["../../src/tools/discard-idea-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAGtE,sEAAsE;AACtE,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CA4BpF"}
@@ -0,0 +1,27 @@
1
+ import { loadIdeas, updateIdea } from '../storage/ideas-store.js';
2
+ /** Handle discard_idea tool call. Idempotent if already discarded. */
3
+ export async function handleDiscardIdea(input) {
4
+ const { projectPath, ideaId, reason } = input;
5
+ const ideas = await loadIdeas(projectPath);
6
+ const idea = ideas.find((i) => i.id === ideaId);
7
+ if (!idea) {
8
+ return {
9
+ content: [{ type: 'text', text: `Idea not found: ${ideaId}` }],
10
+ isError: true,
11
+ };
12
+ }
13
+ // Idempotent: already discarded
14
+ if (idea.status === 'discarded') {
15
+ return {
16
+ content: [{ type: 'text', text: `Idea "${idea.title}" was already discarded.` }],
17
+ };
18
+ }
19
+ await updateIdea(projectPath, ideaId, {
20
+ status: 'discarded',
21
+ discardedReason: reason,
22
+ });
23
+ return {
24
+ content: [{ type: 'text', text: `Idea "${idea.title}" discarded. Reason: ${reason}` }],
25
+ };
26
+ }
27
+ //# sourceMappingURL=discard-idea-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discard-idea-handler.js","sourceRoot":"","sources":["../../src/tools/discard-idea-handler.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAElE,sEAAsE;AACtE,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAAuB;IAC7D,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAE9C,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,WAAW,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC;IAEhD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,MAAM,EAAE,EAAE,CAAC;YAC9D,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,gCAAgC;IAChC,IAAI,IAAI,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;QAChC,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,IAAI,CAAC,KAAK,0BAA0B,EAAE,CAAC;SACjF,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE;QACpC,MAAM,EAAE,WAAW;QACnB,eAAe,EAAE,MAAM;KACxB,CAAC,CAAC;IAEH,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,IAAI,CAAC,KAAK,wBAAwB,MAAM,EAAE,EAAE,CAAC;KACvF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { ToolResult, GitConfig, SpecDoneCleanupResult } from '../../types/index.js';
2
+ /**
3
+ * AC-02: Eliminate gone branches + remote prune.
4
+ * AC-03: List stale stashes (>7d); drop if force=true.
5
+ * AC-05: Idempotent.
6
+ * AC-06: Never deletes active branch or current HEAD.
7
+ * AC-07: Returns detailed report.
8
+ */
9
+ export declare function handleCleanup(projectId: string, config?: GitConfig): Promise<ToolResult>;
10
+ /**
11
+ * AC-01: When a spec is marked done, clean its worktree, local branch, and remote branch.
12
+ * Best-effort — never throws. Caller should fire-and-forget or log result.
13
+ */
14
+ export declare function cleanupSpecOnDone(projectPath: string, specId: string, gitBranch: string | undefined): Promise<SpecDoneCleanupResult>;
15
+ //# sourceMappingURL=cleanup-ops.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cleanup-ops.d.ts","sourceRoot":"","sources":["../../../src/tools/git/cleanup-ops.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,UAAU,EAGV,SAAS,EACT,qBAAqB,EACtB,MAAM,sBAAsB,CAAC;AAgR9B;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CA8E9F;AAsBD;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,OAAO,CAAC,qBAAqB,CAAC,CAqFhC"}
@@ -0,0 +1,417 @@
1
+ // tools/git/cleanup-ops.ts — Git cleanup: worktrees, branches, remote prune, stashes (SPEC-203)
2
+ import { existsSync } from 'node:fs';
3
+ import { readdir } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { git, resolveProjectPath } from './git-helpers.js';
6
+ const WORKTREES_DIR = '.claude/worktrees';
7
+ const STALE_DAYS_THRESHOLD = 7;
8
+ const MILLISECONDS_PER_DAY = 86_400_000;
9
+ // ---------------------------------------------------------------------------
10
+ // Worktree helpers
11
+ // ---------------------------------------------------------------------------
12
+ /** Parse `git worktree list --porcelain` output into {path, branch} records. */
13
+ function parseWorktreeList(output) {
14
+ const entries = [];
15
+ let current = {};
16
+ for (const line of output.split('\n')) {
17
+ if (line.startsWith('worktree ')) {
18
+ current = { path: line.slice('worktree '.length).trim() };
19
+ }
20
+ else if (line.startsWith('branch ')) {
21
+ current.branch = line.slice('branch '.length).trim().replace('refs/heads/', '');
22
+ }
23
+ else if (line === '' && current.path) {
24
+ if (current.branch) {
25
+ entries.push({ path: current.path, branch: current.branch });
26
+ }
27
+ current = {};
28
+ }
29
+ }
30
+ return entries;
31
+ }
32
+ /** Returns the set of branches already merged into develop (or main as fallback). */
33
+ async function getMergedBranches(projectPath) {
34
+ const bases = ['develop', 'main', 'master'];
35
+ for (const base of bases) {
36
+ try {
37
+ const { stdout } = await git(projectPath, ['branch', '--merged', base]);
38
+ return new Set(stdout
39
+ .split('\n')
40
+ .map((b) => b.replace(/^\*?\s+/, '').trim())
41
+ .filter(Boolean));
42
+ }
43
+ catch {
44
+ // try next base
45
+ }
46
+ }
47
+ return new Set();
48
+ }
49
+ /** Remove a worktree — idempotent (ignores "not a worktree" errors). */
50
+ async function removeWorktree(projectPath, worktreePath) {
51
+ try {
52
+ await git(projectPath, ['worktree', 'remove', '--force', worktreePath]);
53
+ }
54
+ catch (err) {
55
+ const msg = err instanceof Error ? err.message : String(err);
56
+ // Already gone — not an error
57
+ if (!msg.includes('not a working tree') && !msg.includes('does not exist')) {
58
+ throw err;
59
+ }
60
+ }
61
+ }
62
+ async function cleanupOrphanWorktrees(projectPath, mergedBranches) {
63
+ const removed = [];
64
+ const errors = [];
65
+ let worktreeOutput;
66
+ try {
67
+ const { stdout } = await git(projectPath, ['worktree', 'list', '--porcelain']);
68
+ worktreeOutput = stdout;
69
+ }
70
+ catch {
71
+ return { removed, errors };
72
+ }
73
+ const worktrees = parseWorktreeList(worktreeOutput);
74
+ const worktreesSubdir = join(projectPath, WORKTREES_DIR);
75
+ for (const wt of worktrees) {
76
+ // Never touch the main worktree (same path as projectPath)
77
+ if (wt.path === projectPath) {
78
+ continue;
79
+ }
80
+ const isInWorktreesDir = wt.path.startsWith(worktreesSubdir);
81
+ const isMergedBranch = wt.branch && mergedBranches.has(wt.branch);
82
+ if (isInWorktreesDir || isMergedBranch) {
83
+ // Never remove if it's the current HEAD
84
+ try {
85
+ const { stdout: headBranch } = await git(projectPath, [
86
+ 'rev-parse',
87
+ '--abbrev-ref',
88
+ 'HEAD',
89
+ ]);
90
+ if (headBranch.trim() === wt.branch) {
91
+ continue;
92
+ }
93
+ }
94
+ catch {
95
+ // Skip safety check failure — do not remove
96
+ continue;
97
+ }
98
+ try {
99
+ await removeWorktree(projectPath, wt.path);
100
+ removed.push(wt.path);
101
+ }
102
+ catch (err) {
103
+ errors.push(`worktree ${wt.path}: ${err instanceof Error ? err.message : String(err)}`);
104
+ }
105
+ }
106
+ }
107
+ // Also prune stale worktree admin dirs (git worktree prune)
108
+ try {
109
+ await git(projectPath, ['worktree', 'prune']);
110
+ }
111
+ catch {
112
+ // best-effort
113
+ }
114
+ return { removed, errors };
115
+ }
116
+ // ---------------------------------------------------------------------------
117
+ // Branch helpers
118
+ // ---------------------------------------------------------------------------
119
+ async function cleanupGoneBranches(projectPath) {
120
+ const removed = [];
121
+ const errors = [];
122
+ let vvOutput;
123
+ try {
124
+ const { stdout } = await git(projectPath, ['branch', '-vv']);
125
+ vvOutput = stdout;
126
+ }
127
+ catch {
128
+ return { removed, errors };
129
+ }
130
+ // Lines containing ": gone]" mean upstream was deleted (git branch -vv format: [origin/...: gone])
131
+ const goneBranches = vvOutput
132
+ .split('\n')
133
+ .filter((line) => line.includes(': gone]'))
134
+ .map((line) => line
135
+ .replace(/^\*?\s+/, '')
136
+ .split(/\s+/)[0]
137
+ ?.trim() ?? '')
138
+ .filter(Boolean);
139
+ // Resolve current branch only when needed to avoid an extra git call (AC-05)
140
+ let currentBranch = '';
141
+ if (goneBranches.length > 0) {
142
+ try {
143
+ const { stdout } = await git(projectPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
144
+ currentBranch = stdout.trim();
145
+ }
146
+ catch {
147
+ // ignore
148
+ }
149
+ }
150
+ for (const branch of goneBranches) {
151
+ if (branch === currentBranch) {
152
+ continue;
153
+ }
154
+ try {
155
+ await git(projectPath, ['branch', '-d', branch]);
156
+ removed.push(branch);
157
+ }
158
+ catch {
159
+ // Force-delete if branch is not fully merged (upstream gone = safe)
160
+ try {
161
+ await git(projectPath, ['branch', '-D', branch]);
162
+ removed.push(branch);
163
+ }
164
+ catch (err) {
165
+ errors.push(`branch ${branch}: ${err instanceof Error ? err.message : String(err)}`);
166
+ }
167
+ }
168
+ }
169
+ return { removed, errors };
170
+ }
171
+ // ---------------------------------------------------------------------------
172
+ // Stash helpers
173
+ // ---------------------------------------------------------------------------
174
+ function parseDaysOld(stashMessage) {
175
+ // git stash list format: stash@{0}: WIP on branch: hash message
176
+ // We detect age from the stash index timestamp via git log on the stash ref
177
+ // Since we can't get date from the message directly, we return 0 here
178
+ // and use git log to get the actual date per stash ref in detectStaleStashes.
179
+ void stashMessage;
180
+ return 0;
181
+ }
182
+ async function detectStaleStashes(projectPath) {
183
+ const stale = [];
184
+ let listOutput;
185
+ try {
186
+ const { stdout } = await git(projectPath, ['stash', 'list']);
187
+ listOutput = stdout;
188
+ }
189
+ catch {
190
+ return stale;
191
+ }
192
+ const lines = listOutput.split('\n').filter(Boolean);
193
+ const now = Date.now();
194
+ for (const line of lines) {
195
+ const match = /^(stash@\{(\d+)\}):\s+(.+)$/.exec(line);
196
+ if (!match) {
197
+ continue;
198
+ }
199
+ const [, ref, indexStr, message] = match;
200
+ if (!ref || !indexStr || !message) {
201
+ continue;
202
+ }
203
+ const index = parseInt(indexStr, 10);
204
+ // Get commit date for this stash ref
205
+ let daysOld = parseDaysOld(message);
206
+ try {
207
+ const { stdout: dateOut } = await git(projectPath, ['log', '-1', '--format=%ct', ref]);
208
+ const epochSeconds = parseInt(dateOut.trim(), 10);
209
+ if (!isNaN(epochSeconds)) {
210
+ daysOld = Math.floor((now - epochSeconds * 1000) / MILLISECONDS_PER_DAY);
211
+ }
212
+ }
213
+ catch {
214
+ // Can't determine age — skip this stash
215
+ continue;
216
+ }
217
+ if (daysOld >= STALE_DAYS_THRESHOLD) {
218
+ stale.push({ index, ref, message, daysOld });
219
+ }
220
+ }
221
+ return stale;
222
+ }
223
+ async function dropStaleStashes(projectPath, stale) {
224
+ const errors = [];
225
+ // Drop in reverse index order so indices stay valid
226
+ const sorted = [...stale].sort((a, b) => b.index - a.index);
227
+ let dropped = 0;
228
+ for (const stash of sorted) {
229
+ try {
230
+ await git(projectPath, ['stash', 'drop', stash.ref]);
231
+ dropped++;
232
+ }
233
+ catch (err) {
234
+ errors.push(`stash ${stash.ref}: ${err instanceof Error ? err.message : String(err)}`);
235
+ }
236
+ }
237
+ return { dropped, errors };
238
+ }
239
+ // ---------------------------------------------------------------------------
240
+ // Public handler
241
+ // ---------------------------------------------------------------------------
242
+ /**
243
+ * AC-02: Eliminate gone branches + remote prune.
244
+ * AC-03: List stale stashes (>7d); drop if force=true.
245
+ * AC-05: Idempotent.
246
+ * AC-06: Never deletes active branch or current HEAD.
247
+ * AC-07: Returns detailed report.
248
+ */
249
+ export async function handleCleanup(projectId, config) {
250
+ const force = config?.force ?? false;
251
+ const errors = [];
252
+ let projectPath;
253
+ try {
254
+ projectPath = await resolveProjectPath(projectId);
255
+ }
256
+ catch (err) {
257
+ return {
258
+ content: [
259
+ {
260
+ type: 'text',
261
+ text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }, null, 2),
262
+ },
263
+ ],
264
+ isError: true,
265
+ };
266
+ }
267
+ // 1. Remote prune
268
+ let remotePruned = false;
269
+ try {
270
+ await git(projectPath, ['remote', 'prune', 'origin']);
271
+ remotePruned = true;
272
+ }
273
+ catch (err) {
274
+ errors.push(`remote prune: ${err instanceof Error ? err.message : String(err)}`);
275
+ }
276
+ // 2. Get merged branches (used for worktree and branch cleanup)
277
+ const mergedBranches = await getMergedBranches(projectPath);
278
+ // 3. Remove orphan worktrees
279
+ const { removed: worktreesRemoved, errors: wtErrors } = await cleanupOrphanWorktrees(projectPath, mergedBranches);
280
+ errors.push(...wtErrors);
281
+ // 4. Remove branches with gone upstream
282
+ const { removed: branchesRemoved, errors: branchErrors } = await cleanupGoneBranches(projectPath);
283
+ errors.push(...branchErrors);
284
+ // 5. Detect / drop stale stashes
285
+ const staleStashes = await detectStaleStashes(projectPath);
286
+ let stashesDropped = 0;
287
+ if (force && staleStashes.length > 0) {
288
+ const { dropped, errors: stashErrors } = await dropStaleStashes(projectPath, staleStashes);
289
+ stashesDropped = dropped;
290
+ errors.push(...stashErrors);
291
+ }
292
+ const messageParts = [
293
+ `Worktrees removed: ${worktreesRemoved.length}`,
294
+ `Branches removed: ${branchesRemoved.length}`,
295
+ `Remote pruned: ${remotePruned ? 'yes' : 'no'}`,
296
+ `Stale stashes (>${STALE_DAYS_THRESHOLD}d): ${staleStashes.length}`,
297
+ ...(force ? [`Stashes dropped: ${stashesDropped}`] : ['Use force:true to drop stale stashes']),
298
+ ...(errors.length > 0 ? [`Errors: ${errors.length}`] : []),
299
+ ];
300
+ const report = {
301
+ action: 'cleanup',
302
+ worktreesRemoved,
303
+ branchesRemoved,
304
+ remotePruned,
305
+ stalesStashes: staleStashes,
306
+ stashesDropped,
307
+ errors,
308
+ message: messageParts.join(' | '),
309
+ };
310
+ return {
311
+ content: [{ type: 'text', text: JSON.stringify(report, null, 2) }],
312
+ };
313
+ }
314
+ // ---------------------------------------------------------------------------
315
+ // Spec-done cleanup (AC-01) — called from update-status-actions.ts
316
+ // ---------------------------------------------------------------------------
317
+ /** Checks whether a worktree path for a given spec exists in .claude/worktrees/ */
318
+ async function findSpecWorktree(projectPath, specId) {
319
+ const worktreesPath = join(projectPath, WORKTREES_DIR);
320
+ if (!existsSync(worktreesPath)) {
321
+ return null;
322
+ }
323
+ try {
324
+ const entries = await readdir(worktreesPath);
325
+ const match = entries.find((e) => e.toLowerCase().includes(specId.toLowerCase()));
326
+ return match ? join(worktreesPath, match) : null;
327
+ }
328
+ catch {
329
+ return null;
330
+ }
331
+ }
332
+ /**
333
+ * AC-01: When a spec is marked done, clean its worktree, local branch, and remote branch.
334
+ * Best-effort — never throws. Caller should fire-and-forget or log result.
335
+ */
336
+ export async function cleanupSpecOnDone(projectPath, specId, gitBranch) {
337
+ const result = {
338
+ worktreeRemoved: null,
339
+ localBranchRemoved: null,
340
+ remoteBranchRemoved: null,
341
+ errors: [],
342
+ };
343
+ // 1. Remove worktree if it exists
344
+ const worktreePath = await findSpecWorktree(projectPath, specId);
345
+ if (worktreePath) {
346
+ try {
347
+ await removeWorktree(projectPath, worktreePath);
348
+ result.worktreeRemoved = worktreePath;
349
+ }
350
+ catch (err) {
351
+ result.errors.push(`worktree: ${err instanceof Error ? err.message : String(err)}`);
352
+ }
353
+ }
354
+ if (!gitBranch) {
355
+ return result;
356
+ }
357
+ // 2. Remove local branch if merged
358
+ try {
359
+ const { stdout } = await git(projectPath, ['branch', '--merged', 'develop']);
360
+ const merged = new Set(stdout
361
+ .split('\n')
362
+ .map((b) => b.replace(/^\*?\s+/, '').trim())
363
+ .filter(Boolean));
364
+ const isMerged = merged.has(gitBranch);
365
+ // Also check main as fallback
366
+ let isMergedMain = false;
367
+ if (!isMerged) {
368
+ try {
369
+ const { stdout: mainOut } = await git(projectPath, ['branch', '--merged', 'main']);
370
+ const mergedMain = new Set(mainOut
371
+ .split('\n')
372
+ .map((b) => b.replace(/^\*?\s+/, '').trim())
373
+ .filter(Boolean));
374
+ isMergedMain = mergedMain.has(gitBranch);
375
+ }
376
+ catch {
377
+ // ignore
378
+ }
379
+ }
380
+ if (isMerged || isMergedMain) {
381
+ // Never delete current branch
382
+ const { stdout: headOut } = await git(projectPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
383
+ if (headOut.trim() !== gitBranch) {
384
+ try {
385
+ await git(projectPath, ['branch', '-d', gitBranch]);
386
+ result.localBranchRemoved = gitBranch;
387
+ }
388
+ catch {
389
+ // Force-delete if needed
390
+ try {
391
+ await git(projectPath, ['branch', '-D', gitBranch]);
392
+ result.localBranchRemoved = gitBranch;
393
+ }
394
+ catch (err) {
395
+ result.errors.push(`local branch: ${err instanceof Error ? err.message : String(err)}`);
396
+ }
397
+ }
398
+ }
399
+ }
400
+ }
401
+ catch (err) {
402
+ result.errors.push(`merged check: ${err instanceof Error ? err.message : String(err)}`);
403
+ }
404
+ // 3. Remove remote branch if it exists
405
+ try {
406
+ const { stdout } = await git(projectPath, ['ls-remote', '--heads', 'origin', gitBranch]);
407
+ if (stdout.trim()) {
408
+ await git(projectPath, ['push', 'origin', '--delete', gitBranch]);
409
+ result.remoteBranchRemoved = `origin/${gitBranch}`;
410
+ }
411
+ }
412
+ catch (err) {
413
+ result.errors.push(`remote branch: ${err instanceof Error ? err.message : String(err)}`);
414
+ }
415
+ return result;
416
+ }
417
+ //# sourceMappingURL=cleanup-ops.js.map