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,81 @@
1
+ /**
2
+ * Screen Flow domain types.
3
+ *
4
+ * A ScreenSchema is a structured, framework-agnostic description of one
5
+ * screen in a product. The CLI extracts these from source code and the
6
+ * desktop renders them with a unified <ScreenPreview> component — that's
7
+ * what gives every flow a consistent visual style regardless of the
8
+ * underlying app's design system.
9
+ *
10
+ * The schema deliberately stays high-level (sections, not pixels). When the
11
+ * agent encounters something it can't model cleanly, it falls back to
12
+ * `{ type: 'custom', label }` rather than guessing.
13
+ */
14
+ // ============================================================================
15
+ // Runtime validation for AI-produced extraction
16
+ // ============================================================================
17
+ const SCREEN_KINDS = new Set(['page', 'modal', 'drawer', 'tab', 'state']);
18
+ const EDGE_KINDS = new Set(['navigate', 'modal', 'redirect', 'back']);
19
+ function isRecord(value) {
20
+ return typeof value === 'object' && value !== null;
21
+ }
22
+ function isScreenSchema(value) {
23
+ if (!isRecord(value)) {
24
+ return false;
25
+ }
26
+ if (typeof value.slug !== 'string' || value.slug.length === 0) {
27
+ return false;
28
+ }
29
+ if (typeof value.name !== 'string' || value.name.length === 0) {
30
+ return false;
31
+ }
32
+ if (typeof value.kind !== 'string' || !SCREEN_KINDS.has(value.kind)) {
33
+ return false;
34
+ }
35
+ if (typeof value.layout !== 'string') {
36
+ return false;
37
+ }
38
+ if (!Array.isArray(value.body)) {
39
+ return false;
40
+ }
41
+ return true;
42
+ }
43
+ function isScreenEdge(value) {
44
+ if (!isRecord(value)) {
45
+ return false;
46
+ }
47
+ if (typeof value.fromSlug !== 'string') {
48
+ return false;
49
+ }
50
+ if (typeof value.toSlug !== 'string') {
51
+ return false;
52
+ }
53
+ if (typeof value.triggerLabel !== 'string') {
54
+ return false;
55
+ }
56
+ if (typeof value.kind !== 'string' || !EDGE_KINDS.has(value.kind)) {
57
+ return false;
58
+ }
59
+ return true;
60
+ }
61
+ export function isScreenFlowExtraction(value) {
62
+ if (!isRecord(value)) {
63
+ return false;
64
+ }
65
+ if (typeof value.summary !== 'string') {
66
+ return false;
67
+ }
68
+ if (!Array.isArray(value.nodes)) {
69
+ return false;
70
+ }
71
+ if (!Array.isArray(value.edges)) {
72
+ return false;
73
+ }
74
+ if (!value.nodes.every(isScreenSchema)) {
75
+ return false;
76
+ }
77
+ if (!value.edges.every(isScreenEdge)) {
78
+ return false;
79
+ }
80
+ return true;
81
+ }
@@ -0,0 +1,16 @@
1
+ import { type EdsgerConfig } from '../../types/index.js';
2
+ interface ParsedResult {
3
+ analysis?: any;
4
+ error?: string;
5
+ }
6
+ /**
7
+ * Parse the AI's response. Uses the shared multi-strategy extractor so
8
+ * we tolerate the common AI quirks:
9
+ * 1. ```json fenced block
10
+ * 2. A bare JSON object containing `"analysis"`
11
+ * 3. The entire response is JSON
12
+ * Exported for testability.
13
+ */
14
+ export declare function parsePsychologyResult(responseText: string): ParsedResult;
15
+ export declare function executeUserPsychologyQuery(currentPrompt: string, systemPrompt: string, config: EdsgerConfig, verbose?: boolean, cwd?: string): Promise<Record<string, unknown> | null>;
16
+ export {};
@@ -0,0 +1,105 @@
1
+ import { query } from '@anthropic-ai/claude-agent-sdk';
2
+ import { DEFAULT_MODEL } from '../../constants.js';
3
+ import { extractJsonFromResponse } from '../../utils/json-extractor.js';
4
+ import { logDebug, logError, logInfo } from '../../utils/logger.js';
5
+ /**
6
+ * Parse the AI's response. Uses the shared multi-strategy extractor so
7
+ * we tolerate the common AI quirks:
8
+ * 1. ```json fenced block
9
+ * 2. A bare JSON object containing `"analysis"`
10
+ * 3. The entire response is JSON
11
+ * Exported for testability.
12
+ */
13
+ export function parsePsychologyResult(responseText) {
14
+ const json = extractJsonFromResponse(responseText, '"analysis"');
15
+ if (!json) {
16
+ return {
17
+ error: 'JSON parsing failed: no JSON object containing "analysis" found in response',
18
+ };
19
+ }
20
+ if (json.analysis) {
21
+ return { analysis: json.analysis };
22
+ }
23
+ return { error: 'Invalid JSON structure (missing `analysis` key)' };
24
+ }
25
+ function userMessage(content) {
26
+ return {
27
+ type: 'user',
28
+ message: { role: 'user', content },
29
+ };
30
+ }
31
+ // eslint-disable-next-line @typescript-eslint/require-await -- async generator required by SDK interface
32
+ async function* prompt(analysisPrompt) {
33
+ yield userMessage(analysisPrompt);
34
+ }
35
+ export async function executeUserPsychologyQuery(currentPrompt, systemPrompt, config, verbose, cwd) {
36
+ let lastAssistantResponse = '';
37
+ let structuredResult = null;
38
+ let turnCount = 0;
39
+ logInfo('Connecting to AI agent...');
40
+ for await (const message of query({
41
+ prompt: prompt(currentPrompt),
42
+ options: {
43
+ systemPrompt: {
44
+ type: 'preset',
45
+ preset: 'claude_code',
46
+ append: systemPrompt,
47
+ },
48
+ model: DEFAULT_MODEL,
49
+ maxTurns: 1000,
50
+ permissionMode: 'bypassPermissions',
51
+ ...(cwd ? { cwd } : {}),
52
+ },
53
+ })) {
54
+ if (verbose) {
55
+ logInfo(`Received message type: ${message.type}`);
56
+ }
57
+ if (message.type === 'assistant' && message.message?.content) {
58
+ turnCount++;
59
+ for (const content of message.message.content) {
60
+ if (content.type === 'text') {
61
+ lastAssistantResponse += `${content.text}\n`;
62
+ logDebug(`${content.text}`, verbose);
63
+ }
64
+ else if (content.type === 'tool_use') {
65
+ const input = (content.input ?? {});
66
+ const desc = input.description ?? input.command ?? 'Running...';
67
+ logInfo(`[Turn ${turnCount}] ${content.name}: ${typeof desc === 'string' ? desc.slice(0, 120) : 'Running...'}`);
68
+ }
69
+ }
70
+ }
71
+ if (message.type === 'result') {
72
+ if (message.subtype === 'success') {
73
+ logInfo(`\nUser psychology analysis completed after ${turnCount} turns, parsing results...`);
74
+ const responseText = message.result || lastAssistantResponse;
75
+ const parsed = parsePsychologyResult(responseText);
76
+ if (parsed.error) {
77
+ logError(`Failed to parse psychology result: ${parsed.error}`);
78
+ structuredResult = {
79
+ status: 'error',
80
+ analysis_content: 'Failed to parse analysis results',
81
+ target_personas: [],
82
+ jobs_to_be_done: [],
83
+ pain_points: [],
84
+ motivations: null,
85
+ behavior_triggers: [],
86
+ messaging_angles: [],
87
+ };
88
+ }
89
+ else {
90
+ structuredResult = parsed.analysis;
91
+ }
92
+ }
93
+ else {
94
+ logError(`\nAnalysis incomplete: ${message.subtype}`);
95
+ if (lastAssistantResponse) {
96
+ const parsed = parsePsychologyResult(lastAssistantResponse);
97
+ if (!parsed.error) {
98
+ structuredResult = parsed.analysis;
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+ return structuredResult;
105
+ }
@@ -0,0 +1,10 @@
1
+ import { type ProductInfo } from '../../api/products.js';
2
+ export interface UserPsychologyContext {
3
+ product: ProductInfo;
4
+ }
5
+ export declare function fetchUserPsychologyContext(productId: string, verbose?: boolean): Promise<UserPsychologyContext>;
6
+ export declare function formatContextForPrompt(context: UserPsychologyContext, guidance?: string): string;
7
+ export declare function prepareUserPsychologyContext(productId: string, verbose?: boolean, hasCodebase?: boolean, guidance?: string): Promise<{
8
+ context: UserPsychologyContext;
9
+ analysisPrompt: string;
10
+ }>;
@@ -0,0 +1,65 @@
1
+ import { getProduct } from '../../api/products.js';
2
+ import { logError, logInfo } from '../../utils/logger.js';
3
+ import { createUserPsychologyPromptWithContext } from './prompts.js';
4
+ export async function fetchUserPsychologyContext(productId, verbose) {
5
+ try {
6
+ if (verbose) {
7
+ logInfo(`Fetching psychology context for product: ${productId}`);
8
+ }
9
+ const product = await getProduct(productId, verbose);
10
+ if (verbose) {
11
+ logInfo(` Product: ${product.name}`);
12
+ logInfo(` Issues: ${product.issues?.length || 0}`);
13
+ }
14
+ return { product };
15
+ }
16
+ catch (error) {
17
+ const errorMessage = error instanceof Error ? error.message : String(error);
18
+ logError(`Failed to fetch psychology context: ${errorMessage}`);
19
+ throw new Error(`Context fetch failed: ${errorMessage}`);
20
+ }
21
+ }
22
+ export function formatContextForPrompt(context, guidance) {
23
+ const { product } = context;
24
+ const issuesList = product.issues && product.issues.length > 0
25
+ ? product.issues
26
+ .map((f) => `- **${f.name}**: ${f.description || 'No description'} (Status: ${f.status || 'unknown'})`)
27
+ .join('\n')
28
+ : 'No issues listed.';
29
+ const guidanceSection = guidance
30
+ ? `
31
+
32
+ ## Human Research Guidance
33
+ The product owner has provided the following direction. **You MUST honour this guidance** — it overrides your defaults for which audience segment, market, or use case to focus on.
34
+
35
+ ${guidance}
36
+
37
+ ---`
38
+ : '';
39
+ return `# User Psychology Research Context
40
+
41
+ ## Product Information
42
+ - **Name**: ${product.name}
43
+ - **Product ID**: ${product.id}
44
+ - **Description**: ${product.description || 'No description provided'}
45
+
46
+ ## Product Issues / Capabilities (${product.issues?.length || 0})
47
+ ${issuesList}
48
+ ${guidanceSection}
49
+
50
+ ---
51
+
52
+ **Important**: Build the user psychology profile for the people who would actually use the product described above. Personas, jobs-to-be-done, and pain points must be traceable to the capabilities listed — do not invent users the product cannot serve.${guidance ? ' Honour the human research guidance above.' : ''}`;
53
+ }
54
+ export async function prepareUserPsychologyContext(productId, verbose, hasCodebase = false, guidance) {
55
+ if (verbose) {
56
+ logInfo('Fetching user psychology context...');
57
+ if (guidance) {
58
+ logInfo(`Human guidance: "${guidance.substring(0, 80)}${guidance.length > 80 ? '...' : ''}"`);
59
+ }
60
+ }
61
+ const context = await fetchUserPsychologyContext(productId, verbose);
62
+ const contextInfo = formatContextForPrompt(context, guidance);
63
+ const analysisPrompt = createUserPsychologyPromptWithContext(productId, contextInfo, hasCodebase);
64
+ return { context, analysisPrompt };
65
+ }
@@ -0,0 +1,18 @@
1
+ import { type EdsgerConfig } from '../../types/index.js';
2
+ export interface UserPsychologyOptions {
3
+ productId: string;
4
+ /** Required. Desktop UI always reserves a pending row first. */
5
+ analysisId: string;
6
+ verbose?: boolean;
7
+ guidance?: string;
8
+ }
9
+ export interface UserPsychologyResult {
10
+ productId: string;
11
+ status: 'success' | 'error';
12
+ summary: string;
13
+ analysisId?: string;
14
+ personaCount: number;
15
+ jobCount: number;
16
+ painPointCount: number;
17
+ }
18
+ export declare const analyseUserPsychology: (options: UserPsychologyOptions, config: EdsgerConfig) => Promise<UserPsychologyResult>;
@@ -0,0 +1,96 @@
1
+ import { getGitHubConfigByProduct } from '../../api/github.js';
2
+ import { markUserPsychologyAnalysisFailed, updateUserPsychologyAnalysis, } from '../../api/user-psychology.js';
3
+ import { logError, logInfo } from '../../utils/logger.js';
4
+ import { cleanupIssueRepo, cloneIssueRepo, ensureWorkspaceDir, } from '../../workspace/workspace-manager.js';
5
+ import { executeUserPsychologyQuery } from './agent.js';
6
+ import { prepareUserPsychologyContext } from './context.js';
7
+ import { createUserPsychologySystemPrompt } from './prompts.js';
8
+ export const analyseUserPsychology = async (options, config) => {
9
+ const { productId, analysisId, verbose, guidance } = options;
10
+ if (verbose) {
11
+ logInfo(`Starting user psychology analysis for product ID: ${productId}`);
12
+ }
13
+ let repoCwd;
14
+ let analysisSucceeded = false;
15
+ try {
16
+ // Clone product repo if GitHub is configured. The whole point of this
17
+ // analysis is to ground claims in the actual codebase, so we try hard
18
+ // but degrade gracefully if no repo is connected.
19
+ try {
20
+ const githubConfig = await getGitHubConfigByProduct(productId, verbose);
21
+ if (githubConfig.configured &&
22
+ githubConfig.token &&
23
+ githubConfig.owner &&
24
+ githubConfig.repo) {
25
+ const workspaceRoot = ensureWorkspaceDir();
26
+ const { repoPath } = cloneIssueRepo(workspaceRoot, `psychology-${productId}`, githubConfig.owner, githubConfig.repo, githubConfig.token);
27
+ repoCwd = repoPath;
28
+ logInfo(`Repository cloned to: ${repoCwd}`);
29
+ }
30
+ else {
31
+ logInfo(`No GitHub repo configured for product, running without codebase access. ${githubConfig.message || ''}`);
32
+ }
33
+ }
34
+ catch (error) {
35
+ logInfo(`Could not clone repo (continuing without codebase): ${error instanceof Error ? error.message : String(error)}`);
36
+ }
37
+ const hasCodebase = !!repoCwd;
38
+ const { analysisPrompt } = await prepareUserPsychologyContext(productId, verbose, hasCodebase, guidance);
39
+ const systemPrompt = await createUserPsychologySystemPrompt(hasCodebase, repoCwd);
40
+ if (verbose) {
41
+ logInfo('Starting AI query for user psychology analysis...');
42
+ }
43
+ const analysisResult = await executeUserPsychologyQuery(analysisPrompt, systemPrompt, config, verbose, repoCwd);
44
+ if (!analysisResult) {
45
+ throw new Error('No analysis results received');
46
+ }
47
+ const analysisContent = analysisResult.analysis_content || 'Analysis completed';
48
+ const targetPersonas = (analysisResult.target_personas || []);
49
+ const jobsToBeDone = (analysisResult.jobs_to_be_done || []);
50
+ const painPoints = (analysisResult.pain_points || []);
51
+ const motivations = (analysisResult.motivations || null);
52
+ const behaviorTriggers = (analysisResult.behavior_triggers ||
53
+ []);
54
+ const messagingAngles = (analysisResult.messaging_angles || []);
55
+ const saved = await updateUserPsychologyAnalysis(analysisId, {
56
+ analysis_content: analysisContent,
57
+ target_personas: targetPersonas,
58
+ jobs_to_be_done: jobsToBeDone,
59
+ pain_points: painPoints,
60
+ motivations,
61
+ behavior_triggers: behaviorTriggers,
62
+ messaging_angles: messagingAngles,
63
+ status: 'completed',
64
+ }, verbose);
65
+ logInfo(`Psychology analysis completed: ${targetPersonas.length} personas, ${jobsToBeDone.length} jobs, ${painPoints.length} pain points`);
66
+ analysisSucceeded = true;
67
+ return {
68
+ productId,
69
+ status: 'success',
70
+ summary: analysisContent,
71
+ analysisId: saved?.id ?? analysisId,
72
+ personaCount: targetPersonas.length,
73
+ jobCount: jobsToBeDone.length,
74
+ painPointCount: painPoints.length,
75
+ };
76
+ }
77
+ catch (error) {
78
+ const message = error instanceof Error ? error.message : String(error);
79
+ logError(`User psychology analysis failed: ${message}`);
80
+ await markUserPsychologyAnalysisFailed(analysisId, message, verbose);
81
+ return {
82
+ productId,
83
+ status: 'error',
84
+ summary: `Analysis failed: ${message}`,
85
+ analysisId,
86
+ personaCount: 0,
87
+ jobCount: 0,
88
+ painPointCount: 0,
89
+ };
90
+ }
91
+ finally {
92
+ if (analysisSucceeded) {
93
+ cleanupIssueRepo(repoCwd);
94
+ }
95
+ }
96
+ };
@@ -0,0 +1,2 @@
1
+ export declare const createUserPsychologySystemPrompt: (hasCodebase?: boolean, projectDir?: string) => Promise<string>;
2
+ export declare const createUserPsychologyPromptWithContext: (productId: string, contextInfo: string, hasCodebase?: boolean) => string;
@@ -0,0 +1,41 @@
1
+ import { processConditionals, resolveSkill, } from '../../services/skill-resolver.js';
2
+ import { OUTPUT_CONTRACTS } from '../output-contracts.js';
3
+ export const createUserPsychologySystemPrompt = async (hasCodebase = false, projectDir) => {
4
+ const skill = await resolveSkill('phase/user-psychology', { projectDir });
5
+ if (!skill) {
6
+ throw new Error('Failed to load skill: phase/user-psychology');
7
+ }
8
+ let { prompt } = skill;
9
+ prompt = processConditionals(prompt, { hasCodebase });
10
+ return `${prompt}
11
+
12
+ ${OUTPUT_CONTRACTS['user-psychology']}`;
13
+ };
14
+ export const createUserPsychologyPromptWithContext = (productId, contextInfo, hasCodebase = false) => {
15
+ const codebaseInstructions = hasCodebase
16
+ ? `
17
+ **Step 1 - Explore the codebase FIRST**: Before writing any persona or job, use your file reading tools to actually open this project. Look at:
18
+ - README.md, CLAUDE.md, package.json for what the product claims to be
19
+ - Landing pages, marketing copy, documentation
20
+ - The 3-5 highest-leverage source files / features
21
+ - Any UI flows that hint at the user's workflow
22
+
23
+ **Step 2 - Build the profile**: Now, using what you discovered AND the context below:`
24
+ : `
25
+ **Build the profile**: Using the product context below:`;
26
+ return `Please produce a rigorous user psychology profile for product ID: ${productId}
27
+
28
+ ${contextInfo}
29
+
30
+ ## Analysis Instructions
31
+ ${codebaseInstructions}
32
+ 1. Apply all 6 frameworks defined in your system prompt (Personas, JTBD, Pain Points, Motivations, Behavior Triggers, Messaging Angles).
33
+ 2. Every persona, job, and pain you produce must be traceable to a specific feature, file, or context line — cite the evidence inline.
34
+ 3. Cover at least one EMOTIONAL and one SOCIAL job in jobs_to_be_done — not only functional ones.
35
+ 4. Pain points must be written in the USER'S voice ("I keep losing track of…"), not yours ("users struggle with…").
36
+ 5. Messaging angles must each tie back to a specific persona name and a specific JTBD statement.
37
+
38
+ **CRITICAL**: No placeholders. No "[insert role]". No generic "users want to be productive." Be concrete or be silent.
39
+
40
+ Return ONLY the JSON response as specified in your instructions.`;
41
+ };
@@ -3,13 +3,26 @@
3
3
  * Provides functions to log issue lifecycle events to the database
4
4
  */
5
5
  import { callMcpEndpoint } from '../api/mcp-client.js';
6
+ import { getSupabase, hasSupabaseSession } from '../supabase/client.js';
6
7
  import { logError, logInfo } from '../utils/logger.js';
8
+ /** Insert one row into issue_audit_logs and return its id, or null on failure. */
9
+ async function insertAuditLogSdk(payload) {
10
+ const { data, error } = await getSupabase()
11
+ .from('issue_audit_logs')
12
+ .insert(payload)
13
+ .select('id')
14
+ .single();
15
+ if (error) {
16
+ throw new Error(error.message);
17
+ }
18
+ return data?.id ?? null;
19
+ }
7
20
  /**
8
21
  * Log a phase event (start, completion, or failure)
9
22
  */
10
23
  export async function logIssuePhaseEvent(params, verbose) {
11
24
  try {
12
- const result = await callMcpEndpoint('issue_audit_logs/create', {
25
+ const payload = {
13
26
  issue_id: params.issueId,
14
27
  event_type: params.eventType,
15
28
  phase: params.phase,
@@ -17,7 +30,22 @@ export async function logIssuePhaseEvent(params, verbose) {
17
30
  result: params.result || 'info',
18
31
  metadata: params.metadata || {},
19
32
  error_message: params.errorMessage || null,
20
- });
33
+ };
34
+ let logId = null;
35
+ let usedSdk = false;
36
+ if (hasSupabaseSession()) {
37
+ try {
38
+ logId = await insertAuditLogSdk(payload);
39
+ usedSdk = true;
40
+ }
41
+ catch {
42
+ // Fall through to MCP
43
+ }
44
+ }
45
+ if (!usedSdk) {
46
+ const result = await callMcpEndpoint('issue_audit_logs/create', payload);
47
+ logId = result?.id || null;
48
+ }
21
49
  if (verbose) {
22
50
  logInfo(`✅ Logged ${params.eventType} for phase ${params.phase}`);
23
51
  }
@@ -41,7 +69,7 @@ export async function logIssuePhaseEvent(params, verbose) {
41
69
  }
42
70
  }
43
71
  }
44
- return result?.id || null;
72
+ return logId;
45
73
  }
46
74
  catch (error) {
47
75
  // Always log errors, not just in verbose mode
@@ -57,7 +85,7 @@ export async function logIssuePhaseEvent(params, verbose) {
57
85
  */
58
86
  export async function logIssueChecklistEvent(params, verbose) {
59
87
  try {
60
- const result = await callMcpEndpoint('issue_audit_logs/create', {
88
+ const payload = {
61
89
  issue_id: params.issueId,
62
90
  event_type: params.eventType,
63
91
  source: 'pipeline',
@@ -66,11 +94,26 @@ export async function logIssueChecklistEvent(params, verbose) {
66
94
  checklist_id: params.checklistId,
67
95
  ...(params.metadata || {}),
68
96
  },
69
- });
97
+ };
98
+ let logId = null;
99
+ let usedSdk = false;
100
+ if (hasSupabaseSession()) {
101
+ try {
102
+ logId = await insertAuditLogSdk(payload);
103
+ usedSdk = true;
104
+ }
105
+ catch {
106
+ // Fall through to MCP
107
+ }
108
+ }
109
+ if (!usedSdk) {
110
+ const result = await callMcpEndpoint('issue_audit_logs/create', payload);
111
+ logId = result?.id || null;
112
+ }
70
113
  if (verbose) {
71
114
  logInfo(`✅ Logged ${params.eventType} for checklist ${params.checklistId}`);
72
115
  }
73
- return result?.id || null;
116
+ return logId;
74
117
  }
75
118
  catch (error) {
76
119
  // Always log errors, not just in verbose mode
@@ -89,7 +132,7 @@ export async function logIssueVerificationEvent(params, verbose) {
89
132
  const eventType = params.result === 'success'
90
133
  ? 'verification_passed'
91
134
  : 'verification_failed';
92
- const result = await callMcpEndpoint('issue_audit_logs/create', {
135
+ const payload = {
93
136
  issue_id: params.issueId,
94
137
  event_type: eventType,
95
138
  phase: params.phase,
@@ -99,11 +142,26 @@ export async function logIssueVerificationEvent(params, verbose) {
99
142
  iteration: params.iteration,
100
143
  ...params.verificationData,
101
144
  },
102
- });
145
+ };
146
+ let logId = null;
147
+ let usedSdk = false;
148
+ if (hasSupabaseSession()) {
149
+ try {
150
+ logId = await insertAuditLogSdk(payload);
151
+ usedSdk = true;
152
+ }
153
+ catch {
154
+ // Fall through to MCP
155
+ }
156
+ }
157
+ if (!usedSdk) {
158
+ const result = await callMcpEndpoint('issue_audit_logs/create', payload);
159
+ logId = result?.id || null;
160
+ }
103
161
  if (verbose) {
104
162
  logInfo(`✅ Logged verification ${params.result} for phase ${params.phase} (iteration ${params.iteration})`);
105
163
  }
106
- return result?.id || null;
164
+ return logId;
107
165
  }
108
166
  catch (error) {
109
167
  // Always log errors, not just in verbose mode