edsger 0.56.2 → 0.57.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/chat.js +55 -2
- package/dist/api/cross-product.d.ts +8 -1
- package/dist/api/cross-product.js +44 -1
- package/dist/api/intelligence.js +98 -0
- package/dist/api/issues/get-issue.js +26 -0
- package/dist/api/issues/issue-utils.js +52 -0
- package/dist/api/issues/test-cases.js +89 -14
- package/dist/api/issues/update-issue.js +46 -8
- package/dist/api/issues/user-stories.js +89 -14
- package/dist/api/products/test-cases.d.ts +18 -0
- package/dist/api/products/test-cases.js +51 -0
- package/dist/api/products.js +21 -0
- package/dist/api/release-test-cases.js +38 -0
- package/dist/api/releases.js +86 -0
- package/dist/api/tasks.js +41 -4
- package/dist/api/test-reports.js +22 -4
- package/dist/api/user-psychology.d.ts +101 -0
- package/dist/api/user-psychology.js +143 -0
- package/dist/auth/auth-store.d.ts +33 -0
- package/dist/auth/auth-store.js +39 -0
- package/dist/commands/agent-workflow/chat-worker.js +187 -15
- package/dist/commands/agent-workflow/processor.d.ts +11 -0
- package/dist/commands/agent-workflow/processor.js +81 -2
- package/dist/commands/product-test-cases/index.d.ts +12 -0
- package/dist/commands/product-test-cases/index.js +40 -0
- package/dist/commands/screen-flow/index.d.ts +16 -0
- package/dist/commands/screen-flow/index.js +45 -0
- package/dist/commands/user-psychology/index.d.ts +7 -0
- package/dist/commands/user-psychology/index.js +51 -0
- package/dist/index.js +65 -0
- package/dist/phases/analyze-logs/index.js +27 -6
- package/dist/phases/bug-fixing/context-fetcher.js +26 -5
- package/dist/phases/find-features/index.js +53 -9
- package/dist/phases/find-shared/mcp.js +21 -0
- package/dist/phases/growth-analysis/context.d.ts +5 -3
- package/dist/phases/growth-analysis/context.js +52 -5
- package/dist/phases/output-contracts.js +129 -0
- package/dist/phases/pr-resolve/github-reply.d.ts +5 -2
- package/dist/phases/pr-resolve/github-reply.js +19 -3
- package/dist/phases/pr-resolve/index.js +19 -5
- package/dist/phases/pr-resolve/prompts.js +17 -18
- package/dist/phases/product-test-cases/index.d.ts +25 -0
- package/dist/phases/product-test-cases/index.js +174 -0
- package/dist/phases/product-test-cases/prompts.d.ts +24 -0
- package/dist/phases/product-test-cases/prompts.js +80 -0
- package/dist/phases/product-test-cases/types.d.ts +17 -0
- package/dist/phases/product-test-cases/types.js +27 -0
- package/dist/phases/screen-flow/index.d.ts +23 -0
- package/dist/phases/screen-flow/index.js +229 -0
- package/dist/phases/screen-flow/prompts.d.ts +19 -0
- package/dist/phases/screen-flow/prompts.js +39 -0
- package/dist/phases/screen-flow/theme.d.ts +19 -0
- package/dist/phases/screen-flow/theme.js +182 -0
- package/dist/phases/screen-flow/types.d.ts +130 -0
- package/dist/phases/screen-flow/types.js +66 -0
- package/dist/phases/user-psychology/agent.d.ts +16 -0
- package/dist/phases/user-psychology/agent.js +105 -0
- package/dist/phases/user-psychology/context.d.ts +10 -0
- package/dist/phases/user-psychology/context.js +65 -0
- package/dist/phases/user-psychology/index.d.ts +18 -0
- package/dist/phases/user-psychology/index.js +96 -0
- package/dist/phases/user-psychology/prompts.d.ts +2 -0
- package/dist/phases/user-psychology/prompts.js +41 -0
- package/dist/services/audit-logs.js +67 -9
- package/dist/services/branches.js +90 -14
- package/dist/services/phase-ratings.js +71 -9
- package/dist/services/product-logs.js +65 -5
- package/dist/services/pull-requests.js +74 -14
- package/dist/skills/phase/screen-flow/SKILL.md +78 -0
- package/dist/skills/phase/user-psychology/SKILL.md +135 -0
- package/dist/supabase/client.d.ts +23 -0
- package/dist/supabase/client.js +90 -0
- package/dist/system/session-manager.js +97 -24
- package/dist/types/index.d.ts +3 -0
- package/dist/utils/logger.js +24 -4
- package/package.json +5 -4
- package/vitest.config.ts +1 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: edsger product-test-cases <productId>
|
|
3
|
+
*
|
|
4
|
+
* Clones the product's repo, asks Claude to draft a product-level regression
|
|
5
|
+
* suite (deduped against existing approved + unapproved cases), saves the new
|
|
6
|
+
* ones as drafts, and cleans up the workspace.
|
|
7
|
+
*/
|
|
8
|
+
import { getGitHubConfigByProduct } from '../../api/github.js';
|
|
9
|
+
import { generateProductTestCases } from '../../phases/product-test-cases/index.js';
|
|
10
|
+
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
11
|
+
export async function runProductTestCases(productId, options) {
|
|
12
|
+
const { branch, verbose } = options;
|
|
13
|
+
logInfo(`Starting product test cases generation for product ${productId}`);
|
|
14
|
+
const githubConfig = await getGitHubConfigByProduct(productId, verbose);
|
|
15
|
+
if (!githubConfig.configured ||
|
|
16
|
+
!githubConfig.token ||
|
|
17
|
+
!githubConfig.owner ||
|
|
18
|
+
!githubConfig.repo) {
|
|
19
|
+
logError(`GitHub not configured for product ${productId}: ${githubConfig.message || 'No installation found'}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const result = await generateProductTestCases({
|
|
23
|
+
productId,
|
|
24
|
+
githubToken: githubConfig.token,
|
|
25
|
+
owner: githubConfig.owner,
|
|
26
|
+
repo: githubConfig.repo,
|
|
27
|
+
branch,
|
|
28
|
+
verbose,
|
|
29
|
+
});
|
|
30
|
+
if (result.status === 'success') {
|
|
31
|
+
logSuccess(result.message);
|
|
32
|
+
if (result.summary) {
|
|
33
|
+
logInfo(result.summary);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
logError(result.message);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: edsger screen-flow <productId> --flow-id <id>
|
|
3
|
+
*
|
|
4
|
+
* Maps the product's user-facing screens and transitions into a structured
|
|
5
|
+
* flow stored in screen_flows / screen_flow_nodes / screen_flow_edges.
|
|
6
|
+
*
|
|
7
|
+
* The desktop UI creates a pending screen_flows row first, then invokes
|
|
8
|
+
* the CLI with --flow-id; the CLI flips status running → success/failed
|
|
9
|
+
* and populates the nodes/edges tables.
|
|
10
|
+
*/
|
|
11
|
+
export interface ScreenFlowCliOptions {
|
|
12
|
+
flowId: string;
|
|
13
|
+
guidance?: string;
|
|
14
|
+
verbose?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function runScreenFlow(productId: string, options: ScreenFlowCliOptions): Promise<void>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: edsger screen-flow <productId> --flow-id <id>
|
|
3
|
+
*
|
|
4
|
+
* Maps the product's user-facing screens and transitions into a structured
|
|
5
|
+
* flow stored in screen_flows / screen_flow_nodes / screen_flow_edges.
|
|
6
|
+
*
|
|
7
|
+
* The desktop UI creates a pending screen_flows row first, then invokes
|
|
8
|
+
* the CLI with --flow-id; the CLI flips status running → success/failed
|
|
9
|
+
* and populates the nodes/edges tables.
|
|
10
|
+
*/
|
|
11
|
+
import { runScreenFlowPhase } from '../../phases/screen-flow/index.js';
|
|
12
|
+
import { deregisterSession, registerSession, } from '../../system/session-manager.js';
|
|
13
|
+
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
14
|
+
export async function runScreenFlow(productId, options) {
|
|
15
|
+
const { flowId, guidance, verbose } = options;
|
|
16
|
+
if (!productId) {
|
|
17
|
+
throw new Error('Product ID is required for screen-flow');
|
|
18
|
+
}
|
|
19
|
+
if (!flowId) {
|
|
20
|
+
throw new Error('--flow-id is required (the pending screen_flows row id)');
|
|
21
|
+
}
|
|
22
|
+
await registerSession({ command: 'screen-flow', productId });
|
|
23
|
+
logInfo(`Starting screen flow generation for product ${productId}`);
|
|
24
|
+
try {
|
|
25
|
+
const result = await runScreenFlowPhase({
|
|
26
|
+
productId,
|
|
27
|
+
flowId,
|
|
28
|
+
guidance,
|
|
29
|
+
verbose,
|
|
30
|
+
});
|
|
31
|
+
if (result.status === 'success') {
|
|
32
|
+
logSuccess(result.message);
|
|
33
|
+
if (result.summary) {
|
|
34
|
+
logInfo(`\nSummary: ${result.summary}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
logError(result.message);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await deregisterSession();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type CliOptions } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run AI-powered user psychology analysis for a product. Reads the product
|
|
4
|
+
* repo, applies JTBD + Fogg + persona frameworks, and writes the structured
|
|
5
|
+
* profile back to user_psychology_analyses.
|
|
6
|
+
*/
|
|
7
|
+
export declare const runUserPsychologyAnalysis: (options: CliOptions) => Promise<void>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { analyseUserPsychology, } from '../../phases/user-psychology/index.js';
|
|
2
|
+
import { deregisterSession, registerSession, } from '../../system/session-manager.js';
|
|
3
|
+
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
4
|
+
import { validateConfiguration } from '../../utils/validation.js';
|
|
5
|
+
/**
|
|
6
|
+
* Run AI-powered user psychology analysis for a product. Reads the product
|
|
7
|
+
* repo, applies JTBD + Fogg + persona frameworks, and writes the structured
|
|
8
|
+
* profile back to user_psychology_analyses.
|
|
9
|
+
*/
|
|
10
|
+
export const runUserPsychologyAnalysis = async (options) => {
|
|
11
|
+
const productId = options.userPsychology;
|
|
12
|
+
if (!productId) {
|
|
13
|
+
throw new Error('Product ID is required for user psychology analysis');
|
|
14
|
+
}
|
|
15
|
+
const analysisId = options.userPsychologyAnalysisId;
|
|
16
|
+
if (!analysisId) {
|
|
17
|
+
throw new Error('--analysis-id is required (reserved by the desktop UI)');
|
|
18
|
+
}
|
|
19
|
+
const config = validateConfiguration(options);
|
|
20
|
+
await registerSession({ command: 'user-psychology', productId });
|
|
21
|
+
logInfo(`Starting user psychology analysis for product: ${productId}`);
|
|
22
|
+
try {
|
|
23
|
+
const result = await analyseUserPsychology({
|
|
24
|
+
productId,
|
|
25
|
+
analysisId,
|
|
26
|
+
verbose: options.verbose,
|
|
27
|
+
guidance: options.userPsychologyGuidance,
|
|
28
|
+
}, config);
|
|
29
|
+
if (result.status === 'success') {
|
|
30
|
+
logSuccess('User psychology analysis completed.');
|
|
31
|
+
logInfo(` Personas: ${result.personaCount}`);
|
|
32
|
+
logInfo(` Jobs-to-be-Done: ${result.jobCount}`);
|
|
33
|
+
logInfo(` Pain points: ${result.painPointCount}`);
|
|
34
|
+
if (result.analysisId) {
|
|
35
|
+
logInfo(` Analysis ID: ${result.analysisId}`);
|
|
36
|
+
}
|
|
37
|
+
logInfo('\nView full results in the Psychology tab of your product.');
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
logError(`User psychology analysis failed: ${result.summary}`);
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
logError(`User psychology analysis failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await deregisterSession();
|
|
50
|
+
}
|
|
51
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -19,11 +19,13 @@ import { runFindBugs } from './commands/find-bugs/index.js';
|
|
|
19
19
|
import { runFindFeatures } from './commands/find-features/index.js';
|
|
20
20
|
import { parseCategoriesOption, runFindSmells, } from './commands/find-smells/index.js';
|
|
21
21
|
import { runGrowthAnalysis } from './commands/growth-analysis/index.js';
|
|
22
|
+
import { runScreenFlow } from './commands/screen-flow/index.js';
|
|
22
23
|
import { runInit } from './commands/init/index.js';
|
|
23
24
|
import { runIntelligence } from './commands/intelligence/index.js';
|
|
24
25
|
import { runIssueAnalysisCommand } from './commands/issue-analysis/index.js';
|
|
25
26
|
import { runPRResolve } from './commands/pr-resolve/index.js';
|
|
26
27
|
import { runPRReview } from './commands/pr-review/index.js';
|
|
28
|
+
import { runProductTestCases } from './commands/product-test-cases/index.js';
|
|
27
29
|
import { runQualityBenchmarkCli } from './commands/quality-benchmark/index.js';
|
|
28
30
|
import { runRefactor } from './commands/refactor/refactor.js';
|
|
29
31
|
import { runReleaseSyncCommand } from './commands/release-sync/index.js';
|
|
@@ -34,6 +36,7 @@ import { runSyncSentryIssues } from './commands/sync-sentry-issues/index.js';
|
|
|
34
36
|
import { runTaskWorker } from './commands/task-worker/index.js';
|
|
35
37
|
import { runTechnicalDesignCommand } from './commands/technical-design/index.js';
|
|
36
38
|
import { runTestCasesAnalysisCommand } from './commands/test-cases-analysis/index.js';
|
|
39
|
+
import { runUserPsychologyAnalysis } from './commands/user-psychology/index.js';
|
|
37
40
|
import { runUserStoriesAnalysisCommand } from './commands/user-stories-analysis/index.js';
|
|
38
41
|
import { runWorkflow } from './commands/workflow/index.js';
|
|
39
42
|
import { DEFAULT_MAX_FILES as FIND_ARCHITECTURE_DEFAULT_MAX_FILES } from './phases/find-architecture/index.js';
|
|
@@ -157,6 +160,51 @@ program
|
|
|
157
160
|
}
|
|
158
161
|
});
|
|
159
162
|
// ============================================================
|
|
163
|
+
// Subcommand: edsger screen-flow <productId>
|
|
164
|
+
// ============================================================
|
|
165
|
+
program
|
|
166
|
+
.command('screen-flow <productId>')
|
|
167
|
+
.description('Generate a structured screen flow (screens + transitions) for a product from its source code')
|
|
168
|
+
.requiredOption('--flow-id <id>', 'Pending screen_flows row id to populate')
|
|
169
|
+
.option('-g, --guidance <text>', 'Human direction for the AI (focus areas, exclusions)')
|
|
170
|
+
.option('-v, --verbose', 'Verbose output')
|
|
171
|
+
.action(async (productId, opts) => {
|
|
172
|
+
try {
|
|
173
|
+
await runScreenFlow(productId, {
|
|
174
|
+
flowId: opts.flowId,
|
|
175
|
+
guidance: opts.guidance,
|
|
176
|
+
verbose: opts.verbose,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// ============================================================
|
|
185
|
+
// Subcommand: edsger user-psychology <productId>
|
|
186
|
+
// ============================================================
|
|
187
|
+
program
|
|
188
|
+
.command('user-psychology <productId>')
|
|
189
|
+
.description('Research the product target users and produce a structured psychographic profile (personas + JTBD + Fogg + messaging angles)')
|
|
190
|
+
.option('-v, --verbose', 'Verbose output')
|
|
191
|
+
.option('-g, --guidance <text>', 'Human direction for the AI (audience focus, market, segment)')
|
|
192
|
+
.requiredOption('--analysis-id <id>', 'ID of the pending analysis row to fill in (reserved by the desktop UI)')
|
|
193
|
+
.action(async (productId, opts) => {
|
|
194
|
+
try {
|
|
195
|
+
await runUserPsychologyAnalysis({
|
|
196
|
+
userPsychology: productId,
|
|
197
|
+
verbose: opts.verbose,
|
|
198
|
+
userPsychologyGuidance: opts.guidance,
|
|
199
|
+
userPsychologyAnalysisId: opts.analysisId,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
// ============================================================
|
|
160
208
|
// Subcommand: edsger financing-deck <productId>
|
|
161
209
|
// ============================================================
|
|
162
210
|
program
|
|
@@ -629,6 +677,23 @@ program
|
|
|
629
677
|
}
|
|
630
678
|
});
|
|
631
679
|
// ============================================================
|
|
680
|
+
// Subcommand: edsger product-test-cases <productId>
|
|
681
|
+
// ============================================================
|
|
682
|
+
program
|
|
683
|
+
.command('product-test-cases <productId>')
|
|
684
|
+
.description("Generate product-level regression test cases by exploring the product's repo. Existing approved cases are preserved; replaceable (draft / pending_approval) ones may be deleted and replaced.")
|
|
685
|
+
.option('--branch <name>', 'Branch to scan (defaults to repo default branch)')
|
|
686
|
+
.option('-v, --verbose', 'Verbose output')
|
|
687
|
+
.action(async (productId, opts) => {
|
|
688
|
+
try {
|
|
689
|
+
await runProductTestCases(productId, opts);
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
// ============================================================
|
|
632
697
|
// Subcommand: edsger pr-resolve <productId>
|
|
633
698
|
// ============================================================
|
|
634
699
|
program
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* user's session logs (only if the latest log is >1 hour old), and creates
|
|
6
6
|
* product_user_feedbacks for any issues or improvement suggestions found.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { getProduct } from '../../api/products.js';
|
|
9
9
|
import { getPendingLogsByUser, markLogsAnalyzed, } from '../../services/product-logs.js';
|
|
10
|
+
import { getSupabase, hasSupabaseSession } from '../../supabase/client.js';
|
|
10
11
|
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
12
|
+
import { callMcpEndpoint } from '../../api/mcp-client.js';
|
|
11
13
|
import { executeLogAnalysis } from './agent.js';
|
|
12
14
|
import { createAnalyzeLogsSystemPrompt, createAnalyzeLogsUserPrompt, } from './prompts.js';
|
|
13
15
|
/**
|
|
@@ -19,9 +21,10 @@ export async function analyzeLogs(options) {
|
|
|
19
21
|
// 1. Get product info for context
|
|
20
22
|
let productName = productId;
|
|
21
23
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
// Use the shared product wrapper — it tries the user-JWT SDK path first
|
|
25
|
+
// and falls back to the resources/read MCP path when the row isn't
|
|
26
|
+
// directly accessible (e.g. slug lookup).
|
|
27
|
+
const product = (await getProduct(productId));
|
|
25
28
|
if (product?.name) {
|
|
26
29
|
productName = product.name;
|
|
27
30
|
}
|
|
@@ -100,7 +103,7 @@ export async function analyzeLogs(options) {
|
|
|
100
103
|
async function createFeedbacksFromAnalysis(productId, group, result, verbose) {
|
|
101
104
|
for (const feedback of result.feedbacks) {
|
|
102
105
|
try {
|
|
103
|
-
|
|
106
|
+
const payload = {
|
|
104
107
|
product_id: productId,
|
|
105
108
|
category: feedback.category,
|
|
106
109
|
title: feedback.title,
|
|
@@ -119,7 +122,25 @@ async function createFeedbacksFromAnalysis(productId, group, result, verbose) {
|
|
|
119
122
|
to: group.logs[group.logs.length - 1]?.created_at,
|
|
120
123
|
},
|
|
121
124
|
},
|
|
122
|
-
}
|
|
125
|
+
};
|
|
126
|
+
let usedSdk = false;
|
|
127
|
+
if (hasSupabaseSession()) {
|
|
128
|
+
try {
|
|
129
|
+
const { error } = await getSupabase()
|
|
130
|
+
.from('product_user_feedbacks')
|
|
131
|
+
.insert({ ...payload, status: 'new' });
|
|
132
|
+
if (error) {
|
|
133
|
+
throw new Error(error.message);
|
|
134
|
+
}
|
|
135
|
+
usedSdk = true;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Fall through to MCP
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!usedSdk) {
|
|
142
|
+
await callMcpEndpoint('product_user_feedbacks/create', payload);
|
|
143
|
+
}
|
|
123
144
|
if (verbose) {
|
|
124
145
|
logInfo(`Created feedback: [${feedback.category}] ${feedback.title}`);
|
|
125
146
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getIssue, getTestCases, getUserStories, } from '../../api/issues/index.js';
|
|
2
2
|
import { callMcpEndpoint } from '../../api/mcp-client.js';
|
|
3
|
+
import { getSupabase, hasSupabaseSession } from '../../supabase/client.js';
|
|
3
4
|
import { logError, logInfo } from '../../utils/logger.js';
|
|
4
5
|
/**
|
|
5
6
|
* Fetch all bug fixing context information via MCP endpoints
|
|
@@ -21,11 +22,31 @@ export async function fetchBugFixingContext(issueId, verbose) {
|
|
|
21
22
|
if (verbose) {
|
|
22
23
|
logInfo('Fetching latest test report...');
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
let raw = null;
|
|
26
|
+
if (hasSupabaseSession()) {
|
|
27
|
+
try {
|
|
28
|
+
const { data, error } = await getSupabase()
|
|
29
|
+
.from('test_reports')
|
|
30
|
+
.select('*')
|
|
31
|
+
.eq('issue_id', issueId)
|
|
32
|
+
.order('created_at', { ascending: false })
|
|
33
|
+
.limit(1)
|
|
34
|
+
.maybeSingle();
|
|
35
|
+
if (error) {
|
|
36
|
+
throw new Error(error.message);
|
|
37
|
+
}
|
|
38
|
+
raw = data;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Fall through to MCP
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (raw == null) {
|
|
45
|
+
const testReportResult = (await callMcpEndpoint('test_reports/latest', { issue_id: issueId }));
|
|
46
|
+
raw = testReportResult.test_report ?? null;
|
|
47
|
+
}
|
|
48
|
+
if (raw) {
|
|
49
|
+
latestTestReport = raw;
|
|
29
50
|
}
|
|
30
51
|
}
|
|
31
52
|
catch (error) {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
16
16
|
import { callMcpEndpoint } from '../../api/mcp-client.js';
|
|
17
17
|
import { DEFAULT_MODEL } from '../../constants.js';
|
|
18
|
+
import { getSupabase, hasSupabaseSession } from '../../supabase/client.js';
|
|
18
19
|
import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
|
|
19
20
|
import { cleanupIssueRepo, cloneIssueRepo, ensureWorkspaceDir, syncRepoToRef, } from '../../workspace/workspace-manager.js';
|
|
20
21
|
import { detectDefaultBranch } from '../find-shared/git.js';
|
|
@@ -211,11 +212,33 @@ async function fetchFeedbacks(productId, sinceIso) {
|
|
|
211
212
|
//
|
|
212
213
|
// NOT to be confused with `feedbacks/list_for_product`, which returns
|
|
213
214
|
// Claude-Code phase feedback and is an internal-workflow artefact.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
let rows = [];
|
|
216
|
+
let usedSdk = false;
|
|
217
|
+
if (hasSupabaseSession()) {
|
|
218
|
+
try {
|
|
219
|
+
const { data, error } = await getSupabase()
|
|
220
|
+
.from('product_user_feedbacks')
|
|
221
|
+
.select('*')
|
|
222
|
+
.eq('product_id', productId)
|
|
223
|
+
.gte('created_at', sinceIso)
|
|
224
|
+
.order('created_at', { ascending: false });
|
|
225
|
+
if (error) {
|
|
226
|
+
throw new Error(error.message);
|
|
227
|
+
}
|
|
228
|
+
rows = (data || []);
|
|
229
|
+
usedSdk = true;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Fall through to MCP
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!usedSdk) {
|
|
236
|
+
const result = (await callMcpEndpoint('product_user_feedbacks/list_for_product', {
|
|
237
|
+
product_id: productId,
|
|
238
|
+
since: sinceIso,
|
|
239
|
+
}));
|
|
240
|
+
rows = result.feedbacks || [];
|
|
241
|
+
}
|
|
219
242
|
return rows.map((f) => ({
|
|
220
243
|
id: f.id,
|
|
221
244
|
body: f.title ? `${f.title}\n${f.description}`.trim() : f.description,
|
|
@@ -232,10 +255,31 @@ async function fetchIntelligenceReports(productId, focusReportId) {
|
|
|
232
255
|
try {
|
|
233
256
|
// `intelligence_reports` schema (20260327000000_create_intelligence.sql:69):
|
|
234
257
|
// id, product_id, report_type, title, summary, full_report, …, created_at.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
258
|
+
let reports = [];
|
|
259
|
+
let usedSdk = false;
|
|
260
|
+
if (hasSupabaseSession()) {
|
|
261
|
+
try {
|
|
262
|
+
const { data, error } = await getSupabase()
|
|
263
|
+
.from('intelligence_reports')
|
|
264
|
+
.select('id, title, summary, created_at')
|
|
265
|
+
.eq('product_id', productId)
|
|
266
|
+
.order('created_at', { ascending: false });
|
|
267
|
+
if (error) {
|
|
268
|
+
throw new Error(error.message);
|
|
269
|
+
}
|
|
270
|
+
reports = (data || []);
|
|
271
|
+
usedSdk = true;
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// Fall through to MCP
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (!usedSdk) {
|
|
278
|
+
const result = (await callMcpEndpoint('intelligence/reports/list', {
|
|
279
|
+
product_id: productId,
|
|
280
|
+
}));
|
|
281
|
+
reports = result.reports || [];
|
|
282
|
+
}
|
|
239
283
|
if (focusReportId) {
|
|
240
284
|
// If focused, put the focus report first and keep 5 others for context.
|
|
241
285
|
const focus = reports.filter((r) => r.id === focusReportId);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* place, and the per-phase orchestrators stay focused on their own logic.
|
|
8
8
|
*/
|
|
9
9
|
import { callMcpEndpoint } from '../../api/mcp-client.js';
|
|
10
|
+
import { getSupabase, hasSupabaseSession } from '../../supabase/client.js';
|
|
10
11
|
import { logError, logWarning } from '../../utils/logger.js';
|
|
11
12
|
export async function fetchProductBasics(productId) {
|
|
12
13
|
try {
|
|
@@ -55,6 +56,26 @@ export function isTerminalStatus(status) {
|
|
|
55
56
|
*/
|
|
56
57
|
export async function createIssue(input) {
|
|
57
58
|
try {
|
|
59
|
+
if (hasSupabaseSession()) {
|
|
60
|
+
try {
|
|
61
|
+
const { data, error } = await getSupabase()
|
|
62
|
+
.from('issues')
|
|
63
|
+
.insert({
|
|
64
|
+
product_id: input.productId,
|
|
65
|
+
name: input.title,
|
|
66
|
+
description: input.description,
|
|
67
|
+
})
|
|
68
|
+
.select('id')
|
|
69
|
+
.single();
|
|
70
|
+
if (error) {
|
|
71
|
+
throw new Error(error.message);
|
|
72
|
+
}
|
|
73
|
+
return data?.id || null;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Fall through to MCP
|
|
77
|
+
}
|
|
78
|
+
}
|
|
58
79
|
const result = (await callMcpEndpoint('issues/create', {
|
|
59
80
|
product_id: input.productId,
|
|
60
81
|
name: input.title,
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { type GrowthCampaign } from '../../api/growth.js';
|
|
2
2
|
import { type ProductInfo } from '../../api/products.js';
|
|
3
|
+
import { type UserPsychologyAnalysis } from '../../api/user-psychology.js';
|
|
3
4
|
export interface GrowthAnalysisContext {
|
|
4
5
|
product: ProductInfo;
|
|
5
6
|
previousCampaigns: GrowthCampaign[];
|
|
7
|
+
/** Most recent completed user-psychology profile, if any. Lets the
|
|
8
|
+
* growth agent ground content in real personas + JTBDs + messaging
|
|
9
|
+
* angles instead of guessing the audience from scratch. */
|
|
10
|
+
psychology: UserPsychologyAnalysis | null;
|
|
6
11
|
}
|
|
7
12
|
/**
|
|
8
13
|
* Fetch all context needed for growth analysis via MCP endpoints
|
|
9
14
|
*/
|
|
10
15
|
export declare function fetchGrowthAnalysisContext(productId: string, verbose?: boolean): Promise<GrowthAnalysisContext>;
|
|
11
|
-
/**
|
|
12
|
-
* Format context into a prompt string for the AI
|
|
13
|
-
*/
|
|
14
16
|
export declare function formatContextForPrompt(context: GrowthAnalysisContext, guidance?: string): string;
|
|
15
17
|
/**
|
|
16
18
|
* Prepare the full analysis prompt with all context
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getGrowthCampaigns } from '../../api/growth.js';
|
|
2
2
|
import { getProduct } from '../../api/products.js';
|
|
3
|
+
import { getLatestUserPsychologyAnalysis, } from '../../api/user-psychology.js';
|
|
3
4
|
import { logError, logInfo } from '../../utils/logger.js';
|
|
4
5
|
import { createGrowthAnalysisPromptWithContext } from './prompts.js';
|
|
5
6
|
/**
|
|
@@ -10,16 +11,18 @@ export async function fetchGrowthAnalysisContext(productId, verbose) {
|
|
|
10
11
|
if (verbose) {
|
|
11
12
|
logInfo(`Fetching growth analysis context for product: ${productId}`);
|
|
12
13
|
}
|
|
13
|
-
const [product, previousCampaigns] = await Promise.all([
|
|
14
|
+
const [product, previousCampaigns, psychology] = await Promise.all([
|
|
14
15
|
getProduct(productId, verbose),
|
|
15
16
|
getGrowthCampaigns(productId, verbose),
|
|
17
|
+
getLatestUserPsychologyAnalysis(productId, verbose),
|
|
16
18
|
]);
|
|
17
19
|
if (verbose) {
|
|
18
20
|
logInfo(`Growth analysis context fetched:`);
|
|
19
21
|
logInfo(` Product: ${product.name}`);
|
|
20
22
|
logInfo(` Previous campaigns: ${previousCampaigns.length}`);
|
|
23
|
+
logInfo(` Psychology profile: ${psychology ? 'available' : 'none'}`);
|
|
21
24
|
}
|
|
22
|
-
return { product, previousCampaigns };
|
|
25
|
+
return { product, previousCampaigns, psychology };
|
|
23
26
|
}
|
|
24
27
|
catch (error) {
|
|
25
28
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -30,8 +33,52 @@ export async function fetchGrowthAnalysisContext(productId, verbose) {
|
|
|
30
33
|
/**
|
|
31
34
|
* Format context into a prompt string for the AI
|
|
32
35
|
*/
|
|
36
|
+
function formatPsychologyForPrompt(psychology) {
|
|
37
|
+
if (!psychology) {
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
const personas = (psychology.target_personas ?? [])
|
|
41
|
+
.slice(0, 5)
|
|
42
|
+
.map((p, i) => {
|
|
43
|
+
const drivers = p.decision_drivers?.length
|
|
44
|
+
? ` — decision drivers: ${p.decision_drivers.join(', ')}`
|
|
45
|
+
: '';
|
|
46
|
+
return `${i + 1}. **${p.name}** — ${p.archetype}${drivers}`;
|
|
47
|
+
})
|
|
48
|
+
.join('\n');
|
|
49
|
+
const jobs = (psychology.jobs_to_be_done ?? [])
|
|
50
|
+
.slice(0, 8)
|
|
51
|
+
.map((j) => `- [${j.type}] ${j.statement}`)
|
|
52
|
+
.join('\n');
|
|
53
|
+
const pains = (psychology.pain_points ?? [])
|
|
54
|
+
.slice(0, 6)
|
|
55
|
+
.map((pp) => `- [${pp.severity}] ${pp.pain}`)
|
|
56
|
+
.join('\n');
|
|
57
|
+
const angles = (psychology.messaging_angles ?? [])
|
|
58
|
+
.slice(0, 6)
|
|
59
|
+
.map((m) => `- **${m.angle_name}** (${m.psychological_lever}): "${m.hook}" — for ${m.persona}`)
|
|
60
|
+
.join('\n');
|
|
61
|
+
return `
|
|
62
|
+
|
|
63
|
+
## User Psychology Profile (latest research, dated ${psychology.created_at.slice(0, 10)})
|
|
64
|
+
Use this as the authoritative source for who the audience is. Content suggestions must speak to one of these personas and answer one of their jobs-to-be-done. Reuse the messaging angles below as hooks where they fit.
|
|
65
|
+
|
|
66
|
+
### Target Personas
|
|
67
|
+
${personas || '(none)'}
|
|
68
|
+
|
|
69
|
+
### Jobs-to-be-Done
|
|
70
|
+
${jobs || '(none)'}
|
|
71
|
+
|
|
72
|
+
### Pain Points
|
|
73
|
+
${pains || '(none)'}
|
|
74
|
+
|
|
75
|
+
### Pre-validated Messaging Angles
|
|
76
|
+
${angles || '(none)'}
|
|
77
|
+
|
|
78
|
+
---`;
|
|
79
|
+
}
|
|
33
80
|
export function formatContextForPrompt(context, guidance) {
|
|
34
|
-
const { product, previousCampaigns } = context;
|
|
81
|
+
const { product, previousCampaigns, psychology } = context;
|
|
35
82
|
const campaignsList = previousCampaigns.length > 0
|
|
36
83
|
? previousCampaigns
|
|
37
84
|
.map((c, i) => `${i + 1}. **${c.title}** [${c.channel}] (${c.status})
|
|
@@ -64,14 +111,14 @@ ${guidance}
|
|
|
64
111
|
|
|
65
112
|
## Product Issues (${product.issues?.length || 0})
|
|
66
113
|
${issuesList}
|
|
67
|
-
${guidanceSection}
|
|
114
|
+
${guidanceSection}${formatPsychologyForPrompt(psychology)}
|
|
68
115
|
|
|
69
116
|
## Previous Growth Campaigns (${previousCampaigns.length})
|
|
70
117
|
${campaignsList}
|
|
71
118
|
|
|
72
119
|
---
|
|
73
120
|
|
|
74
|
-
**Important**: Analyze the product above and create a growth strategy.${guidance ? ' Follow the human growth guidance provided above for direction, themes, and tone.' : ''} Each content suggestion must be DIFFERENT from all previous campaigns listed above. Use the product name, description, and issues to write specific, concrete content — never use placeholder text.`;
|
|
121
|
+
**Important**: Analyze the product above and create a growth strategy.${guidance ? ' Follow the human growth guidance provided above for direction, themes, and tone.' : ''}${psychology ? ' Anchor every content suggestion to a persona from the psychology profile above and answer a specific job-to-be-done.' : ''} Each content suggestion must be DIFFERENT from all previous campaigns listed above. Use the product name, description, and issues to write specific, concrete content — never use placeholder text.`;
|
|
75
122
|
}
|
|
76
123
|
/**
|
|
77
124
|
* Prepare the full analysis prompt with all context
|