edsger 0.56.3 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/api/chat.js +55 -2
  2. package/dist/api/cross-product.d.ts +8 -1
  3. package/dist/api/cross-product.js +44 -1
  4. package/dist/api/intelligence.js +98 -0
  5. package/dist/api/issues/get-issue.js +26 -0
  6. package/dist/api/issues/issue-utils.js +52 -0
  7. package/dist/api/issues/test-cases.js +89 -14
  8. package/dist/api/issues/update-issue.js +46 -8
  9. package/dist/api/issues/user-stories.js +89 -14
  10. package/dist/api/products/test-cases.d.ts +18 -0
  11. package/dist/api/products/test-cases.js +51 -0
  12. package/dist/api/products.js +21 -0
  13. package/dist/api/release-test-cases.js +38 -0
  14. package/dist/api/releases.js +86 -0
  15. package/dist/api/tasks.js +41 -4
  16. package/dist/api/test-reports.js +22 -4
  17. package/dist/api/user-psychology.d.ts +101 -0
  18. package/dist/api/user-psychology.js +143 -0
  19. package/dist/auth/auth-store.d.ts +33 -0
  20. package/dist/auth/auth-store.js +39 -0
  21. package/dist/commands/agent-workflow/chat-worker.js +187 -15
  22. package/dist/commands/agent-workflow/processor.d.ts +11 -0
  23. package/dist/commands/agent-workflow/processor.js +81 -2
  24. package/dist/commands/product-test-cases/index.d.ts +12 -0
  25. package/dist/commands/product-test-cases/index.js +40 -0
  26. package/dist/commands/screen-flow/index.d.ts +16 -0
  27. package/dist/commands/screen-flow/index.js +45 -0
  28. package/dist/commands/user-psychology/index.d.ts +7 -0
  29. package/dist/commands/user-psychology/index.js +51 -0
  30. package/dist/index.js +65 -0
  31. package/dist/phases/analyze-logs/index.js +27 -6
  32. package/dist/phases/bug-fixing/context-fetcher.js +26 -5
  33. package/dist/phases/find-features/index.js +53 -9
  34. package/dist/phases/find-shared/mcp.js +21 -0
  35. package/dist/phases/growth-analysis/context.d.ts +5 -3
  36. package/dist/phases/growth-analysis/context.js +52 -5
  37. package/dist/phases/output-contracts.js +140 -0
  38. package/dist/phases/pr-resolve/github-reply.d.ts +5 -2
  39. package/dist/phases/pr-resolve/github-reply.js +19 -3
  40. package/dist/phases/pr-resolve/index.js +19 -5
  41. package/dist/phases/pr-resolve/prompts.js +17 -18
  42. package/dist/phases/pr-shared/agent-utils.d.ts +11 -3
  43. package/dist/phases/pr-shared/agent-utils.js +48 -4
  44. package/dist/phases/product-test-cases/index.d.ts +25 -0
  45. package/dist/phases/product-test-cases/index.js +174 -0
  46. package/dist/phases/product-test-cases/prompts.d.ts +24 -0
  47. package/dist/phases/product-test-cases/prompts.js +80 -0
  48. package/dist/phases/product-test-cases/types.d.ts +17 -0
  49. package/dist/phases/product-test-cases/types.js +27 -0
  50. package/dist/phases/screen-flow/index.d.ts +23 -0
  51. package/dist/phases/screen-flow/index.js +285 -0
  52. package/dist/phases/screen-flow/mcp-server.d.ts +195 -0
  53. package/dist/phases/screen-flow/mcp-server.js +262 -0
  54. package/dist/phases/screen-flow/prompts.d.ts +19 -0
  55. package/dist/phases/screen-flow/prompts.js +41 -0
  56. package/dist/phases/screen-flow/theme.d.ts +19 -0
  57. package/dist/phases/screen-flow/theme.js +193 -0
  58. package/dist/phases/screen-flow/types.d.ts +130 -0
  59. package/dist/phases/screen-flow/types.js +81 -0
  60. package/dist/phases/user-psychology/agent.d.ts +16 -0
  61. package/dist/phases/user-psychology/agent.js +105 -0
  62. package/dist/phases/user-psychology/context.d.ts +10 -0
  63. package/dist/phases/user-psychology/context.js +65 -0
  64. package/dist/phases/user-psychology/index.d.ts +18 -0
  65. package/dist/phases/user-psychology/index.js +96 -0
  66. package/dist/phases/user-psychology/prompts.d.ts +2 -0
  67. package/dist/phases/user-psychology/prompts.js +41 -0
  68. package/dist/services/audit-logs.js +67 -9
  69. package/dist/services/branches.js +90 -14
  70. package/dist/services/phase-ratings.js +71 -9
  71. package/dist/services/product-logs.js +65 -5
  72. package/dist/services/pull-requests.js +74 -14
  73. package/dist/skills/phase/screen-flow/SKILL.md +78 -0
  74. package/dist/skills/phase/user-psychology/SKILL.md +135 -0
  75. package/dist/supabase/client.d.ts +23 -0
  76. package/dist/supabase/client.js +90 -0
  77. package/dist/system/session-manager.js +97 -24
  78. package/dist/types/index.d.ts +3 -0
  79. package/dist/utils/logger.js +24 -4
  80. package/package.json +4 -3
  81. 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 { callMcpEndpoint } from '../../api/mcp-client.js';
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
- const product = (await callMcpEndpoint('products/get', {
23
- product_id: productId,
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
- await callMcpEndpoint('product_user_feedbacks/create', {
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
- const testReportResult = (await callMcpEndpoint('test_reports/latest', {
25
- issue_id: issueId,
26
- }));
27
- if (testReportResult.test_report) {
28
- latestTestReport = testReportResult.test_report;
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
- const result = (await callMcpEndpoint('product_user_feedbacks/list_for_product', {
215
- product_id: productId,
216
- since: sinceIso,
217
- }));
218
- const rows = result.feedbacks || [];
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
- const result = (await callMcpEndpoint('intelligence/reports/list', {
236
- product_id: productId,
237
- }));
238
- let reports = result.reports || [];
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