@zibby/core 0.1.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 (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/package.json +94 -0
  4. package/src/agents/base.js +361 -0
  5. package/src/constants.js +47 -0
  6. package/src/enrichment/base.js +49 -0
  7. package/src/enrichment/enrichers/accessibility-enricher.js +197 -0
  8. package/src/enrichment/enrichers/dom-enricher.js +171 -0
  9. package/src/enrichment/enrichers/page-state-enricher.js +129 -0
  10. package/src/enrichment/enrichers/position-enricher.js +67 -0
  11. package/src/enrichment/index.js +96 -0
  12. package/src/enrichment/mcp-integration.js +149 -0
  13. package/src/enrichment/mcp-ref-enricher.js +78 -0
  14. package/src/enrichment/pipeline.js +192 -0
  15. package/src/enrichment/trace-text-enricher.js +115 -0
  16. package/src/framework/AGENTS.md +98 -0
  17. package/src/framework/agents/base.js +72 -0
  18. package/src/framework/agents/claude-strategy.js +278 -0
  19. package/src/framework/agents/cursor-strategy.js +459 -0
  20. package/src/framework/agents/index.js +105 -0
  21. package/src/framework/agents/utils/cursor-output-formatter.js +67 -0
  22. package/src/framework/agents/utils/openai-proxy-formatter.js +249 -0
  23. package/src/framework/code-generator.js +301 -0
  24. package/src/framework/constants.js +33 -0
  25. package/src/framework/context-loader.js +101 -0
  26. package/src/framework/function-bridge.js +78 -0
  27. package/src/framework/function-skill-registry.js +20 -0
  28. package/src/framework/graph-compiler.js +342 -0
  29. package/src/framework/graph.js +610 -0
  30. package/src/framework/index.js +28 -0
  31. package/src/framework/node-registry.js +163 -0
  32. package/src/framework/node.js +259 -0
  33. package/src/framework/output-parser.js +71 -0
  34. package/src/framework/skill-registry.js +55 -0
  35. package/src/framework/state-utils.js +52 -0
  36. package/src/framework/state.js +67 -0
  37. package/src/framework/tool-resolver.js +65 -0
  38. package/src/index.js +342 -0
  39. package/src/runtime/generation/base.js +46 -0
  40. package/src/runtime/generation/index.js +70 -0
  41. package/src/runtime/generation/mcp-ref-strategy.js +197 -0
  42. package/src/runtime/generation/stable-id-strategy.js +170 -0
  43. package/src/runtime/stable-id-runtime.js +248 -0
  44. package/src/runtime/verification/base.js +44 -0
  45. package/src/runtime/verification/index.js +67 -0
  46. package/src/runtime/verification/playwright-json-strategy.js +119 -0
  47. package/src/runtime/zibby-runtime.js +299 -0
  48. package/src/sync/index.js +2 -0
  49. package/src/sync/uploader.js +29 -0
  50. package/src/tools/run-playwright-test.js +158 -0
  51. package/src/utils/adf-converter.js +68 -0
  52. package/src/utils/ast-utils.js +37 -0
  53. package/src/utils/ci-setup.js +124 -0
  54. package/src/utils/cursor-utils.js +71 -0
  55. package/src/utils/logger.js +144 -0
  56. package/src/utils/mcp-config-writer.js +115 -0
  57. package/src/utils/node-schema-parser.js +522 -0
  58. package/src/utils/post-process-events.js +55 -0
  59. package/src/utils/result-handler.js +102 -0
  60. package/src/utils/ripple-effect.js +84 -0
  61. package/src/utils/selector-generator.js +239 -0
  62. package/src/utils/streaming-parser.js +387 -0
  63. package/src/utils/test-post-processor.js +211 -0
  64. package/src/utils/timeline.js +217 -0
  65. package/src/utils/trace-parser.js +325 -0
  66. package/src/utils/video-organizer.js +91 -0
  67. package/templates/browser-test-automation/README.md +114 -0
  68. package/templates/browser-test-automation/graph.js +54 -0
  69. package/templates/browser-test-automation/nodes/execute-live.js +250 -0
  70. package/templates/browser-test-automation/nodes/generate-script.js +77 -0
  71. package/templates/browser-test-automation/nodes/index.js +3 -0
  72. package/templates/browser-test-automation/nodes/preflight.js +59 -0
  73. package/templates/browser-test-automation/nodes/utils.js +154 -0
  74. package/templates/browser-test-automation/result-handler.js +286 -0
  75. package/templates/code-analysis/graph.js +72 -0
  76. package/templates/code-analysis/index.js +18 -0
  77. package/templates/code-analysis/nodes/analyze-ticket-node.js +204 -0
  78. package/templates/code-analysis/nodes/create-pr-node.js +175 -0
  79. package/templates/code-analysis/nodes/finalize-node.js +118 -0
  80. package/templates/code-analysis/nodes/generate-code-node.js +425 -0
  81. package/templates/code-analysis/nodes/generate-test-cases-node.js +376 -0
  82. package/templates/code-analysis/nodes/services/prMetaService.js +86 -0
  83. package/templates/code-analysis/nodes/setup-node.js +142 -0
  84. package/templates/code-analysis/prompts/analyze-ticket.md +181 -0
  85. package/templates/code-analysis/prompts/generate-code.md +33 -0
  86. package/templates/code-analysis/prompts/generate-test-cases.md +110 -0
  87. package/templates/code-analysis/state.js +40 -0
  88. package/templates/code-implementation/graph.js +35 -0
  89. package/templates/code-implementation/index.js +7 -0
  90. package/templates/code-implementation/state.js +14 -0
  91. package/templates/global-setup.js +56 -0
  92. package/templates/index.js +94 -0
  93. package/templates/register-nodes.js +24 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * MCP Recorder Integration with Enrichment Pipeline
3
+ *
4
+ * This module provides utilities to enrich MCP-recorded events
5
+ * after test execution (when we have access to trace data)
6
+ */
7
+
8
+ import { createDefaultPipeline, createMinimalPipeline } from './index.js';
9
+ import { readFileSync, writeFileSync } from 'fs';
10
+ import { join } from 'path';
11
+
12
+ /**
13
+ * Enrich recorded events.json file with data from Playwright trace
14
+ *
15
+ * @param {string} sessionPath - Path to session directory
16
+ * @param {Object} config - Enrichment configuration
17
+ * @returns {Promise<Object>} - { enrichedCount, skippedCount, errors }
18
+ */
19
+ export async function enrichRecordedEvents(sessionPath, _config = {}) {
20
+ const eventsPath = join(sessionPath, 'events.json');
21
+ const _tracePath = join(sessionPath, 'trace.zip');
22
+
23
+ try {
24
+ // Read events
25
+ const events = JSON.parse(readFileSync(eventsPath, 'utf-8'));
26
+
27
+ // For now, we add placeholder enrichment metadata
28
+ // Full enrichment requires re-playing with Playwright access
29
+ const enriched = events.map(event => ({
30
+ ...event,
31
+ _enrichmentNote: 'Full enrichment requires live Playwright access. Use EnrichmentPipeline during test execution.'
32
+ }));
33
+
34
+ // Save enriched events
35
+ const backupPath = `${eventsPath }.backup`;
36
+ writeFileSync(backupPath, JSON.stringify(events, null, 2));
37
+ writeFileSync(eventsPath, JSON.stringify(enriched, null, 2));
38
+
39
+ return {
40
+ enrichedCount: enriched.length,
41
+ skippedCount: 0,
42
+ errors: []
43
+ };
44
+ } catch (error) {
45
+ console.error('[EnrichmentIntegration] Failed to enrich events:', error.message);
46
+ return {
47
+ enrichedCount: 0,
48
+ skippedCount: 0,
49
+ errors: [error.message]
50
+ };
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Example: How to use enrichment pipeline during live test execution
56
+ *
57
+ * This is a reference implementation for integrating enrichment
58
+ * into your test execution flow where you have Playwright access.
59
+ */
60
+ export class LiveEnrichmentRecorder {
61
+ constructor(config = {}) {
62
+ // Create enrichment pipeline
63
+ this.pipeline = config.minimal
64
+ ? createMinimalPipeline(config)
65
+ : createDefaultPipeline(config);
66
+
67
+ this.events = [];
68
+ this.config = config;
69
+ }
70
+
71
+ /**
72
+ * Record and enrich an event during test execution
73
+ *
74
+ * @param {string} type - Event type (click, fill, etc)
75
+ * @param {Object} data - Event data
76
+ * @param {Object} context - { page, element, ref }
77
+ * @returns {Promise<Object>} - Enriched event
78
+ */
79
+ async recordEvent(type, data, context) {
80
+ // Create base event (MCP format)
81
+ const baseEvent = {
82
+ id: this.events.length,
83
+ type,
84
+ timestamp: new Date().toISOString(),
85
+ data
86
+ };
87
+
88
+ // Enrich with pipeline
89
+ const enriched = await this.pipeline.enrich(baseEvent, {
90
+ ...context,
91
+ event: baseEvent
92
+ });
93
+
94
+ this.events.push(enriched);
95
+ return enriched;
96
+ }
97
+
98
+ /**
99
+ * Save enriched events to file
100
+ * @param {string} filePath
101
+ */
102
+ saveEvents(filePath) {
103
+ writeFileSync(filePath, JSON.stringify(this.events, null, 2));
104
+ console.log(`[LiveEnrichment] Saved ${this.events.length} enriched events to ${filePath}`);
105
+
106
+ // Log statistics
107
+ this.pipeline.logStatus();
108
+ }
109
+
110
+ /**
111
+ * Get pipeline statistics
112
+ */
113
+ getStats() {
114
+ return this.pipeline.getStats();
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Example integration with Cursor Agent workflow
120
+ *
121
+ * Usage in execute_live node:
122
+ *
123
+ * ```javascript
124
+ * import { LiveEnrichmentRecorder } from '@zibby/core';
125
+ *
126
+ * const recorder = new LiveEnrichmentRecorder({ minimal: true });
127
+ *
128
+ * // During test execution (when you have page/element access):
129
+ * const page = await browser.newPage();
130
+ * const element = await page.locator('button');
131
+ *
132
+ * // Record enriched event
133
+ * await recorder.recordEvent('click', {
134
+ * tool: 'browser_click',
135
+ * params: { element: 'Submit button', ref: 'k123' }
136
+ * }, {
137
+ * page,
138
+ * element
139
+ * });
140
+ *
141
+ * // Save at end
142
+ * recorder.saveEvents('enriched-events.json');
143
+ * ```
144
+ */
145
+
146
+ export default {
147
+ enrichRecordedEvents,
148
+ LiveEnrichmentRecorder
149
+ };
@@ -0,0 +1,78 @@
1
+ import { EventEnricher } from './base.js';
2
+
3
+ /**
4
+ * MCPRefEnricher - Captures ACTUAL element text from trace DOM snapshots
5
+ *
6
+ * Problem: AI might say "Login button" but actual button text is "登录" (Chinese)
7
+ * Solution: Extract the REAL text from trace DOM snapshot at action time
8
+ *
9
+ * Priority order:
10
+ * 1. Extract actual text/label from trace DOM snapshot (handles Chinese/any language)
11
+ * 2. Fall back to MCP element description (AI's description)
12
+ *
13
+ * This ensures Chinese, Arabic, Japanese, etc. apps work perfectly!
14
+ */
15
+ export class MCPRefEnricher extends EventEnricher {
16
+ constructor(config = {}) {
17
+ super(config);
18
+ this.priority = 200; // Highest priority
19
+ }
20
+
21
+ getName() {
22
+ return 'MCPRef';
23
+ }
24
+
25
+ getPriority() {
26
+ return this.priority;
27
+ }
28
+
29
+ async enrich(event, context) {
30
+ const ref = event.data?.params?.ref;
31
+ const mcpElement = event.data?.params?.element; // What AI said
32
+
33
+ if (!ref && !mcpElement) {
34
+ return null;
35
+ }
36
+
37
+ // Try to extract ACTUAL element properties from the DOM (if we have element access)
38
+ let actualText = null;
39
+ let actualRole = null;
40
+ let actualLabel = null;
41
+
42
+ if (context?.element) {
43
+ try {
44
+ // Extract the REAL text that user sees (not what AI said)
45
+ const elementData = await context.element.evaluate(el => {
46
+ return {
47
+ text: el.textContent?.trim() || '',
48
+ innerText: el.innerText?.trim() || '',
49
+ value: el.value || '',
50
+ label: el.getAttribute('aria-label') || el.getAttribute('label') || '',
51
+ role: el.getAttribute('role') || el.tagName.toLowerCase(),
52
+ placeholder: el.getAttribute('placeholder') || '',
53
+ title: el.getAttribute('title') || ''
54
+ };
55
+ });
56
+
57
+ // Use the most specific text we can find
58
+ actualText = elementData.text || elementData.innerText || elementData.value || elementData.placeholder;
59
+ actualRole = elementData.role;
60
+ actualLabel = elementData.label || elementData.title;
61
+
62
+ console.log(`[MCPRefEnricher] ✅ Captured actual text: "${actualText}" (AI said: "${mcpElement}")`);
63
+ } catch (e) {
64
+ console.log(`[MCPRefEnricher] ⚠️ Could not extract actual text: ${e.message}`);
65
+ }
66
+ }
67
+
68
+ return {
69
+ mcpRef: ref,
70
+ mcpElement, // What AI said (might be translated)
71
+ actualText, // ✅ REAL text from DOM (Chinese/any language)
72
+ actualRole, // ✅ REAL role (button, link, textbox)
73
+ actualLabel, // ✅ REAL aria-label
74
+ // Use actual text if available, otherwise fall back to AI's description
75
+ recordedSelector: actualText || mcpElement
76
+ };
77
+ }
78
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Enrichment Pipeline Manager
3
+ * Orchestrates multiple enrichers to add data to events
4
+ */
5
+
6
+ export class EnrichmentPipeline {
7
+ constructor(config = {}) {
8
+ this.enrichers = [];
9
+ this.config = config;
10
+ this.stats = {
11
+ totalEvents: 0,
12
+ enrichedEvents: 0,
13
+ skippedEvents: 0,
14
+ errors: {}
15
+ };
16
+ }
17
+
18
+ /**
19
+ * Register an enricher
20
+ * @param {EventEnricher} enricher
21
+ */
22
+ register(enricher) {
23
+ this.enrichers.push(enricher);
24
+ // Sort by priority (highest first)
25
+ this.enrichers.sort((a, b) => b.getPriority() - a.getPriority());
26
+ return this;
27
+ }
28
+
29
+ /**
30
+ * Unregister an enricher by name
31
+ * @param {string} name
32
+ */
33
+ unregister(name) {
34
+ this.enrichers = this.enrichers.filter(e => e.getName() !== name);
35
+ return this;
36
+ }
37
+
38
+ /**
39
+ * Get enricher by name
40
+ * @param {string} name
41
+ */
42
+ get(name) {
43
+ return this.enrichers.find(e => e.getName() === name);
44
+ }
45
+
46
+ /**
47
+ * Enable/disable specific enricher
48
+ * @param {string} name
49
+ * @param {boolean} enabled
50
+ */
51
+ setEnabled(name, enabled) {
52
+ const enricher = this.get(name);
53
+ if (enricher) {
54
+ enricher.enabled = enabled;
55
+ }
56
+ return this;
57
+ }
58
+
59
+ /**
60
+ * Enrich an event through the pipeline
61
+ * @param {Object} event - MCP event
62
+ * @param {Object} context - { page, element, ref, session }
63
+ * @returns {Promise<Object>} - Enriched event
64
+ */
65
+ async enrich(event, context) {
66
+ this.stats.totalEvents++;
67
+
68
+ // Start with original event
69
+ const enriched = { ...event };
70
+
71
+ // Track which enrichers ran
72
+ const enrichersRun = [];
73
+ const enrichersSkipped = [];
74
+ const enrichersFailed = [];
75
+
76
+ // Run each enricher in priority order
77
+ for (const enricher of this.enrichers) {
78
+ try {
79
+ // Check if enricher can run
80
+ if (!enricher.canEnrich(context)) {
81
+ enrichersSkipped.push(enricher.getName());
82
+ continue;
83
+ }
84
+
85
+ // Run enricher
86
+ const startTime = Date.now();
87
+ const data = await enricher.enrich(event, context);
88
+ const duration = Date.now() - startTime;
89
+
90
+ if (data) {
91
+ // Merge enriched data
92
+ Object.assign(enriched, data);
93
+ enrichersRun.push({ name: enricher.getName(), duration });
94
+ } else {
95
+ enrichersSkipped.push(enricher.getName());
96
+ }
97
+ } catch (error) {
98
+ console.warn(`[EnrichmentPipeline] ${enricher.getName()} failed:`, error.message);
99
+ enrichersFailed.push(enricher.getName());
100
+
101
+ // Track errors
102
+ this.stats.errors[enricher.getName()] = (this.stats.errors[enricher.getName()] || 0) + 1;
103
+ }
104
+ }
105
+
106
+ // Add enrichment metadata
107
+ enriched._enrichment = {
108
+ version: '1.0',
109
+ timestamp: new Date().toISOString(),
110
+ enrichers: {
111
+ run: enrichersRun,
112
+ skipped: enrichersSkipped,
113
+ failed: enrichersFailed
114
+ }
115
+ };
116
+
117
+ // Update stats
118
+ if (enrichersRun.length > 0) {
119
+ this.stats.enrichedEvents++;
120
+ } else {
121
+ this.stats.skippedEvents++;
122
+ }
123
+
124
+ return enriched;
125
+ }
126
+
127
+ /**
128
+ * Enrich multiple events in batch
129
+ * @param {Array} events
130
+ * @param {Object} context
131
+ * @returns {Promise<Array>}
132
+ */
133
+ async enrichBatch(events, context) {
134
+ const results = [];
135
+
136
+ for (const event of events) {
137
+ const enriched = await this.enrich(event, context);
138
+ results.push(enriched);
139
+ }
140
+
141
+ return results;
142
+ }
143
+
144
+ /**
145
+ * Get pipeline statistics
146
+ */
147
+ getStats() {
148
+ return {
149
+ ...this.stats,
150
+ enrichers: this.enrichers.map(e => ({
151
+ name: e.getName(),
152
+ enabled: e.enabled,
153
+ priority: e.getPriority(),
154
+ errors: this.stats.errors[e.getName()] || 0
155
+ }))
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Reset statistics
161
+ */
162
+ resetStats() {
163
+ this.stats = {
164
+ totalEvents: 0,
165
+ enrichedEvents: 0,
166
+ skippedEvents: 0,
167
+ errors: {}
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Log pipeline status
173
+ */
174
+ logStatus() {
175
+ console.log('\n📊 Enrichment Pipeline Status:');
176
+ console.log(` Total events: ${this.stats.totalEvents}`);
177
+ console.log(` Enriched: ${this.stats.enrichedEvents}`);
178
+ console.log(` Skipped: ${this.stats.skippedEvents}`);
179
+ console.log(`\n Registered enrichers (${this.enrichers.length}):`);
180
+
181
+ for (const enricher of this.enrichers) {
182
+ const status = enricher.enabled ? '✓' : '✗';
183
+ const errors = this.stats.errors[enricher.getName()] || 0;
184
+ const errorStr = errors > 0 ? ` (${errors} errors)` : '';
185
+ console.log(` ${status} ${enricher.getName()} (priority: ${enricher.getPriority()})${errorStr}`);
186
+ }
187
+
188
+ console.log();
189
+ }
190
+ }
191
+
192
+ export default EnrichmentPipeline;
@@ -0,0 +1,115 @@
1
+ import { EventEnricher } from './base.js';
2
+ import { TraceParser } from '../utils/trace-parser.js';
3
+ import { existsSync } from 'fs';
4
+ import { join } from 'path';
5
+
6
+ /**
7
+ * TraceTextEnricher - Extracts ACTUAL element text from Playwright trace
8
+ *
9
+ * Problem: AI might say "Login button" but actual DOM has "登录" (Chinese)
10
+ * Solution: Parse trace.zip to find the actual element text at action time
11
+ *
12
+ * This runs AFTER recording as a post-processing step.
13
+ */
14
+ export class TraceTextEnricher extends EventEnricher {
15
+ constructor(config = {}) {
16
+ super(config);
17
+ this.priority = 190; // Just below MCPRefEnricher
18
+ this.traceData = null;
19
+ }
20
+
21
+ getName() {
22
+ return 'TraceText';
23
+ }
24
+
25
+ getPriority() {
26
+ return this.priority;
27
+ }
28
+
29
+ /**
30
+ * Load trace data once for all events
31
+ */
32
+ async loadTrace(sessionPath) {
33
+ if (this.traceData) return;
34
+
35
+ const _tracePath = join(sessionPath, 'traces');
36
+ const traceZipPath = join(sessionPath, 'trace.zip');
37
+
38
+ if (existsSync(traceZipPath)) {
39
+ try {
40
+ this.traceData = await TraceParser.parseTraceZip(traceZipPath);
41
+ console.log(`[TraceTextEnricher] ✅ Loaded trace with ${this.traceData.length} actions`);
42
+ } catch (e) {
43
+ console.log(`[TraceTextEnricher] ⚠️ Failed to parse trace: ${e.message}`);
44
+ }
45
+ }
46
+ }
47
+
48
+ async enrich(event, context) {
49
+ const ref = event.data?.params?.ref;
50
+ const eventId = event.id;
51
+ if (ref === undefined && eventId === undefined) return null;
52
+
53
+ // Load trace on first event
54
+ if (!this.traceData && context.sessionPath) {
55
+ await this.loadTrace(context.sessionPath);
56
+ }
57
+
58
+ if (!this.traceData) return null;
59
+
60
+ // Match by sequence order: event.id maps to traceData[id]
61
+ // This is more reliable than matching by ref (which trace doesn't have)
62
+ const traceAction = this.traceData[eventId];
63
+ if (!traceAction) {
64
+ console.log(`[TraceTextEnricher] ⚠️ No trace action for event ${eventId}`);
65
+ return null;
66
+ }
67
+
68
+ // NEW: Use the actualText extracted from accessibility tree
69
+ const actualText = traceAction.actualText || this._extractTextFromSelector(traceAction.selector);
70
+ const actualRole = traceAction.actualRole;
71
+ const actualAriaLabel = traceAction.actualAriaLabel;
72
+
73
+ if (actualText || actualRole || actualAriaLabel) {
74
+ console.log(`[TraceTextEnricher] ✅ Event ${eventId}: text="${actualText}", role="${actualRole}", label="${actualAriaLabel}"`);
75
+ return {
76
+ traceActualText: actualText,
77
+ traceActualRole: actualRole,
78
+ traceActualAriaLabel: actualAriaLabel,
79
+ traceSelector: traceAction.selector,
80
+ traceStrategies: traceAction.strategies // Include all strategies
81
+ };
82
+ }
83
+
84
+ return null;
85
+ }
86
+
87
+ /**
88
+ * Extract actual text from Playwright's internal selector format
89
+ * Examples:
90
+ * - internal:label="登录" -> "登录"
91
+ * - getByText('Submit') -> "Submit"
92
+ * - getByRole('button', { name: '确定' }) -> "确定"
93
+ */
94
+ _extractTextFromSelector(selector) {
95
+ if (!selector) return null;
96
+
97
+ // Try internal:label format
98
+ const labelMatch = selector.match(/internal:label="([^"]+)"/);
99
+ if (labelMatch) return labelMatch[1];
100
+
101
+ // Try internal:text format
102
+ const textMatch = selector.match(/internal:text="([^"]+)"/);
103
+ if (textMatch) return textMatch[1];
104
+
105
+ // Try getByText format
106
+ const getByTextMatch = selector.match(/getByText\(['"]([^'"]+)['"]\)/);
107
+ if (getByTextMatch) return getByTextMatch[1];
108
+
109
+ // Try getByRole with name format
110
+ const getByRoleMatch = selector.match(/name:\s*['"]([^'"]+)['"]/);
111
+ if (getByRoleMatch) return getByRoleMatch[1];
112
+
113
+ return null;
114
+ }
115
+ }
@@ -0,0 +1,98 @@
1
+ # Workflow Execution Architecture
2
+
3
+ ## Single Source of Truth: Template Node Files
4
+
5
+ Workflows are **compiled from source code**, not loaded from JSON files.
6
+
7
+ The template node definitions in `packages/core/templates/` are the single source of truth.
8
+ There is no static JSON workflow file — any such file (e.g. `default-analysis-workflow.json`) is stale and unused.
9
+
10
+ ## How It Works
11
+
12
+ ### 1. Graph Builders Compile Templates into Workflows
13
+
14
+ Each workflow type has a graph builder that imports node definitions and wires them together:
15
+
16
+ ```
17
+ packages/core/templates/code-analysis/graph.js → buildAnalysisGraph()
18
+ packages/core/templates/code-implementation/graph.js → buildImplementationGraph()
19
+ ```
20
+
21
+ These builders import the actual node files:
22
+
23
+ ```
24
+ packages/core/templates/code-analysis/nodes/
25
+ ├── setup-node.js
26
+ ├── analyze-ticket-node.js
27
+ ├── generate-code-node.js
28
+ ├── generate-test-cases-node.js
29
+ └── finalize-node.js
30
+ ```
31
+
32
+ Each node file exports `{ name, outputSchema, execute }`. The `execute` function is the actual runtime code.
33
+
34
+ ### 2. Backend Resolves Workflow at Execution Time
35
+
36
+ When a user triggers an analysis (`POST /projects/:projectId/tickets/:ticketKey/analyze`), the backend resolves the workflow in this order:
37
+
38
+ 1. **WORKFLOWS_TABLE** (DynamoDB) — user-customized workflow saved via the UI workflow editor
39
+ 2. **project.analysisGraphConfig** (PROJECTS_TABLE) — legacy per-project config
40
+ 3. **null** — no saved workflow; the CLI will use the compiled default
41
+
42
+ The resolved config (or null) is uploaded to S3 as `context.json` and passed to the ECS container via `CONTEXT_PRESIGNED_URL`.
43
+
44
+ ### 3. ECS Container Compiles and Runs the Graph
45
+
46
+ The CLI (`packages/cli/src/commands/analyze-graph.js`) picks the workflow source:
47
+
48
+ ```
49
+ if (options.workflow) → local file (--workflow flag, dev only)
50
+ else if (execCtx.graphConfig) → custom saved workflow from DynamoDB (serialized JSON)
51
+ else → getDefaultGraph('analysis') → buildAnalysisGraph() from templates
52
+ ```
53
+
54
+ When using the default path, `buildAnalysisGraph()` imports the node files directly — the `execute` functions run as compiled JavaScript, not as eval'd strings.
55
+
56
+ When using a saved custom workflow, `compileGraph()` deserializes the JSON and wraps `executeCode` strings via `new Function()`.
57
+
58
+ ### 4. Key Implication: Saved Workflows Can Go Stale
59
+
60
+ If a user saves a workflow via the UI editor, the `executeCode` is serialized as a string snapshot into DynamoDB. Future code fixes to the template node files **do not** propagate to saved workflows.
61
+
62
+ **To pick up template fixes**: reset the workflow in the UI settings (Project → Settings → Workflow → Reset to Default). This deletes the DynamoDB record and forces the CLI to recompile from templates.
63
+
64
+ ## State Access Pattern
65
+
66
+ The graph stores each node's output under `state[nodeName]`:
67
+
68
+ ```js
69
+ // In graph.js after node execution:
70
+ state.update({ [currentNode]: result.output });
71
+
72
+ // So to access previous node results:
73
+ state.analyze_ticket?.analysis // NOT state.analysis
74
+ state.generate_code?.codeImplementation // NOT state.codeImplementation
75
+ ```
76
+
77
+ ## invokeAgent Return Shape
78
+
79
+ When `invokeAgent` is called with a `schema` option, it returns `{ raw, structured }` (not the structured data directly):
80
+
81
+ ```js
82
+ const result = await invokeAgent(prompt, { state, model, schema: SomeSchema });
83
+ const output = result?.structured || result; // unwrap
84
+ ```
85
+
86
+ Without a schema, it returns a plain string.
87
+
88
+ ## File-Based Structured Output (Cursor Strategy)
89
+
90
+ For Cursor agent, structured output uses a file-based approach:
91
+
92
+ 1. Framework generates a unique file path: `.zibby/tmp/zibby-result-<timestamp>.json`
93
+ 2. Instructions appended to prompt telling agent to write JSON to that file
94
+ 3. After agent completes, framework reads the file and validates with Zod
95
+ 4. If validation fails and `strictMode` is enabled, falls back to OpenAI proxy for schema enforcement
96
+ 5. Result files are kept (not deleted) for debugging
97
+
98
+ The Claude strategy uses its SDK's native structured output — completely separate path.
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Base class for AI agent strategies.
3
+ * All provider implementations (Cursor, Claude, etc.) must extend this class.
4
+ *
5
+ * @abstract
6
+ */
7
+ export class AgentStrategy {
8
+ /**
9
+ * @param {string} name - Provider identifier (e.g. 'cursor', 'claude')
10
+ * @param {string} description - Human-readable description
11
+ * @param {number} [priority=0] - Selection priority (higher = preferred)
12
+ */
13
+ constructor(name, description, priority = 0) {
14
+ this.name = name;
15
+ this.description = description;
16
+ this.priority = priority;
17
+ }
18
+
19
+ /**
20
+ * Execute a prompt against this agent.
21
+ *
22
+ * @abstract
23
+ * @param {string} prompt - The prompt text
24
+ * @param {AgentInvokeOptions} options
25
+ * @returns {Promise<string | AgentStructuredResult>}
26
+ * - Without schema: returns raw output string
27
+ * - With schema: returns { raw: string, structured: object }
28
+ * - On failure: throws Error
29
+ *
30
+ * @typedef {Object} AgentInvokeOptions
31
+ * @property {string} [model] - Model name or 'auto'
32
+ * @property {string} [workspace] - Working directory
33
+ * @property {object} [schema] - Zod schema for structured output
34
+ * @property {Array} [tools] - MCP tools available to the agent
35
+ * @property {Array} [images] - Image attachments (provider-specific)
36
+ * @property {string} [sessionPath] - Session artifact directory
37
+ * @property {number} [timeout] - Execution timeout in ms
38
+ * @property {object} [config] - Full workflow config
39
+ *
40
+ * @typedef {Object} AgentStructuredResult
41
+ * @property {string} raw - Raw agent output
42
+ * @property {object} structured - Parsed and validated output
43
+ */
44
+ async invoke(prompt, _options = {}) {
45
+ throw new Error('AgentStrategy.invoke() must be implemented by subclass');
46
+ }
47
+
48
+ /**
49
+ * Check if this agent strategy is available in the current environment.
50
+ *
51
+ * @abstract
52
+ * @param {object} [context] - Environment context (unused by most providers)
53
+ * @returns {boolean}
54
+ */
55
+ canHandle(_context) {
56
+ throw new Error('AgentStrategy.canHandle() must be implemented by subclass');
57
+ }
58
+
59
+ getName() {
60
+ return this.name;
61
+ }
62
+
63
+ getDescription() {
64
+ return this.description;
65
+ }
66
+
67
+ getPriority() {
68
+ return this.priority;
69
+ }
70
+ }
71
+
72
+ export default AgentStrategy;