edsger 0.56.3 → 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 +4 -3
- package/vitest.config.ts +1 -0
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
if (typeof value.slug !== 'string' || value.slug.length === 0)
|
|
26
|
+
return false;
|
|
27
|
+
if (typeof value.name !== 'string' || value.name.length === 0)
|
|
28
|
+
return false;
|
|
29
|
+
if (typeof value.kind !== 'string' || !SCREEN_KINDS.has(value.kind)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (typeof value.layout !== 'string')
|
|
33
|
+
return false;
|
|
34
|
+
if (!Array.isArray(value.body))
|
|
35
|
+
return false;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
function isScreenEdge(value) {
|
|
39
|
+
if (!isRecord(value))
|
|
40
|
+
return false;
|
|
41
|
+
if (typeof value.fromSlug !== 'string')
|
|
42
|
+
return false;
|
|
43
|
+
if (typeof value.toSlug !== 'string')
|
|
44
|
+
return false;
|
|
45
|
+
if (typeof value.triggerLabel !== 'string')
|
|
46
|
+
return false;
|
|
47
|
+
if (typeof value.kind !== 'string' || !EDGE_KINDS.has(value.kind)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
export function isScreenFlowExtraction(value) {
|
|
53
|
+
if (!isRecord(value))
|
|
54
|
+
return false;
|
|
55
|
+
if (typeof value.summary !== 'string')
|
|
56
|
+
return false;
|
|
57
|
+
if (!Array.isArray(value.nodes))
|
|
58
|
+
return false;
|
|
59
|
+
if (!Array.isArray(value.edges))
|
|
60
|
+
return false;
|
|
61
|
+
if (!value.nodes.every(isScreenSchema))
|
|
62
|
+
return false;
|
|
63
|
+
if (!value.edges.every(isScreenEdge))
|
|
64
|
+
return false;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
@@ -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,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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
164
|
+
return logId;
|
|
107
165
|
}
|
|
108
166
|
catch (error) {
|
|
109
167
|
// Always log errors, not just in verbose mode
|