edsger 0.24.0 → 0.25.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/dist/api/growth.d.ts +59 -0
- package/dist/api/growth.js +50 -0
- package/dist/commands/growth-analysis/index.d.ts +7 -0
- package/dist/commands/growth-analysis/index.js +52 -0
- package/dist/commands/workflow/feature-coordinator.js +3 -1
- package/dist/index.js +7 -0
- package/dist/phases/feature-analysis/index.js +16 -9
- package/dist/phases/feature-analysis/outcome.d.ts +14 -0
- package/dist/phases/feature-analysis/outcome.js +51 -1
- package/dist/phases/feature-analysis/prompts.js +2 -1
- package/dist/phases/growth-analysis/agent.d.ts +2 -0
- package/dist/phases/growth-analysis/agent.js +105 -0
- package/dist/phases/growth-analysis/context.d.ts +21 -0
- package/dist/phases/growth-analysis/context.js +68 -0
- package/dist/phases/growth-analysis/index.d.ts +24 -0
- package/dist/phases/growth-analysis/index.js +53 -0
- package/dist/phases/growth-analysis/prompts.d.ts +2 -0
- package/dist/phases/growth-analysis/prompts.js +88 -0
- package/dist/phases/test-cases-analysis/index.js +15 -7
- package/dist/phases/test-cases-analysis/outcome.d.ts +2 -0
- package/dist/phases/test-cases-analysis/outcome.js +24 -0
- package/dist/phases/test-cases-analysis/prompts.js +1 -0
- package/dist/phases/user-stories-analysis/index.js +15 -7
- package/dist/phases/user-stories-analysis/outcome.d.ts +2 -0
- package/dist/phases/user-stories-analysis/outcome.js +24 -0
- package/dist/phases/user-stories-analysis/prompts.js +1 -0
- package/dist/types/features.d.ts +2 -2
- package/dist/types/index.d.ts +3 -2
- package/dist/utils/formatters.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface GrowthCampaign {
|
|
2
|
+
id: string;
|
|
3
|
+
product_id: string;
|
|
4
|
+
analysis_id: string | null;
|
|
5
|
+
channel: string;
|
|
6
|
+
title: string;
|
|
7
|
+
content: string;
|
|
8
|
+
status: string;
|
|
9
|
+
published_url: string | null;
|
|
10
|
+
published_at: string | null;
|
|
11
|
+
created_at: string;
|
|
12
|
+
updated_at: string;
|
|
13
|
+
}
|
|
14
|
+
export interface GrowthAnalysis {
|
|
15
|
+
id: string;
|
|
16
|
+
product_id: string;
|
|
17
|
+
analysis_content: string;
|
|
18
|
+
target_channels: Array<{
|
|
19
|
+
name: string;
|
|
20
|
+
reason: string;
|
|
21
|
+
audience: string;
|
|
22
|
+
priority: string;
|
|
23
|
+
}>;
|
|
24
|
+
content_suggestions: Array<{
|
|
25
|
+
channel: string;
|
|
26
|
+
title: string;
|
|
27
|
+
content: string;
|
|
28
|
+
rationale: string;
|
|
29
|
+
}>;
|
|
30
|
+
search_context: string | null;
|
|
31
|
+
status: string;
|
|
32
|
+
created_at: string;
|
|
33
|
+
updated_at: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get growth campaigns for a product via MCP
|
|
37
|
+
*/
|
|
38
|
+
export declare function getGrowthCampaigns(productId: string, verbose?: boolean): Promise<GrowthCampaign[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Save a growth analysis result via MCP
|
|
41
|
+
*/
|
|
42
|
+
export declare function saveGrowthAnalysis(analysis: {
|
|
43
|
+
product_id: string;
|
|
44
|
+
analysis_content: string;
|
|
45
|
+
target_channels: Array<{
|
|
46
|
+
name: string;
|
|
47
|
+
reason: string;
|
|
48
|
+
audience: string;
|
|
49
|
+
priority: string;
|
|
50
|
+
}>;
|
|
51
|
+
content_suggestions: Array<{
|
|
52
|
+
channel: string;
|
|
53
|
+
title: string;
|
|
54
|
+
content: string;
|
|
55
|
+
rationale: string;
|
|
56
|
+
}>;
|
|
57
|
+
search_context: string | null;
|
|
58
|
+
status: string;
|
|
59
|
+
}, verbose?: boolean): Promise<GrowthAnalysis | null>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { logInfo, logError } from '../utils/logger.js';
|
|
2
|
+
import { callMcpEndpoint } from './mcp-client.js';
|
|
3
|
+
/**
|
|
4
|
+
* Get growth campaigns for a product via MCP
|
|
5
|
+
*/
|
|
6
|
+
export async function getGrowthCampaigns(productId, verbose) {
|
|
7
|
+
if (verbose) {
|
|
8
|
+
logInfo(`Fetching growth campaigns for product: ${productId}`);
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const result = (await callMcpEndpoint('growth/campaigns', {
|
|
12
|
+
product_id: productId,
|
|
13
|
+
}));
|
|
14
|
+
const text = result.content?.[0]?.text || '[]';
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(text);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (verbose) {
|
|
24
|
+
logError(`Failed to fetch growth campaigns: ${error instanceof Error ? error.message : String(error)}`);
|
|
25
|
+
}
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Save a growth analysis result via MCP
|
|
31
|
+
*/
|
|
32
|
+
export async function saveGrowthAnalysis(analysis, verbose) {
|
|
33
|
+
if (verbose) {
|
|
34
|
+
logInfo(`Saving growth analysis for product: ${analysis.product_id}`);
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const result = (await callMcpEndpoint('growth/save_analysis', analysis));
|
|
38
|
+
const text = result.content?.[0]?.text || 'null';
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(text);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
logError(`Failed to save growth analysis: ${error instanceof Error ? error.message : String(error)}`);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CliOptions } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run AI-powered growth analysis for a product.
|
|
4
|
+
* Analyzes the product, reviews previous campaigns, and generates
|
|
5
|
+
* content suggestions for different channels.
|
|
6
|
+
*/
|
|
7
|
+
export declare const runGrowthAnalysis: (options: CliOptions) => Promise<void>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { logInfo, logError, logSuccess } from '../../utils/logger.js';
|
|
2
|
+
import { validateConfiguration } from '../../utils/validation.js';
|
|
3
|
+
import { analyseGrowth, } from '../../phases/growth-analysis/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Run AI-powered growth analysis for a product.
|
|
6
|
+
* Analyzes the product, reviews previous campaigns, and generates
|
|
7
|
+
* content suggestions for different channels.
|
|
8
|
+
*/
|
|
9
|
+
export const runGrowthAnalysis = async (options) => {
|
|
10
|
+
const productId = options.growthAnalysis;
|
|
11
|
+
if (!productId) {
|
|
12
|
+
throw new Error('Product ID is required for growth analysis');
|
|
13
|
+
}
|
|
14
|
+
const config = validateConfiguration(options);
|
|
15
|
+
logInfo(`Starting growth analysis for product: ${productId}`);
|
|
16
|
+
try {
|
|
17
|
+
const result = await analyseGrowth({
|
|
18
|
+
productId,
|
|
19
|
+
verbose: options.verbose,
|
|
20
|
+
}, config);
|
|
21
|
+
if (result.status === 'success') {
|
|
22
|
+
logSuccess('Growth analysis completed successfully!');
|
|
23
|
+
logInfo(` Recommended channels: ${result.targetChannels.length}`);
|
|
24
|
+
logInfo(` Content suggestions: ${result.contentSuggestions.length}`);
|
|
25
|
+
if (result.analysisId) {
|
|
26
|
+
logInfo(` Analysis saved with ID: ${result.analysisId}`);
|
|
27
|
+
}
|
|
28
|
+
// Display summary
|
|
29
|
+
if (result.targetChannels.length > 0) {
|
|
30
|
+
logInfo('\nRecommended Channels:');
|
|
31
|
+
result.targetChannels.forEach((ch) => {
|
|
32
|
+
logInfo(` [${ch.priority.toUpperCase()}] ${ch.name} - ${ch.reason}`);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (result.contentSuggestions.length > 0) {
|
|
36
|
+
logInfo('\nContent Suggestions:');
|
|
37
|
+
result.contentSuggestions.forEach((s, i) => {
|
|
38
|
+
logInfo(` ${i + 1}. [${s.channel}] ${s.title}`);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
logInfo('\nView full results in the Growth tab of your product dashboard.');
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
logError(`Growth analysis failed: ${result.summary}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
logError(`Growth analysis failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
@@ -13,13 +13,15 @@ import { getFeature } from '../../api/features/get-feature.js';
|
|
|
13
13
|
import { markWorkflowPhaseCompleted } from '../../api/features/update-feature.js';
|
|
14
14
|
import { logError, logInfo, logWarning } from '../../utils/logger.js';
|
|
15
15
|
import { logPhaseResult } from '../../utils/pipeline-logger.js';
|
|
16
|
-
import { runFeatureAnalysisPhase, runTechnicalDesignPhase, runBranchPlanningPhase, runCodeImplementationPhase, runFunctionalTestingPhase, runCodeRefinePhase, runCodeReviewPhase, runAutonomousPhase, } from './executors/phase-executor.js';
|
|
16
|
+
import { runFeatureAnalysisPhase, runUserStoriesAnalysisPhase, runTestCasesAnalysisPhase, runTechnicalDesignPhase, runBranchPlanningPhase, runCodeImplementationPhase, runFunctionalTestingPhase, runCodeRefinePhase, runCodeReviewPhase, runAutonomousPhase, } from './executors/phase-executor.js';
|
|
17
17
|
/**
|
|
18
18
|
* Map workflow phase names (underscore format) to phase runner functions
|
|
19
19
|
* Note: code_refine includes built-in verification loop (similar to technical_design)
|
|
20
20
|
*/
|
|
21
21
|
const PHASE_RUNNERS = {
|
|
22
22
|
feature_analysis: runFeatureAnalysisPhase,
|
|
23
|
+
user_stories_analysis: runUserStoriesAnalysisPhase,
|
|
24
|
+
test_cases_analysis: runTestCasesAnalysisPhase,
|
|
23
25
|
technical_design: runTechnicalDesignPhase,
|
|
24
26
|
branch_planning: runBranchPlanningPhase,
|
|
25
27
|
code_implementation: runCodeImplementationPhase,
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { runWorkflow } from './commands/workflow/index.js';
|
|
|
9
9
|
import { runCodeReview } from './commands/code-review/index.js';
|
|
10
10
|
import { runRefactor } from './commands/refactor/refactor.js';
|
|
11
11
|
import { runInit } from './commands/init/index.js';
|
|
12
|
+
import { runGrowthAnalysis } from './commands/growth-analysis/index.js';
|
|
12
13
|
// Get package.json version dynamically
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
15
|
const __dirname = dirname(__filename);
|
|
@@ -28,6 +29,7 @@ program
|
|
|
28
29
|
.option('-f, --files <patterns...>', 'Review specific file patterns')
|
|
29
30
|
.option('--refactor', 'Refactor code in current directory')
|
|
30
31
|
.option('--init', 'Initialize .edsger directory with project templates')
|
|
32
|
+
.option('--growth-analysis <productId>', 'Run AI-powered growth analysis for a product')
|
|
31
33
|
.option('-c, --config <path>', 'Path to config file')
|
|
32
34
|
.option('-v, --verbose', 'Verbose output');
|
|
33
35
|
program.action(async (options) => {
|
|
@@ -45,6 +47,11 @@ export const runEdsger = async (options) => {
|
|
|
45
47
|
await runInit(options);
|
|
46
48
|
return;
|
|
47
49
|
}
|
|
50
|
+
// Handle growth analysis mode
|
|
51
|
+
if (options.growthAnalysis) {
|
|
52
|
+
await runGrowthAnalysis(options);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
48
55
|
// Handle refactor mode
|
|
49
56
|
if (options.refactor) {
|
|
50
57
|
await runRefactor(options);
|
|
@@ -3,7 +3,7 @@ import { prepareAnalysisContext } from './context.js';
|
|
|
3
3
|
import { createFeatureAnalysisSystemPrompt } from './prompts.js';
|
|
4
4
|
import { executeAnalysisQuery } from './agent.js';
|
|
5
5
|
import { performVerificationCycle } from '../feature-analysis-verification/index.js';
|
|
6
|
-
import { deleteArtifacts, deleteSpecificArtifacts, updateArtifactsToReady, saveAnalysisArtifactsAsDraft, buildAnalysisResult, } from './outcome.js';
|
|
6
|
+
import { deleteArtifacts, deleteSpecificArtifacts, updateArtifactsToReady, saveAnalysisArtifactsAsDraft, buildAnalysisResult, resetReadyArtifactsToDraft, getAllDraftArtifactIds, } from './outcome.js';
|
|
7
7
|
import { logFeaturePhaseEvent } from '../../services/audit-logs.js';
|
|
8
8
|
export const analyseFeature = async (options, config, checklistContext) => {
|
|
9
9
|
const { featureId, verbose } = options;
|
|
@@ -11,6 +11,11 @@ export const analyseFeature = async (options, config, checklistContext) => {
|
|
|
11
11
|
logInfo(`Starting feature analysis for feature ID: ${featureId}`);
|
|
12
12
|
}
|
|
13
13
|
try {
|
|
14
|
+
// Reset ready artifacts to draft so AI can manage them on re-run
|
|
15
|
+
const resetResult = await resetReadyArtifactsToDraft(featureId, verbose);
|
|
16
|
+
if (verbose && (resetResult.resetUserStories > 0 || resetResult.resetTestCases > 0)) {
|
|
17
|
+
logInfo(`✅ Reset ${resetResult.resetUserStories} user stories and ${resetResult.resetTestCases} test cases to draft for re-analysis`);
|
|
18
|
+
}
|
|
14
19
|
const context = await prepareAnalysisContext(featureId, checklistContext, verbose);
|
|
15
20
|
const systemPrompt = createFeatureAnalysisSystemPrompt();
|
|
16
21
|
const initialAnalysisPrompt = context.analysisPrompt;
|
|
@@ -81,12 +86,13 @@ export const analyseFeature = async (options, config, checklistContext) => {
|
|
|
81
86
|
// Perform verification cycle
|
|
82
87
|
const verificationCycle = await performVerificationCycle(structuredAnalysisResult, checklistContext || null, context.featureContext, config, currentIteration, maxIterations, featureId, verbose);
|
|
83
88
|
verificationResult = verificationCycle.verificationResult;
|
|
84
|
-
// If verification passed, update artifacts to ready and exit
|
|
89
|
+
// If verification passed, update ALL remaining draft artifacts to ready and exit
|
|
85
90
|
if (verificationCycle.passed) {
|
|
86
91
|
if (verbose) {
|
|
87
|
-
logInfo('✅ Verification passed! Updating artifacts to ready status...');
|
|
92
|
+
logInfo('✅ Verification passed! Updating all draft artifacts to ready status...');
|
|
88
93
|
}
|
|
89
|
-
await
|
|
94
|
+
const allDrafts = await getAllDraftArtifactIds(featureId, verbose);
|
|
95
|
+
await updateArtifactsToReady(allDrafts.userStoryIds, allDrafts.testCaseIds, verbose);
|
|
90
96
|
break;
|
|
91
97
|
}
|
|
92
98
|
// Verification failed
|
|
@@ -112,16 +118,17 @@ export const analyseFeature = async (options, config, checklistContext) => {
|
|
|
112
118
|
if (!structuredAnalysisResult) {
|
|
113
119
|
throw new Error('No analysis results received');
|
|
114
120
|
}
|
|
115
|
-
// If no checklist was used, update draft artifacts to ready now
|
|
121
|
+
// If no checklist was used, update all draft artifacts to ready now
|
|
116
122
|
if (!checklistContext ||
|
|
117
123
|
checklistContext.checklists.length === 0 ||
|
|
118
124
|
!verificationResult) {
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
const allDrafts = await getAllDraftArtifactIds(featureId, verbose);
|
|
126
|
+
if (allDrafts.userStoryIds.length > 0 ||
|
|
127
|
+
allDrafts.testCaseIds.length > 0) {
|
|
121
128
|
if (verbose) {
|
|
122
|
-
logInfo('✅ No checklist verification needed. Updating artifacts to ready status...');
|
|
129
|
+
logInfo('✅ No checklist verification needed. Updating all draft artifacts to ready status...');
|
|
123
130
|
}
|
|
124
|
-
await updateArtifactsToReady(
|
|
131
|
+
await updateArtifactsToReady(allDrafts.userStoryIds, allDrafts.testCaseIds, verbose);
|
|
125
132
|
}
|
|
126
133
|
}
|
|
127
134
|
// Check if verification failed after all iterations
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { FeatureAnalysisResult } from '../../types/index.js';
|
|
2
2
|
import { FeatureAnalysisContext } from './context.js';
|
|
3
|
+
/**
|
|
4
|
+
* Reset ready user stories and test cases to draft for re-analysis
|
|
5
|
+
*/
|
|
6
|
+
export declare function resetReadyArtifactsToDraft(featureId: string, verbose?: boolean): Promise<{
|
|
7
|
+
resetUserStories: number;
|
|
8
|
+
resetTestCases: number;
|
|
9
|
+
}>;
|
|
10
|
+
/**
|
|
11
|
+
* Get all draft artifact IDs for a feature
|
|
12
|
+
*/
|
|
13
|
+
export declare function getAllDraftArtifactIds(featureId: string, verbose?: boolean): Promise<{
|
|
14
|
+
userStoryIds: string[];
|
|
15
|
+
testCaseIds: string[];
|
|
16
|
+
}>;
|
|
3
17
|
/**
|
|
4
18
|
* Delete artifacts after verification failure
|
|
5
19
|
*/
|
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
import { logInfo, logError } from '../../utils/logger.js';
|
|
2
2
|
import { batchUpdateUserStoryStatus, batchUpdateTestCaseStatus, batchDeleteUserStories, batchDeleteTestCases, getUserStories, getTestCases, } from '../../api/features/index.js';
|
|
3
3
|
import { callMcpEndpoint } from '../../api/mcp-client.js';
|
|
4
|
+
/**
|
|
5
|
+
* Reset ready user stories and test cases to draft for re-analysis
|
|
6
|
+
*/
|
|
7
|
+
export async function resetReadyArtifactsToDraft(featureId, verbose) {
|
|
8
|
+
const [stories, testCases] = await Promise.all([
|
|
9
|
+
getUserStories(featureId, false),
|
|
10
|
+
getTestCases(featureId, false),
|
|
11
|
+
]);
|
|
12
|
+
const readyStoryIds = stories
|
|
13
|
+
.filter((s) => s.status === 'ready')
|
|
14
|
+
.map((s) => s.id);
|
|
15
|
+
const readyTestCaseIds = testCases
|
|
16
|
+
.filter((tc) => tc.status === 'ready')
|
|
17
|
+
.map((tc) => tc.id);
|
|
18
|
+
if (readyStoryIds.length > 0) {
|
|
19
|
+
if (verbose) {
|
|
20
|
+
logInfo(`🔄 Resetting ${readyStoryIds.length} ready user stories to draft for re-analysis...`);
|
|
21
|
+
}
|
|
22
|
+
await batchUpdateUserStoryStatus(readyStoryIds, 'draft', verbose);
|
|
23
|
+
}
|
|
24
|
+
if (readyTestCaseIds.length > 0) {
|
|
25
|
+
if (verbose) {
|
|
26
|
+
logInfo(`🔄 Resetting ${readyTestCaseIds.length} ready test cases to draft for re-analysis...`);
|
|
27
|
+
}
|
|
28
|
+
await batchUpdateTestCaseStatus(readyTestCaseIds, 'draft', verbose);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
resetUserStories: readyStoryIds.length,
|
|
32
|
+
resetTestCases: readyTestCaseIds.length,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get all draft artifact IDs for a feature
|
|
37
|
+
*/
|
|
38
|
+
export async function getAllDraftArtifactIds(featureId, verbose) {
|
|
39
|
+
const [stories, testCases] = await Promise.all([
|
|
40
|
+
getUserStories(featureId, false),
|
|
41
|
+
getTestCases(featureId, false),
|
|
42
|
+
]);
|
|
43
|
+
const userStoryIds = stories
|
|
44
|
+
.filter((s) => s.status === 'draft')
|
|
45
|
+
.map((s) => s.id);
|
|
46
|
+
const testCaseIds = testCases
|
|
47
|
+
.filter((tc) => tc.status === 'draft')
|
|
48
|
+
.map((tc) => tc.id);
|
|
49
|
+
if (verbose) {
|
|
50
|
+
logInfo(`Found ${userStoryIds.length} draft user stories and ${testCaseIds.length} draft test cases`);
|
|
51
|
+
}
|
|
52
|
+
return { userStoryIds, testCaseIds };
|
|
53
|
+
}
|
|
4
54
|
/**
|
|
5
55
|
* Delete artifacts after verification failure
|
|
6
56
|
*/
|
|
@@ -180,7 +230,7 @@ export function buildAnalysisResult(featureId, context, structuredAnalysisResult
|
|
|
180
230
|
featureInfo: context.feature,
|
|
181
231
|
existingUserStories: context.existing_user_stories.map((story) => ({
|
|
182
232
|
...story,
|
|
183
|
-
status: (story.status === 'ready' ? 'ready' : 'draft'),
|
|
233
|
+
status: (story.status === 'ready' ? 'ready' : story.status === 'approved' ? 'approved' : 'draft'),
|
|
184
234
|
created_at: story.created_at || new Date().toISOString(),
|
|
185
235
|
updated_at: story.updated_at || new Date().toISOString(),
|
|
186
236
|
})),
|
|
@@ -79,9 +79,10 @@ When you receive feedback requesting deletion (e.g., "Remove duplicated test cas
|
|
|
79
79
|
|
|
80
80
|
**IMPORTANT DELETION RESTRICTIONS**:
|
|
81
81
|
- You can ONLY delete artifacts with status 'draft'
|
|
82
|
-
- Artifacts with other statuses (ready, in_progress, done, cancelled) CANNOT be deleted
|
|
82
|
+
- Artifacts with other statuses (ready, approved, in_progress, done, cancelled) CANNOT be deleted
|
|
83
83
|
- Only artifacts marked with [DELETABLE] in the context can be deleted
|
|
84
84
|
- Each deletable artifact includes an "ID:" field with the UUID
|
|
85
|
+
- You MUST NOT delete, recreate, or modify artifacts marked with [APPROVED - DO NOT MODIFY]. These have been human-approved and are immutable. Do not create new items that duplicate approved ones.
|
|
85
86
|
|
|
86
87
|
To delete artifacts:
|
|
87
88
|
1. Find the exact UUID from the "ID:" field in the context (e.g., "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { DEFAULT_MODEL } from '../../constants.js';
|
|
3
|
+
import { logInfo, logError } from '../../utils/logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* Parse JSON result from Claude Code response
|
|
6
|
+
*/
|
|
7
|
+
function parseGrowthResult(responseText) {
|
|
8
|
+
try {
|
|
9
|
+
let jsonResult = null;
|
|
10
|
+
// First try to extract JSON from markdown code block
|
|
11
|
+
const jsonBlockMatch = responseText.match(/```json\s*\n([\s\S]*?)\n\s*```/);
|
|
12
|
+
if (jsonBlockMatch) {
|
|
13
|
+
jsonResult = JSON.parse(jsonBlockMatch[1]);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
// Try to parse the entire response as JSON
|
|
17
|
+
jsonResult = JSON.parse(responseText);
|
|
18
|
+
}
|
|
19
|
+
if (jsonResult && jsonResult.analysis) {
|
|
20
|
+
return { analysis: jsonResult.analysis };
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
return { error: 'Invalid JSON structure' };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return {
|
|
28
|
+
error: `JSON parsing failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function userMessage(content) {
|
|
33
|
+
return {
|
|
34
|
+
type: 'user',
|
|
35
|
+
message: { role: 'user', content: content },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function* prompt(analysisPrompt) {
|
|
39
|
+
yield userMessage(analysisPrompt);
|
|
40
|
+
}
|
|
41
|
+
export async function executeGrowthAnalysisQuery(currentPrompt, systemPrompt, config, verbose) {
|
|
42
|
+
let lastAssistantResponse = '';
|
|
43
|
+
let structuredResult = null;
|
|
44
|
+
for await (const message of query({
|
|
45
|
+
prompt: prompt(currentPrompt),
|
|
46
|
+
options: {
|
|
47
|
+
systemPrompt: {
|
|
48
|
+
type: 'preset',
|
|
49
|
+
preset: 'claude_code',
|
|
50
|
+
append: systemPrompt,
|
|
51
|
+
},
|
|
52
|
+
model: DEFAULT_MODEL,
|
|
53
|
+
maxTurns: 1000,
|
|
54
|
+
permissionMode: 'bypassPermissions',
|
|
55
|
+
},
|
|
56
|
+
})) {
|
|
57
|
+
if (verbose) {
|
|
58
|
+
logInfo(`Received message type: ${message.type}`);
|
|
59
|
+
}
|
|
60
|
+
if (message.type === 'assistant' && message.message?.content) {
|
|
61
|
+
for (const content of message.message.content) {
|
|
62
|
+
if (content.type === 'text') {
|
|
63
|
+
lastAssistantResponse += content.text + '\n';
|
|
64
|
+
if (verbose) {
|
|
65
|
+
console.log(`\n${content.text}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (content.type === 'tool_use') {
|
|
69
|
+
if (verbose) {
|
|
70
|
+
console.log(`\n${content.name}: ${content.input.description || 'Running...'}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (message.type === 'result') {
|
|
76
|
+
if (message.subtype === 'success') {
|
|
77
|
+
logInfo('\nGrowth analysis completed, parsing results...');
|
|
78
|
+
const responseText = message.result || lastAssistantResponse;
|
|
79
|
+
const parsed = parseGrowthResult(responseText);
|
|
80
|
+
if (parsed.error) {
|
|
81
|
+
logError(`Failed to parse growth analysis result: ${parsed.error}`);
|
|
82
|
+
structuredResult = {
|
|
83
|
+
status: 'error',
|
|
84
|
+
analysis_content: 'Failed to parse analysis results',
|
|
85
|
+
target_channels: [],
|
|
86
|
+
content_suggestions: [],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
structuredResult = parsed.analysis;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
logError(`\nAnalysis incomplete: ${message.subtype}`);
|
|
95
|
+
if (lastAssistantResponse) {
|
|
96
|
+
const parsed = parseGrowthResult(lastAssistantResponse);
|
|
97
|
+
if (!parsed.error) {
|
|
98
|
+
structuredResult = parsed.analysis;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return structuredResult;
|
|
105
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ProductInfo } from '../../api/products.js';
|
|
2
|
+
import { type GrowthCampaign } from '../../api/growth.js';
|
|
3
|
+
export interface GrowthAnalysisContext {
|
|
4
|
+
product: ProductInfo;
|
|
5
|
+
previousCampaigns: GrowthCampaign[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Fetch all context needed for growth analysis via MCP endpoints
|
|
9
|
+
*/
|
|
10
|
+
export declare function fetchGrowthAnalysisContext(productId: string, verbose?: boolean): Promise<GrowthAnalysisContext>;
|
|
11
|
+
/**
|
|
12
|
+
* Format context into a prompt string for the AI
|
|
13
|
+
*/
|
|
14
|
+
export declare function formatContextForPrompt(context: GrowthAnalysisContext): string;
|
|
15
|
+
/**
|
|
16
|
+
* Prepare the full analysis prompt with all context
|
|
17
|
+
*/
|
|
18
|
+
export declare function prepareGrowthAnalysisContext(productId: string, verbose?: boolean): Promise<{
|
|
19
|
+
context: GrowthAnalysisContext;
|
|
20
|
+
analysisPrompt: string;
|
|
21
|
+
}>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { logInfo, logError } from '../../utils/logger.js';
|
|
2
|
+
import { getProduct } from '../../api/products.js';
|
|
3
|
+
import { getGrowthCampaigns, } from '../../api/growth.js';
|
|
4
|
+
import { createGrowthAnalysisPromptWithContext } from './prompts.js';
|
|
5
|
+
/**
|
|
6
|
+
* Fetch all context needed for growth analysis via MCP endpoints
|
|
7
|
+
*/
|
|
8
|
+
export async function fetchGrowthAnalysisContext(productId, verbose) {
|
|
9
|
+
try {
|
|
10
|
+
if (verbose) {
|
|
11
|
+
logInfo(`Fetching growth analysis context for product: ${productId}`);
|
|
12
|
+
}
|
|
13
|
+
const [product, previousCampaigns] = await Promise.all([
|
|
14
|
+
getProduct(productId, verbose),
|
|
15
|
+
getGrowthCampaigns(productId, verbose),
|
|
16
|
+
]);
|
|
17
|
+
if (verbose) {
|
|
18
|
+
logInfo(`Growth analysis context fetched:`);
|
|
19
|
+
logInfo(` Product: ${product.name}`);
|
|
20
|
+
logInfo(` Previous campaigns: ${previousCampaigns.length}`);
|
|
21
|
+
}
|
|
22
|
+
return { product, previousCampaigns };
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
26
|
+
logError(`Failed to fetch growth analysis context: ${errorMessage}`);
|
|
27
|
+
throw new Error(`Context fetch failed: ${errorMessage}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Format context into a prompt string for the AI
|
|
32
|
+
*/
|
|
33
|
+
export function formatContextForPrompt(context) {
|
|
34
|
+
const { product, previousCampaigns } = context;
|
|
35
|
+
const campaignsList = previousCampaigns.length > 0
|
|
36
|
+
? previousCampaigns
|
|
37
|
+
.map((c, i) => `${i + 1}. **${c.title}** [${c.channel}] (${c.status})
|
|
38
|
+
${c.content.substring(0, 300)}${c.content.length > 300 ? '...' : ''}
|
|
39
|
+
${c.published_url ? `Published: ${c.published_url}` : 'Not yet published'}
|
|
40
|
+
Created: ${c.created_at}`)
|
|
41
|
+
.join('\n\n')
|
|
42
|
+
: 'No previous campaigns.';
|
|
43
|
+
return `# Growth Analysis Context
|
|
44
|
+
|
|
45
|
+
## Product Information
|
|
46
|
+
- **Name**: ${product.name}
|
|
47
|
+
- **Product ID**: ${product.id}
|
|
48
|
+
- **Description**: ${product.description || 'No description provided'}
|
|
49
|
+
|
|
50
|
+
## Previous Growth Campaigns (${previousCampaigns.length})
|
|
51
|
+
${campaignsList}
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
**Important**: Analyze the product above and create a growth strategy. Each content suggestion must be DIFFERENT from all previous campaigns listed above.`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Prepare the full analysis prompt with all context
|
|
59
|
+
*/
|
|
60
|
+
export async function prepareGrowthAnalysisContext(productId, verbose) {
|
|
61
|
+
if (verbose) {
|
|
62
|
+
logInfo('Fetching growth analysis context via MCP endpoints...');
|
|
63
|
+
}
|
|
64
|
+
const context = await fetchGrowthAnalysisContext(productId, verbose);
|
|
65
|
+
const contextInfo = formatContextForPrompt(context);
|
|
66
|
+
const analysisPrompt = createGrowthAnalysisPromptWithContext(productId, contextInfo);
|
|
67
|
+
return { context, analysisPrompt };
|
|
68
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { EdsgerConfig } from '../../types/index.js';
|
|
2
|
+
export interface GrowthAnalysisOptions {
|
|
3
|
+
productId: string;
|
|
4
|
+
verbose?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface GrowthAnalysisResult {
|
|
7
|
+
productId: string;
|
|
8
|
+
status: 'success' | 'error';
|
|
9
|
+
summary: string;
|
|
10
|
+
analysisId?: string;
|
|
11
|
+
targetChannels: Array<{
|
|
12
|
+
name: string;
|
|
13
|
+
reason: string;
|
|
14
|
+
audience: string;
|
|
15
|
+
priority: string;
|
|
16
|
+
}>;
|
|
17
|
+
contentSuggestions: Array<{
|
|
18
|
+
channel: string;
|
|
19
|
+
title: string;
|
|
20
|
+
content: string;
|
|
21
|
+
rationale: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export declare const analyseGrowth: (options: GrowthAnalysisOptions, config: EdsgerConfig) => Promise<GrowthAnalysisResult>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { logInfo, logError } from '../../utils/logger.js';
|
|
2
|
+
import { prepareGrowthAnalysisContext } from './context.js';
|
|
3
|
+
import { createGrowthAnalysisSystemPrompt } from './prompts.js';
|
|
4
|
+
import { executeGrowthAnalysisQuery } from './agent.js';
|
|
5
|
+
import { saveGrowthAnalysis } from '../../api/growth.js';
|
|
6
|
+
export const analyseGrowth = async (options, config) => {
|
|
7
|
+
const { productId, verbose } = options;
|
|
8
|
+
if (verbose) {
|
|
9
|
+
logInfo(`Starting growth analysis for product ID: ${productId}`);
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const { context, analysisPrompt } = await prepareGrowthAnalysisContext(productId, verbose);
|
|
13
|
+
const systemPrompt = createGrowthAnalysisSystemPrompt();
|
|
14
|
+
if (verbose) {
|
|
15
|
+
logInfo('Starting AI query for growth analysis...');
|
|
16
|
+
}
|
|
17
|
+
const analysisResult = await executeGrowthAnalysisQuery(analysisPrompt, systemPrompt, config, verbose);
|
|
18
|
+
if (!analysisResult) {
|
|
19
|
+
throw new Error('No analysis results received');
|
|
20
|
+
}
|
|
21
|
+
// Save analysis result via MCP
|
|
22
|
+
if (verbose) {
|
|
23
|
+
logInfo('Saving growth analysis results...');
|
|
24
|
+
}
|
|
25
|
+
const savedAnalysis = await saveGrowthAnalysis({
|
|
26
|
+
product_id: productId,
|
|
27
|
+
analysis_content: analysisResult.analysis_content || '',
|
|
28
|
+
target_channels: analysisResult.target_channels || [],
|
|
29
|
+
content_suggestions: analysisResult.content_suggestions || [],
|
|
30
|
+
search_context: analysisResult.search_context || null,
|
|
31
|
+
status: 'completed',
|
|
32
|
+
}, verbose);
|
|
33
|
+
logInfo(`Growth analysis completed: ${analysisResult.target_channels?.length || 0} channels, ${analysisResult.content_suggestions?.length || 0} suggestions`);
|
|
34
|
+
return {
|
|
35
|
+
productId,
|
|
36
|
+
status: 'success',
|
|
37
|
+
summary: analysisResult.analysis_content || 'Analysis completed',
|
|
38
|
+
analysisId: savedAnalysis?.id,
|
|
39
|
+
targetChannels: analysisResult.target_channels || [],
|
|
40
|
+
contentSuggestions: analysisResult.content_suggestions || [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
logError(`Growth analysis failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
45
|
+
return {
|
|
46
|
+
productId,
|
|
47
|
+
status: 'error',
|
|
48
|
+
summary: `Analysis failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
49
|
+
targetChannels: [],
|
|
50
|
+
contentSuggestions: [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export const createGrowthAnalysisSystemPrompt = () => {
|
|
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
|
+
|
|
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
|
+
|
|
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
|
|
12
|
+
|
|
13
|
+
**Channel Expertise**:
|
|
14
|
+
- Twitter/X: Short-form content, threads, engagement posts
|
|
15
|
+
- Reddit: Community discussions, valuable contributions, AMAs
|
|
16
|
+
- Hacker News: Technical deep-dives, Show HN posts, thoughtful comments
|
|
17
|
+
- LinkedIn: Professional insights, case studies, thought leadership
|
|
18
|
+
- Blog: SEO-optimized articles, tutorials, product updates
|
|
19
|
+
- Product Hunt: Launch posts, feature announcements
|
|
20
|
+
- Dev.to: Technical tutorials, developer-focused content
|
|
21
|
+
- Discord/Slack: Community engagement, support channels
|
|
22
|
+
- Email Newsletter: Subscriber updates, exclusive content
|
|
23
|
+
- YouTube: Video tutorials, demos, product showcases
|
|
24
|
+
|
|
25
|
+
**Content Quality Standards**:
|
|
26
|
+
- Each content piece should be specific, not generic
|
|
27
|
+
- Content should provide genuine value to the audience
|
|
28
|
+
- Adapt tone and format to each channel's culture
|
|
29
|
+
- Include specific hooks, headlines, or opening lines
|
|
30
|
+
- Consider timing and relevance to current events
|
|
31
|
+
- Content must be different from all previous campaigns
|
|
32
|
+
|
|
33
|
+
**CRITICAL - Result Format**:
|
|
34
|
+
You MUST return ONLY a JSON object. Do NOT include any text before or after the JSON.
|
|
35
|
+
|
|
36
|
+
\`\`\`json
|
|
37
|
+
{
|
|
38
|
+
"analysis": {
|
|
39
|
+
"product_id": "PRODUCT_ID",
|
|
40
|
+
"status": "success",
|
|
41
|
+
"analysis_content": "Comprehensive analysis summary including product positioning, target audience insights, and growth strategy overview",
|
|
42
|
+
"target_channels": [
|
|
43
|
+
{
|
|
44
|
+
"name": "Channel name (e.g., twitter, reddit, hackernews, linkedin, blog, producthunt, devto, discord, email, youtube)",
|
|
45
|
+
"reason": "Why this channel is recommended for this product",
|
|
46
|
+
"audience": "Description of the target audience on this channel",
|
|
47
|
+
"priority": "high|medium|low"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"content_suggestions": [
|
|
51
|
+
{
|
|
52
|
+
"channel": "channel_name",
|
|
53
|
+
"title": "Content title or headline",
|
|
54
|
+
"content": "The full content piece ready to be published or adapted",
|
|
55
|
+
"rationale": "Why this content will resonate with the audience on this channel"
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"search_context": "Summary of current trends and context that informed this analysis"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
\`\`\`
|
|
62
|
+
|
|
63
|
+
**Important Guidelines**:
|
|
64
|
+
- Generate at least 3-5 content suggestions across different channels
|
|
65
|
+
- Prioritize channels with high-priority ratings
|
|
66
|
+
- Each content piece should be substantially different from previous campaigns
|
|
67
|
+
- Include a mix of content types (educational, promotional, community-building)
|
|
68
|
+
- Consider the product's current stage and adjust strategy accordingly`;
|
|
69
|
+
};
|
|
70
|
+
export const createGrowthAnalysisPromptWithContext = (productId, contextInfo) => {
|
|
71
|
+
return `Please conduct comprehensive growth analysis for product ID: ${productId}
|
|
72
|
+
|
|
73
|
+
${contextInfo}
|
|
74
|
+
|
|
75
|
+
## Analysis Instructions
|
|
76
|
+
|
|
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.
|
|
86
|
+
|
|
87
|
+
Return ONLY the JSON response as specified in your instructions.`;
|
|
88
|
+
};
|
|
@@ -3,7 +3,7 @@ import { prepareTestCasesAnalysisContext } from './context.js';
|
|
|
3
3
|
import { createTestCasesAnalysisSystemPrompt } from './prompts.js';
|
|
4
4
|
import { executeTestCasesAnalysisQuery } from './agent.js';
|
|
5
5
|
import { performVerificationCycle } from '../feature-analysis-verification/index.js';
|
|
6
|
-
import { deleteTestCaseArtifacts, deleteSpecificTestCases, updateTestCasesToReady, saveTestCasesAsDraft, buildTestCasesAnalysisResult, } from './outcome.js';
|
|
6
|
+
import { deleteTestCaseArtifacts, deleteSpecificTestCases, updateTestCasesToReady, saveTestCasesAsDraft, buildTestCasesAnalysisResult, resetReadyTestCasesToDraft, getAllDraftTestCaseIds, } from './outcome.js';
|
|
7
7
|
import { logFeaturePhaseEvent } from '../../services/audit-logs.js';
|
|
8
8
|
export const analyseTestCases = async (options, config, checklistContext) => {
|
|
9
9
|
const { featureId, verbose } = options;
|
|
@@ -11,6 +11,11 @@ export const analyseTestCases = async (options, config, checklistContext) => {
|
|
|
11
11
|
logInfo(`Starting test cases analysis for feature ID: ${featureId}`);
|
|
12
12
|
}
|
|
13
13
|
try {
|
|
14
|
+
// Reset ready test cases to draft so AI can manage them on re-run
|
|
15
|
+
const resetCount = await resetReadyTestCasesToDraft(featureId, verbose);
|
|
16
|
+
if (verbose && resetCount > 0) {
|
|
17
|
+
logInfo(`✅ Reset ${resetCount} ready test cases to draft for re-analysis`);
|
|
18
|
+
}
|
|
14
19
|
const context = await prepareTestCasesAnalysisContext(featureId, checklistContext, verbose);
|
|
15
20
|
const systemPrompt = createTestCasesAnalysisSystemPrompt();
|
|
16
21
|
const initialAnalysisPrompt = context.analysisPrompt;
|
|
@@ -78,9 +83,11 @@ export const analyseTestCases = async (options, config, checklistContext) => {
|
|
|
78
83
|
verificationResult = verificationCycle.verificationResult;
|
|
79
84
|
if (verificationCycle.passed) {
|
|
80
85
|
if (verbose) {
|
|
81
|
-
logInfo('✅ Verification passed! Updating test cases to ready status...');
|
|
86
|
+
logInfo('✅ Verification passed! Updating all draft test cases to ready status...');
|
|
82
87
|
}
|
|
83
|
-
|
|
88
|
+
// Update ALL remaining draft test cases (both kept old ones and newly created)
|
|
89
|
+
const allDraftIds = await getAllDraftTestCaseIds(featureId, verbose);
|
|
90
|
+
await updateTestCasesToReady(allDraftIds, verbose);
|
|
84
91
|
break;
|
|
85
92
|
}
|
|
86
93
|
if (currentIteration < maxIterations && verificationCycle.nextPrompt) {
|
|
@@ -100,15 +107,16 @@ export const analyseTestCases = async (options, config, checklistContext) => {
|
|
|
100
107
|
if (!structuredAnalysisResult) {
|
|
101
108
|
throw new Error('No analysis results received');
|
|
102
109
|
}
|
|
103
|
-
// If no checklist was used, update draft artifacts to ready
|
|
110
|
+
// If no checklist was used, update all draft artifacts to ready
|
|
104
111
|
if (!checklistContext ||
|
|
105
112
|
checklistContext.checklists.length === 0 ||
|
|
106
113
|
!verificationResult) {
|
|
107
|
-
|
|
114
|
+
const allDraftIds = await getAllDraftTestCaseIds(featureId, verbose);
|
|
115
|
+
if (allDraftIds.length > 0) {
|
|
108
116
|
if (verbose) {
|
|
109
|
-
logInfo('✅ No checklist verification needed. Updating test cases to ready status...');
|
|
117
|
+
logInfo('✅ No checklist verification needed. Updating all draft test cases to ready status...');
|
|
110
118
|
}
|
|
111
|
-
await updateTestCasesToReady(
|
|
119
|
+
await updateTestCasesToReady(allDraftIds, verbose);
|
|
112
120
|
}
|
|
113
121
|
}
|
|
114
122
|
if (verificationResult &&
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { TestCasesAnalysisContext } from './context.js';
|
|
2
|
+
export declare function resetReadyTestCasesToDraft(featureId: string, verbose?: boolean): Promise<number>;
|
|
3
|
+
export declare function getAllDraftTestCaseIds(featureId: string, verbose?: boolean): Promise<string[]>;
|
|
2
4
|
export declare function deleteTestCaseArtifacts(testCaseIds: string[], verbose?: boolean): Promise<void>;
|
|
3
5
|
export declare function deleteSpecificTestCases(featureId: string, deletedTestCaseIds: string[], deletionReasons: Record<string, string>, verbose?: boolean): Promise<void>;
|
|
4
6
|
export declare function updateTestCasesToReady(testCaseIds: string[], verbose?: boolean): Promise<void>;
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import { logInfo, logError } from '../../utils/logger.js';
|
|
2
2
|
import { batchUpdateTestCaseStatus, batchDeleteTestCases, getTestCases, } from '../../api/features/index.js';
|
|
3
3
|
import { callMcpEndpoint } from '../../api/mcp-client.js';
|
|
4
|
+
export async function resetReadyTestCasesToDraft(featureId, verbose) {
|
|
5
|
+
const testCases = await getTestCases(featureId, false);
|
|
6
|
+
const readyIds = testCases
|
|
7
|
+
.filter((tc) => tc.status === 'ready')
|
|
8
|
+
.map((tc) => tc.id);
|
|
9
|
+
if (readyIds.length === 0) {
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
if (verbose) {
|
|
13
|
+
logInfo(`🔄 Resetting ${readyIds.length} ready test cases to draft for re-analysis...`);
|
|
14
|
+
}
|
|
15
|
+
await batchUpdateTestCaseStatus(readyIds, 'draft', verbose);
|
|
16
|
+
return readyIds.length;
|
|
17
|
+
}
|
|
18
|
+
export async function getAllDraftTestCaseIds(featureId, verbose) {
|
|
19
|
+
const testCases = await getTestCases(featureId, false);
|
|
20
|
+
const draftIds = testCases
|
|
21
|
+
.filter((tc) => tc.status === 'draft')
|
|
22
|
+
.map((tc) => tc.id);
|
|
23
|
+
if (verbose) {
|
|
24
|
+
logInfo(`Found ${draftIds.length} draft test cases for feature`);
|
|
25
|
+
}
|
|
26
|
+
return draftIds;
|
|
27
|
+
}
|
|
4
28
|
export async function deleteTestCaseArtifacts(testCaseIds, verbose) {
|
|
5
29
|
if (testCaseIds.length > 0) {
|
|
6
30
|
if (verbose) {
|
|
@@ -66,6 +66,7 @@ You can delete existing draft test cases that are:
|
|
|
66
66
|
- You can ONLY delete artifacts with status 'draft'
|
|
67
67
|
- Only artifacts marked with [DELETABLE] can be deleted
|
|
68
68
|
- Each deletable artifact includes an "ID:" field with the UUID
|
|
69
|
+
- You MUST NOT delete, recreate, or modify artifacts marked with [APPROVED - DO NOT MODIFY]. These have been human-approved and are immutable. Do not create new items that duplicate approved ones.
|
|
69
70
|
|
|
70
71
|
**CRITICAL - Result Format**:
|
|
71
72
|
You MUST return ONLY a JSON object. Do NOT include any explanatory text. Return ONLY the JSON:
|
|
@@ -3,7 +3,7 @@ import { prepareUserStoriesAnalysisContext } from './context.js';
|
|
|
3
3
|
import { createUserStoriesAnalysisSystemPrompt } from './prompts.js';
|
|
4
4
|
import { executeUserStoriesAnalysisQuery } from './agent.js';
|
|
5
5
|
import { performVerificationCycle } from '../feature-analysis-verification/index.js';
|
|
6
|
-
import { deleteUserStoryArtifacts, deleteSpecificUserStories, updateUserStoriesToReady, saveUserStoriesAsDraft, buildUserStoriesAnalysisResult, } from './outcome.js';
|
|
6
|
+
import { deleteUserStoryArtifacts, deleteSpecificUserStories, updateUserStoriesToReady, saveUserStoriesAsDraft, buildUserStoriesAnalysisResult, resetReadyUserStoriesToDraft, getAllDraftUserStoryIds, } from './outcome.js';
|
|
7
7
|
import { logFeaturePhaseEvent } from '../../services/audit-logs.js';
|
|
8
8
|
export const analyseUserStories = async (options, config, checklistContext) => {
|
|
9
9
|
const { featureId, verbose } = options;
|
|
@@ -11,6 +11,11 @@ export const analyseUserStories = async (options, config, checklistContext) => {
|
|
|
11
11
|
logInfo(`Starting user stories analysis for feature ID: ${featureId}`);
|
|
12
12
|
}
|
|
13
13
|
try {
|
|
14
|
+
// Reset ready user stories to draft so AI can manage them on re-run
|
|
15
|
+
const resetCount = await resetReadyUserStoriesToDraft(featureId, verbose);
|
|
16
|
+
if (verbose && resetCount > 0) {
|
|
17
|
+
logInfo(`✅ Reset ${resetCount} ready user stories to draft for re-analysis`);
|
|
18
|
+
}
|
|
14
19
|
const context = await prepareUserStoriesAnalysisContext(featureId, checklistContext, verbose);
|
|
15
20
|
const systemPrompt = createUserStoriesAnalysisSystemPrompt();
|
|
16
21
|
const initialAnalysisPrompt = context.analysisPrompt;
|
|
@@ -78,9 +83,11 @@ export const analyseUserStories = async (options, config, checklistContext) => {
|
|
|
78
83
|
verificationResult = verificationCycle.verificationResult;
|
|
79
84
|
if (verificationCycle.passed) {
|
|
80
85
|
if (verbose) {
|
|
81
|
-
logInfo('✅ Verification passed! Updating user stories to ready status...');
|
|
86
|
+
logInfo('✅ Verification passed! Updating all draft user stories to ready status...');
|
|
82
87
|
}
|
|
83
|
-
|
|
88
|
+
// Update ALL remaining draft stories (both kept old ones and newly created)
|
|
89
|
+
const allDraftIds = await getAllDraftUserStoryIds(featureId, verbose);
|
|
90
|
+
await updateUserStoriesToReady(allDraftIds, verbose);
|
|
84
91
|
break;
|
|
85
92
|
}
|
|
86
93
|
if (currentIteration < maxIterations && verificationCycle.nextPrompt) {
|
|
@@ -100,15 +107,16 @@ export const analyseUserStories = async (options, config, checklistContext) => {
|
|
|
100
107
|
if (!structuredAnalysisResult) {
|
|
101
108
|
throw new Error('No analysis results received');
|
|
102
109
|
}
|
|
103
|
-
// If no checklist was used, update draft artifacts to ready
|
|
110
|
+
// If no checklist was used, update all draft artifacts to ready
|
|
104
111
|
if (!checklistContext ||
|
|
105
112
|
checklistContext.checklists.length === 0 ||
|
|
106
113
|
!verificationResult) {
|
|
107
|
-
|
|
114
|
+
const allDraftIds = await getAllDraftUserStoryIds(featureId, verbose);
|
|
115
|
+
if (allDraftIds.length > 0) {
|
|
108
116
|
if (verbose) {
|
|
109
|
-
logInfo('✅ No checklist verification needed. Updating user stories to ready status...');
|
|
117
|
+
logInfo('✅ No checklist verification needed. Updating all draft user stories to ready status...');
|
|
110
118
|
}
|
|
111
|
-
await updateUserStoriesToReady(
|
|
119
|
+
await updateUserStoriesToReady(allDraftIds, verbose);
|
|
112
120
|
}
|
|
113
121
|
}
|
|
114
122
|
if (verificationResult &&
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { UserStoriesAnalysisContext } from './context.js';
|
|
2
|
+
export declare function resetReadyUserStoriesToDraft(featureId: string, verbose?: boolean): Promise<number>;
|
|
3
|
+
export declare function getAllDraftUserStoryIds(featureId: string, verbose?: boolean): Promise<string[]>;
|
|
2
4
|
export declare function deleteUserStoryArtifacts(userStoryIds: string[], verbose?: boolean): Promise<void>;
|
|
3
5
|
export declare function deleteSpecificUserStories(featureId: string, deletedUserStoryIds: string[], deletionReasons: Record<string, string>, verbose?: boolean): Promise<void>;
|
|
4
6
|
export declare function updateUserStoriesToReady(userStoryIds: string[], verbose?: boolean): Promise<void>;
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import { logInfo, logError } from '../../utils/logger.js';
|
|
2
2
|
import { batchUpdateUserStoryStatus, batchDeleteUserStories, getUserStories, } from '../../api/features/index.js';
|
|
3
3
|
import { callMcpEndpoint } from '../../api/mcp-client.js';
|
|
4
|
+
export async function resetReadyUserStoriesToDraft(featureId, verbose) {
|
|
5
|
+
const stories = await getUserStories(featureId, false);
|
|
6
|
+
const readyStoryIds = stories
|
|
7
|
+
.filter((story) => story.status === 'ready')
|
|
8
|
+
.map((story) => story.id);
|
|
9
|
+
if (readyStoryIds.length === 0) {
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
if (verbose) {
|
|
13
|
+
logInfo(`🔄 Resetting ${readyStoryIds.length} ready user stories to draft for re-analysis...`);
|
|
14
|
+
}
|
|
15
|
+
await batchUpdateUserStoryStatus(readyStoryIds, 'draft', verbose);
|
|
16
|
+
return readyStoryIds.length;
|
|
17
|
+
}
|
|
18
|
+
export async function getAllDraftUserStoryIds(featureId, verbose) {
|
|
19
|
+
const stories = await getUserStories(featureId, false);
|
|
20
|
+
const draftIds = stories
|
|
21
|
+
.filter((story) => story.status === 'draft')
|
|
22
|
+
.map((story) => story.id);
|
|
23
|
+
if (verbose) {
|
|
24
|
+
logInfo(`Found ${draftIds.length} draft user stories for feature`);
|
|
25
|
+
}
|
|
26
|
+
return draftIds;
|
|
27
|
+
}
|
|
4
28
|
export async function deleteUserStoryArtifacts(userStoryIds, verbose) {
|
|
5
29
|
if (userStoryIds.length > 0) {
|
|
6
30
|
if (verbose) {
|
|
@@ -86,6 +86,7 @@ You can delete existing draft user stories that are:
|
|
|
86
86
|
- You can ONLY delete artifacts with status 'draft'
|
|
87
87
|
- Only artifacts marked with [DELETABLE] in the context can be deleted
|
|
88
88
|
- Each deletable artifact includes an "ID:" field with the UUID
|
|
89
|
+
- You MUST NOT delete, recreate, or modify artifacts marked with [APPROVED - DO NOT MODIFY]. These have been human-approved and are immutable. Do not create new stories that duplicate approved ones.
|
|
89
90
|
|
|
90
91
|
To delete artifacts:
|
|
91
92
|
1. Find the exact UUID from the "ID:" field
|
package/dist/types/features.d.ts
CHANGED
|
@@ -12,8 +12,8 @@ export interface FeatureInfo {
|
|
|
12
12
|
created_at?: string;
|
|
13
13
|
updated_at?: string;
|
|
14
14
|
}
|
|
15
|
-
export type UserStoryStatus = 'draft' | 'ready';
|
|
16
|
-
export type TestCaseStatus = 'draft' | 'ready';
|
|
15
|
+
export type UserStoryStatus = 'draft' | 'ready' | 'approved';
|
|
16
|
+
export type TestCaseStatus = 'draft' | 'ready' | 'approved';
|
|
17
17
|
export interface UserStory {
|
|
18
18
|
id: string;
|
|
19
19
|
title: string;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface CliOptions {
|
|
|
26
26
|
verbose?: boolean;
|
|
27
27
|
refactor?: boolean;
|
|
28
28
|
init?: boolean;
|
|
29
|
+
growthAnalysis?: string;
|
|
29
30
|
}
|
|
30
31
|
export type ReviewSeverity = 'error' | 'warning';
|
|
31
32
|
export type ExitCode = 0 | 1;
|
|
@@ -96,8 +97,8 @@ export interface FeatureData {
|
|
|
96
97
|
};
|
|
97
98
|
};
|
|
98
99
|
}
|
|
99
|
-
export type UserStoryStatus = 'draft' | 'ready';
|
|
100
|
-
export type TestCaseStatus = 'draft' | 'ready';
|
|
100
|
+
export type UserStoryStatus = 'draft' | 'ready' | 'approved';
|
|
101
|
+
export type TestCaseStatus = 'draft' | 'ready' | 'approved';
|
|
101
102
|
export interface UserStory {
|
|
102
103
|
id: string;
|
|
103
104
|
title: string;
|
package/dist/utils/formatters.js
CHANGED
|
@@ -7,7 +7,7 @@ export function formatUserStories(stories) {
|
|
|
7
7
|
return 'No user stories defined.';
|
|
8
8
|
return stories
|
|
9
9
|
.map((story, index) => `${index + 1}. **${story.title}** (Status: ${story.status})
|
|
10
|
-
ID: ${story.id}${story.status === 'draft' ? ' [DELETABLE]' : ''}
|
|
10
|
+
ID: ${story.id}${story.status === 'draft' ? ' [DELETABLE]' : ''}${story.status === 'approved' ? ' [APPROVED - DO NOT MODIFY]' : ''}
|
|
11
11
|
${story.description}`)
|
|
12
12
|
.join('\n\n');
|
|
13
13
|
}
|
|
@@ -19,7 +19,7 @@ export function formatTestCases(cases) {
|
|
|
19
19
|
return 'No test cases defined.';
|
|
20
20
|
return cases
|
|
21
21
|
.map((testCase, index) => `${index + 1}. **${testCase.name}** ${testCase.is_critical ? '[CRITICAL]' : '[OPTIONAL]'} (Status: ${testCase.status || 'unknown'})
|
|
22
|
-
ID: ${testCase.id}${testCase.status === 'draft' ? ' [DELETABLE]' : ''}
|
|
22
|
+
ID: ${testCase.id}${testCase.status === 'draft' ? ' [DELETABLE]' : ''}${testCase.status === 'approved' ? ' [APPROVED - DO NOT MODIFY]' : ''}
|
|
23
23
|
${testCase.description}`)
|
|
24
24
|
.join('\n\n');
|
|
25
25
|
}
|