edsger 0.56.2 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) 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 +129 -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/product-test-cases/index.d.ts +25 -0
  43. package/dist/phases/product-test-cases/index.js +174 -0
  44. package/dist/phases/product-test-cases/prompts.d.ts +24 -0
  45. package/dist/phases/product-test-cases/prompts.js +80 -0
  46. package/dist/phases/product-test-cases/types.d.ts +17 -0
  47. package/dist/phases/product-test-cases/types.js +27 -0
  48. package/dist/phases/screen-flow/index.d.ts +23 -0
  49. package/dist/phases/screen-flow/index.js +229 -0
  50. package/dist/phases/screen-flow/prompts.d.ts +19 -0
  51. package/dist/phases/screen-flow/prompts.js +39 -0
  52. package/dist/phases/screen-flow/theme.d.ts +19 -0
  53. package/dist/phases/screen-flow/theme.js +182 -0
  54. package/dist/phases/screen-flow/types.d.ts +130 -0
  55. package/dist/phases/screen-flow/types.js +66 -0
  56. package/dist/phases/user-psychology/agent.d.ts +16 -0
  57. package/dist/phases/user-psychology/agent.js +105 -0
  58. package/dist/phases/user-psychology/context.d.ts +10 -0
  59. package/dist/phases/user-psychology/context.js +65 -0
  60. package/dist/phases/user-psychology/index.d.ts +18 -0
  61. package/dist/phases/user-psychology/index.js +96 -0
  62. package/dist/phases/user-psychology/prompts.d.ts +2 -0
  63. package/dist/phases/user-psychology/prompts.js +41 -0
  64. package/dist/services/audit-logs.js +67 -9
  65. package/dist/services/branches.js +90 -14
  66. package/dist/services/phase-ratings.js +71 -9
  67. package/dist/services/product-logs.js +65 -5
  68. package/dist/services/pull-requests.js +74 -14
  69. package/dist/skills/phase/screen-flow/SKILL.md +78 -0
  70. package/dist/skills/phase/user-psychology/SKILL.md +135 -0
  71. package/dist/supabase/client.d.ts +23 -0
  72. package/dist/supabase/client.js +90 -0
  73. package/dist/system/session-manager.js +97 -24
  74. package/dist/types/index.d.ts +3 -0
  75. package/dist/utils/logger.js +24 -4
  76. package/package.json +5 -4
  77. package/vitest.config.ts +1 -0
package/dist/api/chat.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * MCP client wrappers for chat operations.
3
3
  * Used by the chat-worker subprocess to interact with chat channels and messages.
4
4
  */
5
+ import { getSupabase, hasSupabaseSession } from '../supabase/client.js';
5
6
  import { logError, logInfo } from '../utils/logger.js';
6
7
  import { callMcpEndpoint } from './mcp-client.js';
7
8
  // ============================================================
