edsger 0.30.3 → 0.31.0

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 (115) hide show
  1. package/.claude/settings.local.json +3 -23
  2. package/dist/api/github.d.ts +11 -0
  3. package/dist/api/github.js +44 -0
  4. package/dist/index.js +19 -6
  5. package/dist/phases/growth-analysis/agent.d.ts +1 -1
  6. package/dist/phases/growth-analysis/agent.js +2 -1
  7. package/dist/phases/growth-analysis/context.d.ts +1 -1
  8. package/dist/phases/growth-analysis/context.js +11 -3
  9. package/dist/phases/growth-analysis/index.js +26 -3
  10. package/dist/phases/growth-analysis/prompts.d.ts +2 -2
  11. package/dist/phases/growth-analysis/prompts.js +34 -17
  12. package/dist/phases/pr-execution/context.js +7 -7
  13. package/dist/phases/pr-splitting/context.js +8 -8
  14. package/dist/phases/pull-request/creator.js +6 -5
  15. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +4 -0
  16. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +133 -0
  17. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +4 -0
  18. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +336 -0
  19. package/dist/services/lifecycle-agent/index.d.ts +24 -0
  20. package/dist/services/lifecycle-agent/index.js +25 -0
  21. package/dist/services/lifecycle-agent/phase-criteria.d.ts +57 -0
  22. package/dist/services/lifecycle-agent/phase-criteria.js +335 -0
  23. package/dist/services/lifecycle-agent/transition-rules.d.ts +60 -0
  24. package/dist/services/lifecycle-agent/transition-rules.js +184 -0
  25. package/dist/services/lifecycle-agent/types.d.ts +190 -0
  26. package/dist/services/lifecycle-agent/types.js +12 -0
  27. package/dist/utils/git-branch-manager.js +2 -2
  28. package/package.json +1 -1
  29. package/.env.local +0 -12
  30. package/dist/api/features/__tests__/regression-prevention.test.d.ts +0 -5
  31. package/dist/api/features/__tests__/regression-prevention.test.js +0 -338
  32. package/dist/api/features/__tests__/status-updater.integration.test.d.ts +0 -5
  33. package/dist/api/features/__tests__/status-updater.integration.test.js +0 -497
  34. package/dist/commands/workflow/pipeline-runner.d.ts +0 -17
  35. package/dist/commands/workflow/pipeline-runner.js +0 -393
  36. package/dist/commands/workflow/runner.d.ts +0 -26
  37. package/dist/commands/workflow/runner.js +0 -119
  38. package/dist/commands/workflow/workflow-runner.d.ts +0 -26
  39. package/dist/commands/workflow/workflow-runner.js +0 -119
  40. package/dist/phases/code-implementation/analyzer-helpers.d.ts +0 -28
  41. package/dist/phases/code-implementation/analyzer-helpers.js +0 -177
  42. package/dist/phases/code-implementation/analyzer.d.ts +0 -32
  43. package/dist/phases/code-implementation/analyzer.js +0 -629
  44. package/dist/phases/code-implementation/context-fetcher.d.ts +0 -17
  45. package/dist/phases/code-implementation/context-fetcher.js +0 -86
  46. package/dist/phases/code-implementation/mcp-server.d.ts +0 -1
  47. package/dist/phases/code-implementation/mcp-server.js +0 -93
  48. package/dist/phases/code-implementation/prompts-improvement.d.ts +0 -5
  49. package/dist/phases/code-implementation/prompts-improvement.js +0 -108
  50. package/dist/phases/code-implementation-verification/verifier.d.ts +0 -31
  51. package/dist/phases/code-implementation-verification/verifier.js +0 -196
  52. package/dist/phases/code-refine/analyzer.d.ts +0 -41
  53. package/dist/phases/code-refine/analyzer.js +0 -561
  54. package/dist/phases/code-refine/context-fetcher.d.ts +0 -94
  55. package/dist/phases/code-refine/context-fetcher.js +0 -423
  56. package/dist/phases/code-refine-verification/analysis/llm-analyzer.d.ts +0 -22
  57. package/dist/phases/code-refine-verification/analysis/llm-analyzer.js +0 -134
  58. package/dist/phases/code-refine-verification/verifier.d.ts +0 -47
  59. package/dist/phases/code-refine-verification/verifier.js +0 -597
  60. package/dist/phases/code-review/analyzer.d.ts +0 -29
  61. package/dist/phases/code-review/analyzer.js +0 -363
  62. package/dist/phases/code-review/context-fetcher.d.ts +0 -92
  63. package/dist/phases/code-review/context-fetcher.js +0 -296
  64. package/dist/phases/feature-analysis/analyzer-helpers.d.ts +0 -10
  65. package/dist/phases/feature-analysis/analyzer-helpers.js +0 -47
  66. package/dist/phases/feature-analysis/analyzer.d.ts +0 -11
  67. package/dist/phases/feature-analysis/analyzer.js +0 -208
  68. package/dist/phases/feature-analysis/context-fetcher.d.ts +0 -26
  69. package/dist/phases/feature-analysis/context-fetcher.js +0 -134
  70. package/dist/phases/feature-analysis/http-fallback.d.ts +0 -20
  71. package/dist/phases/feature-analysis/http-fallback.js +0 -95
  72. package/dist/phases/feature-analysis/mcp-server.d.ts +0 -1
  73. package/dist/phases/feature-analysis/mcp-server.js +0 -144
  74. package/dist/phases/feature-analysis/prompts-improvement.d.ts +0 -8
  75. package/dist/phases/feature-analysis/prompts-improvement.js +0 -109
  76. package/dist/phases/feature-analysis-verification/verifier.d.ts +0 -37
  77. package/dist/phases/feature-analysis-verification/verifier.js +0 -147
  78. package/dist/phases/technical-design/analyzer-helpers.d.ts +0 -25
  79. package/dist/phases/technical-design/analyzer-helpers.js +0 -39
  80. package/dist/phases/technical-design/analyzer.d.ts +0 -21
  81. package/dist/phases/technical-design/analyzer.js +0 -461
  82. package/dist/phases/technical-design/context-fetcher.d.ts +0 -12
  83. package/dist/phases/technical-design/context-fetcher.js +0 -39
  84. package/dist/phases/technical-design/http-fallback.d.ts +0 -17
  85. package/dist/phases/technical-design/http-fallback.js +0 -151
  86. package/dist/phases/technical-design/mcp-server.d.ts +0 -1
  87. package/dist/phases/technical-design/mcp-server.js +0 -157
  88. package/dist/phases/technical-design/prompts-improvement.d.ts +0 -5
  89. package/dist/phases/technical-design/prompts-improvement.js +0 -93
  90. package/dist/phases/technical-design-verification/verifier.d.ts +0 -53
  91. package/dist/phases/technical-design-verification/verifier.js +0 -170
  92. package/dist/services/feature-branches.d.ts +0 -77
  93. package/dist/services/feature-branches.js +0 -205
  94. package/dist/workflow-runner/config/phase-configs.d.ts +0 -5
  95. package/dist/workflow-runner/config/phase-configs.js +0 -120
  96. package/dist/workflow-runner/core/feature-filter.d.ts +0 -16
  97. package/dist/workflow-runner/core/feature-filter.js +0 -46
  98. package/dist/workflow-runner/core/index.d.ts +0 -8
  99. package/dist/workflow-runner/core/index.js +0 -12
  100. package/dist/workflow-runner/core/pipeline-evaluator.d.ts +0 -24
  101. package/dist/workflow-runner/core/pipeline-evaluator.js +0 -32
  102. package/dist/workflow-runner/core/state-manager.d.ts +0 -24
  103. package/dist/workflow-runner/core/state-manager.js +0 -42
  104. package/dist/workflow-runner/core/workflow-logger.d.ts +0 -20
  105. package/dist/workflow-runner/core/workflow-logger.js +0 -65
  106. package/dist/workflow-runner/executors/phase-executor.d.ts +0 -8
  107. package/dist/workflow-runner/executors/phase-executor.js +0 -248
  108. package/dist/workflow-runner/feature-workflow-runner.d.ts +0 -26
  109. package/dist/workflow-runner/feature-workflow-runner.js +0 -119
  110. package/dist/workflow-runner/index.d.ts +0 -2
  111. package/dist/workflow-runner/index.js +0 -2
  112. package/dist/workflow-runner/pipeline-runner.d.ts +0 -17
  113. package/dist/workflow-runner/pipeline-runner.js +0 -393
  114. package/dist/workflow-runner/workflow-processor.d.ts +0 -54
  115. package/dist/workflow-runner/workflow-processor.js +0 -170
