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.
- package/.claude/settings.local.json +3 -23
- package/dist/api/github.d.ts +11 -0
- package/dist/api/github.js +44 -0
- package/dist/index.js +19 -6
- package/dist/phases/growth-analysis/agent.d.ts +1 -1
- package/dist/phases/growth-analysis/agent.js +2 -1
- package/dist/phases/growth-analysis/context.d.ts +1 -1
- package/dist/phases/growth-analysis/context.js +11 -3
- package/dist/phases/growth-analysis/index.js +26 -3
- package/dist/phases/growth-analysis/prompts.d.ts +2 -2
- package/dist/phases/growth-analysis/prompts.js +34 -17
- package/dist/phases/pr-execution/context.js +7 -7
- package/dist/phases/pr-splitting/context.js +8 -8
- package/dist/phases/pull-request/creator.js +6 -5
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +4 -0
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +133 -0
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +4 -0
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +336 -0
- package/dist/services/lifecycle-agent/index.d.ts +24 -0
- package/dist/services/lifecycle-agent/index.js +25 -0
- package/dist/services/lifecycle-agent/phase-criteria.d.ts +57 -0
- package/dist/services/lifecycle-agent/phase-criteria.js +335 -0
- package/dist/services/lifecycle-agent/transition-rules.d.ts +60 -0
- package/dist/services/lifecycle-agent/transition-rules.js +184 -0
- package/dist/services/lifecycle-agent/types.d.ts +190 -0
- package/dist/services/lifecycle-agent/types.js +12 -0
- package/dist/utils/git-branch-manager.js +2 -2
- package/package.json +1 -1
- package/.env.local +0 -12
- package/dist/api/features/__tests__/regression-prevention.test.d.ts +0 -5
- package/dist/api/features/__tests__/regression-prevention.test.js +0 -338
- package/dist/api/features/__tests__/status-updater.integration.test.d.ts +0 -5
- package/dist/api/features/__tests__/status-updater.integration.test.js +0 -497
- package/dist/commands/workflow/pipeline-runner.d.ts +0 -17
- package/dist/commands/workflow/pipeline-runner.js +0 -393
- package/dist/commands/workflow/runner.d.ts +0 -26
- package/dist/commands/workflow/runner.js +0 -119
- package/dist/commands/workflow/workflow-runner.d.ts +0 -26
- package/dist/commands/workflow/workflow-runner.js +0 -119
- package/dist/phases/code-implementation/analyzer-helpers.d.ts +0 -28
- package/dist/phases/code-implementation/analyzer-helpers.js +0 -177
- package/dist/phases/code-implementation/analyzer.d.ts +0 -32
- package/dist/phases/code-implementation/analyzer.js +0 -629
- package/dist/phases/code-implementation/context-fetcher.d.ts +0 -17
- package/dist/phases/code-implementation/context-fetcher.js +0 -86
- package/dist/phases/code-implementation/mcp-server.d.ts +0 -1
- package/dist/phases/code-implementation/mcp-server.js +0 -93
- package/dist/phases/code-implementation/prompts-improvement.d.ts +0 -5
- package/dist/phases/code-implementation/prompts-improvement.js +0 -108
- package/dist/phases/code-implementation-verification/verifier.d.ts +0 -31
- package/dist/phases/code-implementation-verification/verifier.js +0 -196
- package/dist/phases/code-refine/analyzer.d.ts +0 -41
- package/dist/phases/code-refine/analyzer.js +0 -561
- package/dist/phases/code-refine/context-fetcher.d.ts +0 -94
- package/dist/phases/code-refine/context-fetcher.js +0 -423
- package/dist/phases/code-refine-verification/analysis/llm-analyzer.d.ts +0 -22
- package/dist/phases/code-refine-verification/analysis/llm-analyzer.js +0 -134
- package/dist/phases/code-refine-verification/verifier.d.ts +0 -47
- package/dist/phases/code-refine-verification/verifier.js +0 -597
- package/dist/phases/code-review/analyzer.d.ts +0 -29
- package/dist/phases/code-review/analyzer.js +0 -363
- package/dist/phases/code-review/context-fetcher.d.ts +0 -92
- package/dist/phases/code-review/context-fetcher.js +0 -296
- package/dist/phases/feature-analysis/analyzer-helpers.d.ts +0 -10
- package/dist/phases/feature-analysis/analyzer-helpers.js +0 -47
- package/dist/phases/feature-analysis/analyzer.d.ts +0 -11
- package/dist/phases/feature-analysis/analyzer.js +0 -208
- package/dist/phases/feature-analysis/context-fetcher.d.ts +0 -26
- package/dist/phases/feature-analysis/context-fetcher.js +0 -134
- package/dist/phases/feature-analysis/http-fallback.d.ts +0 -20
- package/dist/phases/feature-analysis/http-fallback.js +0 -95
- package/dist/phases/feature-analysis/mcp-server.d.ts +0 -1
- package/dist/phases/feature-analysis/mcp-server.js +0 -144
- package/dist/phases/feature-analysis/prompts-improvement.d.ts +0 -8
- package/dist/phases/feature-analysis/prompts-improvement.js +0 -109
- package/dist/phases/feature-analysis-verification/verifier.d.ts +0 -37
- package/dist/phases/feature-analysis-verification/verifier.js +0 -147
- package/dist/phases/technical-design/analyzer-helpers.d.ts +0 -25
- package/dist/phases/technical-design/analyzer-helpers.js +0 -39
- package/dist/phases/technical-design/analyzer.d.ts +0 -21
- package/dist/phases/technical-design/analyzer.js +0 -461
- package/dist/phases/technical-design/context-fetcher.d.ts +0 -12
- package/dist/phases/technical-design/context-fetcher.js +0 -39
- package/dist/phases/technical-design/http-fallback.d.ts +0 -17
- package/dist/phases/technical-design/http-fallback.js +0 -151
- package/dist/phases/technical-design/mcp-server.d.ts +0 -1
- package/dist/phases/technical-design/mcp-server.js +0 -157
- package/dist/phases/technical-design/prompts-improvement.d.ts +0 -5
- package/dist/phases/technical-design/prompts-improvement.js +0 -93
- package/dist/phases/technical-design-verification/verifier.d.ts +0 -53
- package/dist/phases/technical-design-verification/verifier.js +0 -170
- package/dist/services/feature-branches.d.ts +0 -77
- package/dist/services/feature-branches.js +0 -205
- package/dist/workflow-runner/config/phase-configs.d.ts +0 -5
- package/dist/workflow-runner/config/phase-configs.js +0 -120
- package/dist/workflow-runner/core/feature-filter.d.ts +0 -16
- package/dist/workflow-runner/core/feature-filter.js +0 -46
- package/dist/workflow-runner/core/index.d.ts +0 -8
- package/dist/workflow-runner/core/index.js +0 -12
- package/dist/workflow-runner/core/pipeline-evaluator.d.ts +0 -24
- package/dist/workflow-runner/core/pipeline-evaluator.js +0 -32
- package/dist/workflow-runner/core/state-manager.d.ts +0 -24
- package/dist/workflow-runner/core/state-manager.js +0 -42
- package/dist/workflow-runner/core/workflow-logger.d.ts +0 -20
- package/dist/workflow-runner/core/workflow-logger.js +0 -65
- package/dist/workflow-runner/executors/phase-executor.d.ts +0 -8
- package/dist/workflow-runner/executors/phase-executor.js +0 -248
- package/dist/workflow-runner/feature-workflow-runner.d.ts +0 -26
- package/dist/workflow-runner/feature-workflow-runner.js +0 -119
- package/dist/workflow-runner/index.d.ts +0 -2
- package/dist/workflow-runner/index.js +0 -2
- package/dist/workflow-runner/pipeline-runner.d.ts +0 -17
- package/dist/workflow-runner/pipeline-runner.js +0 -393
- package/dist/workflow-runner/workflow-processor.d.ts +0 -54
- package/dist/workflow-runner/workflow-processor.js +0 -170
|
@@ -1,28 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"
|
|
5
|
-
"Bash(npm run
|
|
6
|
-
|
|
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
|
}
|
package/dist/api/github.d.ts
CHANGED
|
@@ -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
|
package/dist/api/github.js
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
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. **
|
|
8
|
-
2. **
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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,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
|
+
});
|