@@ -23,6 +24,32 @@ export async function listChannels(channelType, channelRefId, verbose) {
23
24
  if (verbose) {
24
25
  logInfo(`Listing channels: type=${channelType || 'all'}`);
25
26
  }
27
+ if (hasSupabaseSession()) {
28
+ try {
29
+ // RLS (chat_channels_select_policy) already filters to channels the
30
+ // user is a member of — no extra membership join needed.
31
+ let query = getSupabase().from('chat_channels').select('*');
32
+ if (channelType) {
33
+ query = query.eq('channel_type', channelType);
34
+ }
35
+ if (channelRefId) {
36
+ query = query.eq('channel_ref_id', channelRefId);
37
+ }
38
+ const { data, error } = await query.order('created_at', {
39
+ ascending: false,
40
+ });
41
+ if (error) {
42
+ throw new Error(error.message);
43
+ }
44
+ return (data || []);
45
+ }
46
+ catch (sdkError) {
47
+ if (verbose) {
48
+ logError(`Direct chat_channels SELECT failed, falling back to MCP: ${sdkError instanceof Error ? sdkError.message : String(sdkError)}`);
49
+ }
50
+ // Fall through to MCP path
51
+ }
52
+ }
26
53
  const result = (await callMcpEndpoint('chat/channels/list', {
27
54
  channel_type: channelType,
28
55
  channel_ref_id: channelRefId,
@@ -103,11 +130,37 @@ export async function claimPendingMessages(channelId, workerId, options = {}, ve
103
130
  if (verbose) {
104
131
  logInfo(`Claiming pending messages for channel ${channelId} (worker: ${workerId})`);
105
132
  }
133
+ const limit = options.limit || 10;
134
+ const staleSeconds = options.staleTimeoutSeconds || 300;
135
+ if (hasSupabaseSession()) {
136
+ try {
137
+ // The JWT-callable RPC enforces `user_is_chat_channel_member(auth.uid(),
138
+ // channel_id)` internally. It uses FOR UPDATE SKIP LOCKED so concurrent
139
+ // workers never claim the same message.
140
+ const { data, error } = await getSupabase().rpc('claim_pending_chat_messages_jwt', {
141
+ p_channel_id: channelId,
142
+ p_worker_id: workerId,
143
+ p_limit: limit,
144
+ // Postgres `interval` accepts the canonical 'N seconds' format.
145
+ p_stale_timeout: `${staleSeconds} seconds`,
146
+ });
147
+ if (error) {
148
+ throw new Error(error.message);
149
+ }
150
+ return (data || []);
151
+ }
152
+ catch (sdkError) {
153
+ if (verbose) {
154
+ logError(`Direct chat claim failed, falling back to MCP: ${sdkError instanceof Error ? sdkError.message : String(sdkError)}`);
155
+ }
156
+ // Fall through to MCP path
157
+ }
158
+ }
106
159
  const result = (await callMcpEndpoint('chat/messages/claim', {
107
160
  channel_id: channelId,
108
161
  worker_id: workerId,
109
- limit: options.limit || 10,
110
- stale_timeout_seconds: options.staleTimeoutSeconds || 300,
162
+ limit,
163
+ stale_timeout_seconds: staleSeconds,
111
164
  }));
112
165
  return result.messages || [];
113
166
  }
@@ -21,6 +21,13 @@ export interface IssueWithProduct extends IssueInfo {
21
21
  export declare function listProducts(verbose?: boolean): Promise<ProductSummary[]>;
22
22
  /**
23
23
  * List all ready_for_ai issues across all products
24
- * Iterates through all accessible products and collects their issues
24
+ *
25
+ * Direct-SDK path: a single `issues` SELECT joining `products` for the name.
26
+ * RLS on `issues` already filters to products the user has access to, so the
27
+ * old "fetch products, then fetch issues per product" N+1 dance is gone — one
28
+ * round trip total.
29
+ *
30
+ * MCP fallback (no Supabase session): keeps the previous N+1 behaviour so
31
+ * legacy CLIs still work until the desktop-app rolls out.
25
32
  */
26
33
  export declare function listAllReadyIssues(verbose?: boolean): Promise<IssueWithProduct[]>;
@@ -5,6 +5,7 @@
5
5
  * processes issues from all products the user has access to,
6
6
  * not just a single product.
7
7
  */
8
+ import { getSupabase, hasSupabaseSession } from '../supabase/client.js';
8
9
  import { logError, logInfo } from '../utils/logger.js';
9
10
  import { callMcpEndpoint } from './mcp-client.js';
10
11
  /**
@@ -29,12 +30,54 @@ export async function listProducts(verbose) {
29
30
  }
30
31
  /**
31
32
  * List all ready_for_ai issues across all products
32
- * Iterates through all accessible products and collects their issues
33
+ *
34
+ * Direct-SDK path: a single `issues` SELECT joining `products` for the name.
35
+ * RLS on `issues` already filters to products the user has access to, so the
36
+ * old "fetch products, then fetch issues per product" N+1 dance is gone — one
37
+ * round trip total.
38
+ *
39
+ * MCP fallback (no Supabase session): keeps the previous N+1 behaviour so
40
+ * legacy CLIs still work until the desktop-app rolls out.
33
41
  */
34
42
  export async function listAllReadyIssues(verbose) {
35
43
  if (verbose) {
36
44
  logInfo('Fetching ready_for_ai issues across all products...');
37
45
  }
46
+ if (hasSupabaseSession()) {
47
+ try {
48
+ const { data, error } = await getSupabase()
49
+ .from('issues')
50
+ .select('*, product:products!inner(id, name)')
51
+ .eq('status', 'ready_for_ai')
52
+ .order('updated_at', { ascending: true });
53
+ if (error) {
54
+ throw new Error(error.message);
55
+ }
56
+ const allIssues = (data || []).map((row) => {
57
+ // The `product` join lands as a nested object; lift its name onto
58
+ // the row to keep the wire shape stable.
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- shape from PostgREST join
60
+ const r = row;
61
+ return {
62
+ ...r,
63
+ product_name: r.product?.name ?? undefined,
64
+ };
65
+ });
66
+ if (verbose) {
67
+ logInfo(`Found ${allIssues.length} ready_for_ai issue(s) across products`);
68
+ allIssues.forEach((f, i) => {
69
+ logInfo(` ${i + 1}. [${f.product_name}] ${f.name}`);
70
+ });
71
+ }
72
+ return allIssues;
73
+ }
74
+ catch (sdkError) {
75
+ if (verbose) {
76
+ logError(`Direct issues SELECT failed, falling back to MCP: ${sdkError instanceof Error ? sdkError.message : String(sdkError)}`);
77
+ }
78
+ // Fall through to MCP path below
79
+ }
80
+ }
38
81
  try {
39
82
  // First, get all products
40
83
  const products = await listProducts(verbose);
@@ -1,3 +1,4 @@
1
+ import { getSupabase, hasSupabaseSession } from '../supabase/client.js';
1
2
  import { logError, logInfo } from '../utils/logger.js';
2
3
  import { callMcpEndpoint } from './mcp-client.js';
3
4
  // =============================================================================
@@ -71,6 +72,23 @@ export async function updateCompetitor(competitorId, updates, verbose) {
71
72
  logInfo(`Updating competitor: ${competitorId}`);
72
73
  }
73
74
  try {
75
+ if (hasSupabaseSession()) {
76
+ try {
77
+ const { data, error } = await getSupabase()
78
+ .from('product_competitors')
79
+ .update(updates)
80
+ .eq('id', competitorId)
81
+ .select()
82
+ .single();
83
+ if (error) {
84
+ throw new Error(error.message);
85
+ }
86
+ return data ?? null;
87
+ }
88
+ catch {
89
+ // Fall through to MCP
90
+ }
91
+ }
74
92
  const result = await callMcpEndpoint('intelligence/competitors/update', {
75
93
  competitor_id: competitorId,
76
94
  ...updates,
@@ -87,6 +105,21 @@ export async function deleteCompetitor(competitorId, verbose) {
87
105
  logInfo(`Deleting competitor: ${competitorId}`);
88
106
  }
89
107
  try {
108
+ if (hasSupabaseSession()) {
109
+ try {
110
+ const { error } = await getSupabase()
111
+ .from('product_competitors')
112
+ .delete()
113
+ .eq('id', competitorId);
114
+ if (error) {
115
+ throw new Error(error.message);
116
+ }
117
+ return true;
118
+ }
119
+ catch {
120
+ // Fall through to MCP
121
+ }
122
+ }
90
123
  await callMcpEndpoint('intelligence/competitors/delete', {
91
124
  competitor_id: competitorId,
92
125
  });
@@ -105,6 +138,29 @@ export async function getSnapshots(opts, verbose) {
105
138
  logInfo(`Fetching snapshots`);
106
139
  }
107
140
  try {
141
+ if (hasSupabaseSession()) {
142
+ try {
143
+ let query = getSupabase().from('competitor_snapshots').select('*');
144
+ if (opts.competitorId) {
145
+ query = query.eq('competitor_id', opts.competitorId);
146
+ }
147
+ if (opts.productId) {
148
+ query = query.eq('product_id', opts.productId);
149
+ }
150
+ query = query.order('created_at', { ascending: false });
151
+ if (opts.limit) {
152
+ query = query.limit(opts.limit);
153
+ }
154
+ const { data, error } = await query;
155
+ if (error) {
156
+ throw new Error(error.message);
157
+ }
158
+ return (data || []);
159
+ }
160
+ catch {
161
+ // Fall through to MCP
162
+ }
163
+ }
108
164
  const result = await callMcpEndpoint('intelligence/snapshots/list', {
109
165
  competitor_id: opts.competitorId,
110
166
  product_id: opts.productId,
@@ -140,6 +196,32 @@ export async function getReports(productId, opts, verbose) {
140
196
  logInfo(`Fetching intelligence reports for product: ${productId}`);
141
197
  }
142
198
  try {
199
+ if (hasSupabaseSession()) {
200
+ try {
201
+ let query = getSupabase()
202
+ .from('intelligence_reports')
203
+ .select('*')
204
+ .eq('product_id', productId);
205
+ if (opts?.reportType) {
206
+ query = query.eq('report_type', opts.reportType);
207
+ }
208
+ if (opts?.status) {
209
+ query = query.eq('status', opts.status);
210
+ }
211
+ query = query.order('created_at', { ascending: false });
212
+ if (opts?.limit) {
213
+ query = query.limit(opts.limit);
214
+ }
215
+ const { data, error } = await query;
216
+ if (error) {
217
+ throw new Error(error.message);
218
+ }
219
+ return (data || []);
220
+ }
221
+ catch {
222
+ // Fall through to MCP
223
+ }
224
+ }
143
225
  const result = await callMcpEndpoint('intelligence/reports/list', {
144
226
  product_id: productId,
145
227
  report_type: opts?.reportType,
@@ -160,6 +242,22 @@ export async function getReport(reportId, verbose) {
160
242
  logInfo(`Fetching report: ${reportId}`);
161
243
  }
162
244
  try {
245
+ if (hasSupabaseSession()) {
246
+ try {
247
+ const { data, error } = await getSupabase()
248
+ .from('intelligence_reports')
249
+ .select('*')
250
+ .eq('id', reportId)
251
+ .maybeSingle();
252
+ if (error) {
253
+ throw new Error(error.message);
254
+ }
255
+ return data ?? null;
256
+ }
257
+ catch {
258
+ // Fall through to MCP
259
+ }
260
+ }
163
261
  const result = await callMcpEndpoint('intelligence/reports/get', {
164
262
  report_id: reportId,
165
263
  });
@@ -1,3 +1,4 @@
1
+ import { getSupabase, hasSupabaseSession } from '../../supabase/client.js';
1
2
  import { logInfo } from '../../utils/logger.js';
2
3
  import { callMcpEndpoint } from '../mcp-client.js';
3
4
  /**
@@ -7,6 +8,31 @@ export async function getIssue(issueId, verbose) {
7
8
  if (verbose) {
8
9
  logInfo(`Fetching issue details for: ${issueId}`);
9
10
  }
11
+ if (hasSupabaseSession()) {
12
+ try {
13
+ const { data, error } = await getSupabase()
14
+ .from('issues')
15
+ .select('*')
16
+ .eq('id', issueId)
17
+ .maybeSingle();
18
+ if (error) {
19
+ throw new Error(error.message);
20
+ }
21
+ if (!data) {
22
+ throw new Error(`Issue not found: ${issueId}`);
23
+ }
24
+ return data;
25
+ }
26
+ catch (sdkErr) {
27
+ // Surface "not found" immediately so callers don't double-fetch via
28
+ // MCP for an issue that genuinely doesn't exist.
29
+ if (sdkErr instanceof Error &&
30
+ sdkErr.message === `Issue not found: ${issueId}`) {
31
+ throw sdkErr;
32
+ }
33
+ // Other errors fall through to MCP
34
+ }
35
+ }
10
36
  const result = (await callMcpEndpoint('issues/get', {
11
37
  issue_id: issueId,
12
38
  }));
@@ -1,3 +1,5 @@
1
+ import { getUserId } from '../../auth/auth-store.js';
2
+ import { getSupabase, hasSupabaseSession } from '../../supabase/client.js';
1
3
  import { logError, logInfo } from '../../utils/logger.js';
2
4
  import { callMcpEndpoint } from '../mcp-client.js';
3
5
  /**
@@ -17,6 +19,56 @@ export async function claimNextIssue(productId, verbose) {
17
19
  ? `Attempting to claim next ready_for_ai issue for product: ${productId}`
18
20
  : 'Attempting to claim next ready_for_ai issue across all products');
19
21
  }
22
+ // Direct-SDK path: call the existing claim_next_ready_issue RPC. The RPC is
23
+ // SECURITY INVOKER and gates access by `developer_id = p_worker_id`, so the
24
+ // user JWT is honoured naturally.
25
+ const userId = getUserId();
26
+ if (hasSupabaseSession() && userId) {
27
+ try {
28
+ const { data, error } = await getSupabase().rpc('claim_next_ready_issue', {
29
+ p_worker_id: userId,
30
+ p_product_id: productId ?? null,
31
+ });
32
+ if (error) {
33
+ throw new Error(error.message);
34
+ }
35
+ const row = Array.isArray(data) ? data[0] : data;
36
+ if (!row) {
37
+ if (verbose) {
38
+ logInfo('No issues available for processing');
39
+ }
40
+ return null;
41
+ }
42
+ // RPC returns rows with `issue_*` prefixed column names — unwrap to
43
+ // the IssueInfo shape callers already consume.
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- raw RPC row
45
+ const r = row;
46
+ const issue = {
47
+ id: r.issue_id,
48
+ name: r.issue_name,
49
+ description: r.issue_description,
50
+ technical_design: r.issue_technical_design,
51
+ status: r.issue_status,
52
+ execution_mode: r.issue_execution_mode,
53
+ workflow: r.issue_workflow,
54
+ product_id: r.issue_product_id,
55
+ created_at: r.issue_created_at,
56
+ updated_at: r.issue_updated_at,
57
+ autonomous_hours: r.issue_autonomous_hours,
58
+ execution_plan: r.issue_execution_plan,
59
+ };
60
+ if (verbose) {
61
+ logInfo(`✅ Claimed issue: ${issue.name} (${issue.id})`);
62
+ }
63
+ return issue;
64
+ }
65
+ catch (sdkError) {
66
+ if (verbose) {
67
+ logError(`Direct issues/claim failed, falling back to MCP: ${sdkError instanceof Error ? sdkError.message : String(sdkError)}`);
68
+ }
69
+ // Fall through to MCP path
70
+ }
71
+ }
20
72
  try {
21
73
  const result = (await callMcpEndpoint('issues/claim', productId ? { product_id: productId } : {}));
22
74
  if (result.issue) {
@@ -1,3 +1,4 @@
1
+ import { getSupabase, hasSupabaseSession } from '../../supabase/client.js';
1
2
  import { logError, logInfo } from '../../utils/logger.js';
2
3
  import { callMcpEndpoint } from '../mcp-client.js';
3
4
  /**
@@ -7,6 +8,22 @@ export async function getTestCases(issueId, verbose) {
7
8
  if (verbose) {
8
9
  logInfo(`Fetching test cases for issue: ${issueId}`);
9
10
  }
11
+ if (hasSupabaseSession()) {
12
+ try {
13
+ const { data, error } = await getSupabase()
14
+ .from('test_cases')
15
+ .select('*')
16
+ .eq('issue_id', issueId)
17
+ .order('created_at', { ascending: true });
18
+ if (error) {
19
+ throw new Error(error.message);
20
+ }
21
+ return (data || []);
22
+ }
23
+ catch {
24
+ // Fall through to MCP
25
+ }
26
+ }
10
27
  const result = (await callMcpEndpoint('test_cases/list', {
11
28
  issue_id: issueId,
12
29
  }));
@@ -20,16 +37,38 @@ export async function createTestCase(issueId, testCase, verbose) {
20
37
  if (verbose) {
21
38
  logInfo(`Creating test case for issue: ${issueId}`);
22
39
  }
23
- await callMcpEndpoint('test_cases/create', {
24
- issue_id: issueId,
25
- test_cases: [
26
- {
40
+ let usedSdk = false;
41
+ if (hasSupabaseSession()) {
42
+ try {
43
+ const { error } = await getSupabase()
44
+ .from('test_cases')
45
+ .insert({
46
+ issue_id: issueId,
27
47
  name: testCase.name,
28
48
  description: testCase.description,
29
49
  is_critical: testCase.is_critical || false,
30
- },
31
- ],
32
- });
50
+ });
51
+ if (error) {
52
+ throw new Error(error.message);
53
+ }
54
+ usedSdk = true;
55
+ }
56
+ catch {
57
+ // Fall through to MCP
58
+ }
59
+ }
60
+ if (!usedSdk) {
61
+ await callMcpEndpoint('test_cases/create', {
62
+ issue_id: issueId,
63
+ test_cases: [
64
+ {
65
+ name: testCase.name,
66
+ description: testCase.description,
67
+ is_critical: testCase.is_critical || false,
68
+ },
69
+ ],
70
+ });
71
+ }
33
72
  if (verbose) {
34
73
  logInfo('✅ Test case created successfully');
35
74
  }
@@ -71,9 +110,27 @@ export async function deleteTestCase(testCaseId, verbose) {
71
110
  if (verbose) {
72
111
  logInfo(`Deleting test case: ${testCaseId}`);
73
112
  }
74
- await callMcpEndpoint('test_cases/delete', {
75
- test_case_id: testCaseId,
76
- });
113
+ let usedSdk = false;
114
+ if (hasSupabaseSession()) {
115
+ try {
116
+ const { error } = await getSupabase()
117
+ .from('test_cases')
118
+ .delete()
119
+ .eq('id', testCaseId);
120
+ if (error) {
121
+ throw new Error(error.message);
122
+ }
123
+ usedSdk = true;
124
+ }
125
+ catch {
126
+ // Fall through to MCP
127
+ }
128
+ }
129
+ if (!usedSdk) {
130
+ await callMcpEndpoint('test_cases/delete', {
131
+ test_case_id: testCaseId,
132
+ });
133
+ }
77
134
  if (verbose) {
78
135
  logInfo('✅ Test case deleted successfully');
79
136
  }
@@ -93,10 +150,28 @@ export async function updateTestCaseStatus(testCaseId, status, verbose) {
93
150
  if (verbose) {
94
151
  logInfo(`Updating test case ${testCaseId} status to: ${status}`);
95
152
  }
96
- await callMcpEndpoint('test_cases/update_status', {
97
- test_case_id: testCaseId,
98
- status,
99
- });
153
+ let usedSdk = false;
154
+ if (hasSupabaseSession()) {
155
+ try {
156
+ const { error } = await getSupabase()
157
+ .from('test_cases')
158
+ .update({ status })
159
+ .eq('id', testCaseId);
160
+ if (error) {
161
+ throw new Error(error.message);
162
+ }
163
+ usedSdk = true;
164
+ }
165
+ catch {
166
+ // Fall through to MCP
167
+ }
168
+ }
169
+ if (!usedSdk) {
170
+ await callMcpEndpoint('test_cases/update_status', {
171
+ test_case_id: testCaseId,
172
+ status,
173
+ });
174
+ }
100
175
  if (verbose) {
101
176
  logInfo('✅ Test case status updated successfully');
102
177
  }
@@ -1,3 +1,4 @@
1
+ import { getSupabase, hasSupabaseSession } from '../../supabase/client.js';
1
2
  import { logError, logInfo } from '../../utils/logger.js';
2
3
  import { callMcpEndpoint } from '../mcp-client.js';
3
4
  /**
@@ -8,10 +9,28 @@ export async function updateIssue(issueId, updates, verbose) {
8
9
  if (verbose) {
9
10
  logInfo(`Updating issue: ${issueId}`);
10
11
  }
11
- await callMcpEndpoint('issues/update', {
12
- issue_id: issueId,
13
- ...updates,
14
- });
12
+ let usedSdk = false;
13
+ if (hasSupabaseSession()) {
14
+ try {
15
+ const { error } = await getSupabase()
16
+ .from('issues')
17
+ .update(updates)
18
+ .eq('id', issueId);
19
+ if (error) {
20
+ throw new Error(error.message);
21
+ }
22
+ usedSdk = true;
23
+ }
24
+ catch {
25
+ // Fall through to MCP
26
+ }
27
+ }
28
+ if (!usedSdk) {
29
+ await callMcpEndpoint('issues/update', {
30
+ issue_id: issueId,
31
+ ...updates,
32
+ });
33
+ }
15
34
  if (verbose) {
16
35
  logInfo('✅ Issue updated successfully');
17
36
  }
@@ -55,10 +74,29 @@ export async function markWorkflowPhaseCompleted(issueId, phaseName, verbose) {
55
74
  logInfo(`Marking workflow phase '${normalizedPhaseName}' as completed for issue: ${issueId}`);
56
75
  }
57
76
  // Fetch current issue to get workflow
58
- const response = (await callMcpEndpoint('issues/get', {
59
- issue_id: issueId,
60
- }));
61
- const issue = response?.issues?.[0];
77
+ let issue = null;
78
+ if (hasSupabaseSession()) {
79
+ try {
80
+ const { data, error } = await getSupabase()
81
+ .from('issues')
82
+ .select('workflow')
83
+ .eq('id', issueId)
84
+ .maybeSingle();
85
+ if (error) {
86
+ throw new Error(error.message);
87
+ }
88
+ issue = data;
89
+ }
90
+ catch {
91
+ // Fall through to MCP
92
+ }
93
+ }
94
+ if (issue == null) {
95
+ const response = (await callMcpEndpoint('issues/get', {
96
+ issue_id: issueId,
97
+ }));
98
+ issue = response?.issues?.[0] || null;
99
+ }
62
100
  if (!issue) {
63
101
  logError(`Issue not found: ${issueId}`);
64
102
  return false;