@@ -1,28 +1,8 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Read(//Users/steven/development/edsger/**)",
5
- "Bash(npm run build)",
6
- "Bash(node:*)",
7
- "Bash(git add:*)",
8
- "Bash(git commit:*)",
9
- "Bash(ls:*)",
10
- "Bash(cat:*)",
11
- "Bash(npm run typecheck:*)",
12
- "Bash(git diff:*)",
13
- "WebSearch",
14
- "WebFetch(domain:supabase.com)",
15
- "Bash(npm install:*)",
16
- "Bash(grep:*)",
17
- "Bash(npx supabase gen types typescript --help:*)",
18
- "Bash(git -C /Users/steven/development/edsger status)",
19
- "Bash(git -C /Users/steven/development/edsger diff)",
20
- "Bash(git -C /Users/steven/development/edsger log --oneline -5)",
21
- "Bash(git -C /Users/steven/development/edsger add supabase/migrations/20251231000000_drop_unused_views.sql)",
22
- "Bash(git -C /Users/steven/development/edsger commit -m \"$\\(cat <<''EOF''\nchore: drop unused database views\n\nRemove test_report_summary and user_stories_with_context views that are defined but never used in the application.\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
23
- "Bash(git -C /Users/steven/development/edsger commit -m \"$\\(cat <<''EOF''\nchore: drop unused database views\n\nRemove test_report_summary and user_stories_with_context views\nthat are defined but never used in the application.\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")"
24
- ],
25
- "deny": [],
26
- "ask": []
4
+ "Bash(npx tsc:*)",
5
+ "Bash(npm run:*)"
6
+ ]
27
7
  }
28
8
  }
@@ -39,6 +39,17 @@ export declare function getGitHubDeveloperConfig(featureId: string, verbose?: bo
39
39
  * Get installation access token for GitHub API operations
40
40
  */
41
41
  export declare function getGitHubInstallationToken(installationId: number, featureId: string, verbose?: boolean): Promise<GitHubInstallationToken | null>;
42
+ /**
43
+ * Get GitHub config and token by product ID (no feature required).
44
+ * Used for product-level operations like growth analysis.
45
+ */
46
+ export declare function getGitHubConfigByProduct(productId: string, verbose?: boolean): Promise<{
47
+ configured: boolean;
48
+ token?: string;
49
+ owner?: string;
50
+ repo?: string;
51
+ message?: string;
52
+ }>;
42
53
  /**
43
54
  * Get GitHub config and token in one call
44
55
  * This is the main entry point for getting GitHub config
@@ -59,6 +59,50 @@ export async function getGitHubInstallationToken(installationId, featureId, verb
59
59
  return null;
60
60
  }
61
61
  }
62
+ /**
63
+ * Get GitHub config and token by product ID (no feature required).
64
+ * Used for product-level operations like growth analysis.
65
+ */
66
+ export async function getGitHubConfigByProduct(productId, verbose) {
67
+ if (verbose) {
68
+ console.log(`🔍 Fetching GitHub config for product: ${productId}`);
69
+ }
70
+ try {
71
+ const result = (await callMcpEndpoint('github/config_and_token_by_product', {
72
+ product_id: productId,
73
+ }));
74
+ if (verbose) {
75
+ if (result.configured) {
76
+ console.log(`✅ GitHub ready: ${result.repository_full_name}`);
77
+ }
78
+ else {
79
+ console.log(`⚠️ GitHub not configured: ${result.message}`);
80
+ }
81
+ }
82
+ if (result.configured && result.token && result.owner && result.repo) {
83
+ return {
84
+ configured: true,
85
+ token: result.token,
86
+ owner: result.owner,
87
+ repo: result.repo,
88
+ };
89
+ }
90
+ return {
91
+ configured: false,
92
+ message: result.message ||
93
+ 'GitHub not configured for this product. Set repo and developer_id in product settings.',
94
+ };
95
+ }
96
+ catch (error) {
97
+ if (verbose) {
98
+ console.error('❌ Failed to get GitHub config for product:', error);
99
+ }
100
+ return {
101
+ configured: false,
102
+ message: error instanceof Error ? error.message : 'Failed to get GitHub config',
103
+ };
104
+ }
105
+ }
62
106
  /**
63
107
  * Get GitHub config and token in one call
64
108
  * This is the main entry point for getting GitHub config
package/dist/index.js CHANGED
@@ -72,6 +72,25 @@ program
72
72
  }
73
73
  });
74
74
  // ============================================================
75
+ // Subcommand: edsger growth <productId>
76
+ // ============================================================
77
+ program
78
+ .command('growth <productId>')
79
+ .description('Run AI-powered growth analysis for a product')
80
+ .option('-v, --verbose', 'Verbose output')
81
+ .action(async (productId, opts) => {
82
+ try {
83
+ await runGrowthAnalysis({
84
+ growthAnalysis: productId,
85
+ verbose: opts.verbose,
86
+ });
87
+ }
88
+ catch (error) {
89
+ logError(error instanceof Error ? error.message : String(error));
90
+ process.exit(1);
91
+ }
92
+ });
93
+ // ============================================================
75
94
  // Default command (options-based, backward compatible)
76
95
  // ============================================================
77
96
  program
@@ -80,7 +99,6 @@ program
80
99
  .option('-f, --files <patterns...>', 'Review specific file patterns')
81
100
  .option('--refactor', 'Refactor code in current directory')
82
101
  .option('--init', 'Initialize .edsger directory with project templates')
83
- .option('--growth-analysis <productId>', 'Run AI-powered growth analysis for a product')
84
102
  .option('--product-level', 'Use legacy product-level workflow (requires EDSGER_PRODUCT_ID, EDSGER_MCP_SERVER_URL, EDSGER_MCP_TOKEN env vars)')
85
103
  .option('--watch-tasks <productId>', 'Watch and execute pending tasks for a product')
86
104
  .option('--concurrency <number>', 'Max concurrent features to process (default: 3)', parseInt)
@@ -106,11 +124,6 @@ export const runEdsger = async (options) => {
106
124
  await runTaskWorker(options);
107
125
  return;
108
126
  }
109
- // Handle growth analysis mode
110
- if (options.growthAnalysis) {
111
- await runGrowthAnalysis(options);
112
- return;
113
- }
114
127
  // Handle refactor mode
115
128
  if (options.refactor) {
116
129
  await runRefactor(options);
@@ -1,2 +1,2 @@
1
1
  import { EdsgerConfig } from '../../types/index.js';
2
- export declare function executeGrowthAnalysisQuery(currentPrompt: string, systemPrompt: string, config: EdsgerConfig, verbose?: boolean): Promise<any | null>;
2
+ export declare function executeGrowthAnalysisQuery(currentPrompt: string, systemPrompt: string, config: EdsgerConfig, verbose?: boolean, cwd?: string): Promise<any | null>;
@@ -38,7 +38,7 @@ function userMessage(content) {
38
38
  async function* prompt(analysisPrompt) {
39
39
  yield userMessage(analysisPrompt);
40
40
  }
41
- export async function executeGrowthAnalysisQuery(currentPrompt, systemPrompt, config, verbose) {
41
+ export async function executeGrowthAnalysisQuery(currentPrompt, systemPrompt, config, verbose, cwd) {
42
42
  let lastAssistantResponse = '';
43
43
  let structuredResult = null;
44
44
  for await (const message of query({
@@ -52,6 +52,7 @@ export async function executeGrowthAnalysisQuery(currentPrompt, systemPrompt, co
52
52
  model: DEFAULT_MODEL,
53
53
  maxTurns: 1000,
54
54
  permissionMode: 'bypassPermissions',
55
+ ...(cwd ? { cwd } : {}),
55
56
  },
56
57
  })) {
57
58
  if (verbose) {
@@ -15,7 +15,7 @@ export declare function formatContextForPrompt(context: GrowthAnalysisContext):
15
15
  /**
16
16
  * Prepare the full analysis prompt with all context
17
17
  */
18
- export declare function prepareGrowthAnalysisContext(productId: string, verbose?: boolean): Promise<{
18
+ export declare function prepareGrowthAnalysisContext(productId: string, verbose?: boolean, hasCodebase?: boolean): Promise<{
19
19
  context: GrowthAnalysisContext;
20
20
  analysisPrompt: string;
21
21
  }>;
@@ -40,6 +40,11 @@ export function formatContextForPrompt(context) {
40
40
  Created: ${c.created_at}`)
41
41
  .join('\n\n')
42
42
  : 'No previous campaigns.';
43
+ const featuresList = product.features && product.features.length > 0
44
+ ? product.features
45
+ .map((f) => `- **${f.name}**: ${f.description || 'No description'} (Status: ${f.status || 'unknown'})`)
46
+ .join('\n')
47
+ : 'No features listed.';
43
48
  return `# Growth Analysis Context
44
49
 
45
50
  ## Product Information
@@ -47,22 +52,25 @@ export function formatContextForPrompt(context) {
47
52
  - **Product ID**: ${product.id}
48
53
  - **Description**: ${product.description || 'No description provided'}
49
54
 
55
+ ## Product Features (${product.features?.length || 0})
56
+ ${featuresList}
57
+
50
58
  ## Previous Growth Campaigns (${previousCampaigns.length})
51
59
  ${campaignsList}
52
60
 
53
61
  ---
54
62
 
55
- **Important**: Analyze the product above and create a growth strategy. Each content suggestion must be DIFFERENT from all previous campaigns listed above.`;
63
+ **Important**: Analyze the product above and create a growth strategy. Each content suggestion must be DIFFERENT from all previous campaigns listed above. Use the product name, description, and features to write specific, concrete content — never use placeholder text.`;
56
64
  }
57
65
  /**
58
66
  * Prepare the full analysis prompt with all context
59
67
  */
60
- export async function prepareGrowthAnalysisContext(productId, verbose) {
68
+ export async function prepareGrowthAnalysisContext(productId, verbose, hasCodebase = false) {
61
69
  if (verbose) {
62
70
  logInfo('Fetching growth analysis context via MCP endpoints...');
63
71
  }
64
72
  const context = await fetchGrowthAnalysisContext(productId, verbose);
65
73
  const contextInfo = formatContextForPrompt(context);
66
- const analysisPrompt = createGrowthAnalysisPromptWithContext(productId, contextInfo);
74
+ const analysisPrompt = createGrowthAnalysisPromptWithContext(productId, contextInfo, hasCodebase);
67
75
  return { context, analysisPrompt };
68
76
  }
@@ -3,18 +3,41 @@ import { prepareGrowthAnalysisContext } from './context.js';
3
3
  import { createGrowthAnalysisSystemPrompt } from './prompts.js';
4
4
  import { executeGrowthAnalysisQuery } from './agent.js';
5
5
  import { saveGrowthAnalysis } from '../../api/growth.js';
6
+ import { getGitHubConfigByProduct } from '../../api/github.js';
7
+ import { ensureWorkspaceDir, cloneFeatureRepo, } from '../../workspace/workspace-manager.js';
6
8
  export const analyseGrowth = async (options, config) => {
7
9
  const { productId, verbose } = options;
8
10
  if (verbose) {
9
11
  logInfo(`Starting growth analysis for product ID: ${productId}`);
10
12
  }
11
13
  try {
12
- const { context, analysisPrompt } = await prepareGrowthAnalysisContext(productId, verbose);
13
- const systemPrompt = createGrowthAnalysisSystemPrompt();
14
+ // Clone product repo if GitHub is configured
15
+ let repoCwd;
16
+ try {
17
+ const githubConfig = await getGitHubConfigByProduct(productId, verbose);
18
+ if (githubConfig.configured &&
19
+ githubConfig.token &&
20
+ githubConfig.owner &&
21
+ githubConfig.repo) {
22
+ const workspaceRoot = ensureWorkspaceDir();
23
+ const { repoPath } = cloneFeatureRepo(workspaceRoot, `growth-${productId}`, githubConfig.owner, githubConfig.repo, githubConfig.token);
24
+ repoCwd = repoPath;
25
+ logInfo(`Repository cloned to: ${repoCwd}`);
26
+ }
27
+ else {
28
+ logInfo(`No GitHub repo configured for product, running without codebase access. ${githubConfig.message || ''}`);
29
+ }
30
+ }
31
+ catch (error) {
32
+ logInfo(`Could not clone repo (continuing without codebase): ${error instanceof Error ? error.message : String(error)}`);
33
+ }
34
+ const hasCodebase = !!repoCwd;
35
+ const { context, analysisPrompt } = await prepareGrowthAnalysisContext(productId, verbose, hasCodebase);
36
+ const systemPrompt = createGrowthAnalysisSystemPrompt(hasCodebase);
14
37
  if (verbose) {
15
38
  logInfo('Starting AI query for growth analysis...');
16
39
  }
17
- const analysisResult = await executeGrowthAnalysisQuery(analysisPrompt, systemPrompt, config, verbose);
40
+ const analysisResult = await executeGrowthAnalysisQuery(analysisPrompt, systemPrompt, config, verbose, repoCwd);
18
41
  if (!analysisResult) {
19
42
  throw new Error('No analysis results received');
20
43
  }
@@ -1,2 +1,2 @@
1
- export declare const createGrowthAnalysisSystemPrompt: () => string;
2
- export declare const createGrowthAnalysisPromptWithContext: (productId: string, contextInfo: string) => string;
1
+ export declare const createGrowthAnalysisSystemPrompt: (hasCodebase?: boolean) => string;
2
+ export declare const createGrowthAnalysisPromptWithContext: (productId: string, contextInfo: string, hasCodebase?: boolean) => string;
@@ -1,14 +1,16 @@
1
- export const createGrowthAnalysisSystemPrompt = () => {
1
+ export const createGrowthAnalysisSystemPrompt = (hasCodebase = false) => {
2
2
  return `You are an expert growth marketer and product strategist. Your task is to analyze a product and generate a comprehensive growth strategy with specific, actionable content recommendations for different channels.
3
3
 
4
4
  **Your Role**: Analyze the product, understand its target audience, review previous campaigns to avoid repetition, and create specific content pieces ready to be published on different platforms.
5
5
 
6
- **Analysis Process**:
7
- 1. **Understand the Product**: Analyze the product's purpose, target audience, unique value proposition, and competitive positioning
8
- 2. **Review Previous Campaigns**: Study past campaigns to understand what has been done, which channels were used, and avoid creating duplicate content
9
- 3. **Research Current Trends**: Consider current industry trends, news, and timely topics relevant to the product
10
- 4. **Identify Target Channels**: Recommend the best channels for growth based on the product's audience
11
- 5. **Create Content Suggestions**: Generate specific, ready-to-publish content pieces for each recommended channel
6
+ **Analysis Process**:${hasCodebase ? `
7
+ 1. **Explore the Codebase**: BEFORE writing any content, use your tools to explore the project's codebase. Read README files, CLAUDE.md, package.json, landing pages, documentation, and key source files to deeply understand what the product actually does, its features, technical architecture, and unique selling points. This is CRITICAL for writing specific, accurate content.
8
+ 2. **Understand the Product**: Based on your codebase exploration AND the provided context, build a complete picture of the product's purpose, target audience, unique value proposition, and competitive positioning` : `
9
+ 1. **Understand the Product**: Analyze the product's purpose, target audience, unique value proposition, and competitive positioning from the provided context`}
10
+ ${hasCodebase ? '3' : '2'}. **Review Previous Campaigns**: Study past campaigns to understand what has been done, which channels were used, and avoid creating duplicate content
11
+ ${hasCodebase ? '4' : '3'}. **Research Current Trends**: Consider current industry trends, news, and timely topics relevant to the product
12
+ ${hasCodebase ? '5' : '4'}. **Identify Target Channels**: Recommend the best channels for growth based on the product's audience
13
+ ${hasCodebase ? '6' : '5'}. **Create Content Suggestions**: Generate specific, ready-to-publish content pieces for each recommended channel${hasCodebase ? ', using real details from your codebase exploration' : ''}
12
14
 
13
15
  **Channel Expertise**:
14
16
  - Twitter/X: Short-form content, threads, engagement posts
@@ -30,6 +32,13 @@ export const createGrowthAnalysisSystemPrompt = () => {
30
32
  - Consider timing and relevance to current events
31
33
  - Content must be different from all previous campaigns
32
34
 
35
+ **CRITICAL - NO PLACEHOLDERS**:
36
+ - NEVER use placeholder brackets like [one-line value prop], [core problem], [link], [Screenshot], [Differentiator 1], etc.
37
+ - ALL content must be fully written and ready to publish as-is
38
+ - Use the actual product name, description, and features provided in the context to write concrete, specific content
39
+ - If you don't have enough information for a detail (e.g., a URL or screenshot), either omit that part entirely or write around it naturally
40
+ - The user should be able to copy-paste the content directly to the target platform without any edits
41
+
33
42
  **CRITICAL - Result Format**:
34
43
  You MUST return ONLY a JSON object. Do NOT include any text before or after the JSON.
35
44
 
@@ -67,22 +76,30 @@ You MUST return ONLY a JSON object. Do NOT include any text before or after the
67
76
  - Include a mix of content types (educational, promotional, community-building)
68
77
  - Consider the product's current stage and adjust strategy accordingly`;
69
78
  };
70
- export const createGrowthAnalysisPromptWithContext = (productId, contextInfo) => {
79
+ export const createGrowthAnalysisPromptWithContext = (productId, contextInfo, hasCodebase = false) => {
80
+ const codebaseInstructions = hasCodebase
81
+ ? `
82
+ **Step 1 - Explore the codebase FIRST**: Before writing anything, use your file reading and search tools to explore this project's codebase. Look at:
83
+ - README.md, CLAUDE.md, package.json for project overview
84
+ - Landing pages, marketing copy, or documentation for existing messaging
85
+ - Key source files to understand what the product actually does technically
86
+ - Any configuration or deployment files that reveal the tech stack
87
+
88
+ **Step 2 - Analyze and create strategy**: Using your deep understanding from Step 1 plus the context above:`
89
+ : `
90
+ **Analyze and create strategy**: Using the product context above:`;
71
91
  return `Please conduct comprehensive growth analysis for product ID: ${productId}
72
92
 
73
93
  ${contextInfo}
74
94
 
75
95
  ## Analysis Instructions
96
+ ${codebaseInstructions}
97
+ 1. Understand the product's real value proposition, target users, and competitive advantages
98
+ 2. Study the previous campaigns listed above - DO NOT suggest content that overlaps with what has already been published
99
+ 3. Identify the most effective channels for reaching this product's target audience
100
+ 4. Generate specific, ready-to-publish content pieces using REAL details${hasCodebase ? ' from the codebase (actual feature names, real technical details, concrete benefits)' : ' from the product context above'}
76
101
 
77
- Analyze the product above and create a growth strategy:
78
-
79
- 1. **Product Analysis**: Understand the product's value proposition, target users, and competitive advantages
80
- 2. **Previous Campaigns Review**: Study the previous campaigns listed above - DO NOT suggest content that overlaps with what has already been published
81
- 3. **Channel Strategy**: Identify the most effective channels for reaching this product's target audience
82
- 4. **Content Creation**: Generate specific, ready-to-publish content pieces for each recommended channel
83
- 5. **Differentiation**: Ensure every suggested content piece is fresh and different from previous campaigns
84
-
85
- Focus on actionable, specific content that can be published immediately. Avoid generic advice.
102
+ **CRITICAL**: Every content piece must be ready to copy-paste and publish. Use actual product details, not placeholders.
86
103
 
87
104
  Return ONLY the JSON response as specified in your instructions.`;
88
105
  };
@@ -70,19 +70,19 @@ export async function fetchPRExecutionContext(featureId, verbose) {
70
70
  logInfo(`Fetching PR execution context for feature: ${featureId}`);
71
71
  }
72
72
  const devBranchName = getDevBranchName(featureId);
73
+ // Fetch GitHub config and data in parallel (need token before remote branch check)
74
+ const [feature, pullRequests, githubConfig] = await Promise.all([
75
+ getFeature(featureId, verbose),
76
+ getPullRequests({ featureId, verbose }),
77
+ getGitHubConfig(featureId, verbose),
78
+ ]);
73
79
  // Verify dev branch exists
74
80
  const localExists = branchExists(devBranchName);
75
- const remoteExists = !localExists && remoteBranchExists(devBranchName);
81
+ const remoteExists = !localExists && remoteBranchExists(devBranchName, githubConfig.token);
76
82
  if (!localExists && !remoteExists) {
77
83
  throw new Error(`Development branch '${devBranchName}' does not exist. ` +
78
84
  `The feature must have code on the dev branch before PR execution.`);
79
85
  }
80
- // Fetch GitHub config and data in parallel
81
- const [feature, pullRequests, githubConfig] = await Promise.all([
82
- getFeature(featureId, verbose),
83
- getPullRequests({ featureId, verbose }),
84
- getGitHubConfig(featureId, verbose),
85
- ]);
86
86
  // If branch only exists on remote, fetch it (using credential helper)
87
87
  if (!localExists && remoteExists) {
88
88
  if (verbose) {
@@ -76,20 +76,20 @@ export async function fetchPRSplittingContext(featureId, verbose, replaceExistin
76
76
  logInfo(`Fetching PR splitting context for feature: ${featureId}`);
77
77
  }
78
78
  const devBranchName = getDevBranchName(featureId);
79
- // Verify dev branch exists
80
- const localExists = branchExists(devBranchName);
81
- const remoteExists = !localExists && remoteBranchExists(devBranchName);
82
- if (!localExists && !remoteExists) {
83
- throw new Error(`Development branch '${devBranchName}' does not exist. ` +
84
- `The feature must have code on the dev branch before PR splitting.`);
85
- }
86
- // Fetch database data and GitHub config in parallel
79
+ // Fetch database data and GitHub config in parallel (need token before remote branch check)
87
80
  const [feature, existing_branches, existing_pull_requests, githubConfig] = await Promise.all([
88
81
  getFeature(featureId, verbose),
89
82
  getBranches({ featureId, verbose }).catch(() => []),
90
83
  getPullRequests({ featureId, verbose }).catch(() => []),
91
84
  getGitHubConfig(featureId, verbose),
92
85
  ]);
86
+ // Verify dev branch exists
87
+ const localExists = branchExists(devBranchName);
88
+ const remoteExists = !localExists && remoteBranchExists(devBranchName, githubConfig.token);
89
+ if (!localExists && !remoteExists) {
90
+ throw new Error(`Development branch '${devBranchName}' does not exist. ` +
91
+ `The feature must have code on the dev branch before PR splitting.`);
92
+ }
93
93
  // If branch only exists on remote, fetch it (using credential helper)
94
94
  if (!localExists && remoteExists) {
95
95
  if (verbose) {
@@ -3,8 +3,8 @@
3
3
  * Creates pull requests from feature branches after successful testing
4
4
  */
5
5
  import { Octokit } from '@octokit/rest';
6
- import { execSync } from 'child_process';
7
- import { gitPush } from '../../utils/git-push.js';
6
+ import { execSync, execFileSync } from 'child_process';
7
+ import { gitPush, buildCredentialArgs } from '../../utils/git-push.js';
8
8
  // GitHub PR title best practice: keep under 72 characters
9
9
  const MAX_PR_TITLE_LENGTH = 72;
10
10
  const PR_TITLE_PREFIX = 'feat: ';
@@ -67,7 +67,7 @@ const discardUncommittedChanges = (verbose) => {
67
67
  * @param branch - The branch to switch to
68
68
  * @param verbose - Whether to log verbose output
69
69
  */
70
- const switchToBranch = (branch, verbose) => {
70
+ const switchToBranch = (branch, verbose, githubToken) => {
71
71
  try {
72
72
  // First check if branch exists locally
73
73
  if (!branchExists(branch)) {
@@ -77,7 +77,8 @@ const switchToBranch = (branch, verbose) => {
77
77
  }
78
78
  try {
79
79
  // Fetch to get latest remote refs
80
- execSync('git fetch origin', { encoding: 'utf-8', stdio: 'pipe' });
80
+ const credArgs = buildCredentialArgs(githubToken);
81
+ execFileSync('git', [...credArgs, 'fetch', 'origin'], { encoding: 'utf-8', stdio: 'pipe' });
81
82
  // Check if remote branch exists
82
83
  execSync(`git rev-parse --verify origin/${branch}`, {
83
84
  encoding: 'utf-8',
@@ -230,7 +231,7 @@ export async function createPullRequest(config, feature) {
230
231
  if (verbose) {
231
232
  console.log(`⚠️ Currently on ${baseBranch} branch, switching to ${devBranch}`);
232
233
  }
233
- switchToBranch(devBranch, verbose);
234
+ switchToBranch(devBranch, verbose, githubToken);
234
235
  currentBranch = devBranch;
235
236
  }
236
237
  // Extract feature ID from current branch (dev/feature-id)
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Unit tests for phase quality criteria definitions
3
+ */
4
+ export {};
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Unit tests for phase quality criteria definitions
3
+ */
4
+ import { describe, it } from 'node:test';
5
+ import assert from 'node:assert';
6
+ import { DEFAULT_PHASE_CRITERIA, USER_STORIES_ANALYSIS_CRITERIA, TEST_CASES_ANALYSIS_CRITERIA, TECHNICAL_DESIGN_CRITERIA, BRANCH_PLANNING_CRITERIA, CODE_IMPLEMENTATION_CRITERIA, FUNCTIONAL_TESTING_CRITERIA, CODE_REVIEW_CRITERIA, getPhaseQualityCriteria, } from '../phase-criteria.js';
7
+ describe('Phase Quality Criteria', () => {
8
+ describe('DEFAULT_PHASE_CRITERIA', () => {
9
+ it('should cover all evaluable phases', () => {
10
+ const expectedPhases = [
11
+ 'user_stories_analysis',
12
+ 'test_cases_analysis',
13
+ 'technical_design',
14
+ 'branch_planning',
15
+ 'code_implementation',
16
+ 'functional_testing',
17
+ 'code_review',
18
+ ];
19
+ for (const phase of expectedPhases) {
20
+ assert.ok(phase in DEFAULT_PHASE_CRITERIA, `Should have criteria for phase: ${phase}`);
21
+ }
22
+ });
23
+ it('should have consistent phase names in criteria objects', () => {
24
+ for (const [key, criteria] of Object.entries(DEFAULT_PHASE_CRITERIA)) {
25
+ assert.strictEqual(criteria.phase, key, `Criteria for ${key} should have matching phase name`);
26
+ }
27
+ });
28
+ });
29
+ describe('Criteria Structural Validity', () => {
30
+ const allCriteria = [
31
+ USER_STORIES_ANALYSIS_CRITERIA,
32
+ TEST_CASES_ANALYSIS_CRITERIA,
33
+ TECHNICAL_DESIGN_CRITERIA,
34
+ BRANCH_PLANNING_CRITERIA,
35
+ CODE_IMPLEMENTATION_CRITERIA,
36
+ FUNCTIONAL_TESTING_CRITERIA,
37
+ CODE_REVIEW_CRITERIA,
38
+ ];
39
+ for (const phaseCriteria of allCriteria) {
40
+ describe(phaseCriteria.phase, () => {
41
+ it('should have advanceThreshold > escalateThreshold', () => {
42
+ assert.ok(phaseCriteria.advanceThreshold > phaseCriteria.escalateThreshold, `advanceThreshold (${phaseCriteria.advanceThreshold}) should be > escalateThreshold (${phaseCriteria.escalateThreshold})`);
43
+ });
44
+ it('should have thresholds in valid range (0-100)', () => {
45
+ assert.ok(phaseCriteria.advanceThreshold >= 0);
46
+ assert.ok(phaseCriteria.advanceThreshold <= 100);
47
+ assert.ok(phaseCriteria.escalateThreshold >= 0);
48
+ assert.ok(phaseCriteria.escalateThreshold <= 100);
49
+ });
50
+ it('should have maxAutoRetries >= 1', () => {
51
+ assert.ok(phaseCriteria.maxAutoRetries >= 1, `maxAutoRetries should be >= 1, got ${phaseCriteria.maxAutoRetries}`);
52
+ });
53
+ it('should have at least one criterion', () => {
54
+ assert.ok(phaseCriteria.criteria.length > 0, 'Should have at least one criterion');
55
+ });
56
+ it('should have criteria weights that approximately sum to 1', () => {
57
+ const totalWeight = phaseCriteria.criteria.reduce((sum, c) => sum + c.weight, 0);
58
+ assert.ok(Math.abs(totalWeight - 1.0) < 0.01, `Weights should sum to ~1.0, got ${totalWeight}`);
59
+ });
60
+ it('should have unique criterion IDs', () => {
61
+ const ids = phaseCriteria.criteria.map((c) => c.id);
62
+ const uniqueIds = new Set(ids);
63
+ assert.strictEqual(ids.length, uniqueIds.size, 'Criterion IDs should be unique');
64
+ });
65
+ it('should have valid criterion weights (0 < weight <= 1)', () => {
66
+ for (const criterion of phaseCriteria.criteria) {
67
+ assert.ok(criterion.weight > 0 && criterion.weight <= 1, `Weight for ${criterion.id} should be between 0 and 1, got ${criterion.weight}`);
68
+ }
69
+ });
70
+ it('should have valid minimum scores (0-100)', () => {
71
+ for (const criterion of phaseCriteria.criteria) {
72
+ assert.ok(criterion.minimumScore >= 0 && criterion.minimumScore <= 100, `minimumScore for ${criterion.id} should be 0-100, got ${criterion.minimumScore}`);
73
+ }
74
+ });
75
+ it('should have non-empty evaluation guidance', () => {
76
+ for (const criterion of phaseCriteria.criteria) {
77
+ assert.ok(criterion.evaluationGuidance.length > 0, `Criterion ${criterion.id} should have evaluation guidance`);
78
+ }
79
+ });
80
+ });
81
+ }
82
+ });
83
+ describe('getPhaseQualityCriteria', () => {
84
+ it('should return default criteria for known phases', () => {
85
+ const criteria = getPhaseQualityCriteria('user_stories_analysis');
86
+ assert.ok(criteria);
87
+ assert.strictEqual(criteria.phase, 'user_stories_analysis');
88
+ assert.strictEqual(criteria.advanceThreshold, USER_STORIES_ANALYSIS_CRITERIA.advanceThreshold);
89
+ });
90
+ it('should return null for unknown phases', () => {
91
+ const criteria = getPhaseQualityCriteria('nonexistent_phase');
92
+ assert.strictEqual(criteria, null);
93
+ });
94
+ it('should apply overrides when provided', () => {
95
+ const criteria = getPhaseQualityCriteria('user_stories_analysis', {
96
+ user_stories_analysis: {
97
+ advanceThreshold: 90,
98
+ maxAutoRetries: 5,
99
+ },
100
+ });
101
+ assert.ok(criteria);
102
+ assert.strictEqual(criteria.advanceThreshold, 90);
103
+ assert.strictEqual(criteria.maxAutoRetries, 5);
104
+ // Non-overridden values should remain from defaults
105
+ assert.strictEqual(criteria.escalateThreshold, USER_STORIES_ANALYSIS_CRITERIA.escalateThreshold);
106
+ });
107
+ it('should return defaults when override map does not include the phase', () => {
108
+ const criteria = getPhaseQualityCriteria('technical_design', {
109
+ user_stories_analysis: { advanceThreshold: 90 },
110
+ });
111
+ assert.ok(criteria);
112
+ assert.strictEqual(criteria.advanceThreshold, TECHNICAL_DESIGN_CRITERIA.advanceThreshold);
113
+ });
114
+ it('should override criteria array when provided', () => {
115
+ const customCriteria = [
116
+ {
117
+ id: 'custom_1',
118
+ name: 'Custom',
119
+ description: 'A custom criterion',
120
+ weight: 1.0,
121
+ minimumScore: 50,
122
+ evaluationGuidance: 'Custom guidance',
123
+ },
124
+ ];
125
+ const criteria = getPhaseQualityCriteria('technical_design', {
126
+ technical_design: { criteria: customCriteria },
127
+ });
128
+ assert.ok(criteria);
129
+ assert.strictEqual(criteria.criteria.length, 1);
130
+ assert.strictEqual(criteria.criteria[0].id, 'custom_1');
131
+ });
132
+ });
133
+ });
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Unit tests for lifecycle agent transition rules and decision logic
3
+ */
4
+ export {};