edsger 0.57.0 → 0.59.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 (36) hide show
  1. package/dist/api/cross-product.js +0 -1
  2. package/dist/api/issues/issue-utils.js +0 -1
  3. package/dist/api/issues/update-issue.js +1 -1
  4. package/dist/commands/agent-workflow/chat-worker.js +1 -1
  5. package/dist/commands/checklists/index.js +1 -1
  6. package/dist/commands/product-techniques/index.d.ts +15 -0
  7. package/dist/commands/product-techniques/index.js +37 -0
  8. package/dist/commands/workflow/executors/phase-executor.js +1 -1
  9. package/dist/index.js +24 -1
  10. package/dist/phases/analyze-logs/index.js +1 -1
  11. package/dist/phases/bug-fixing/context-fetcher.js +4 -2
  12. package/dist/phases/find-features/index.js +1 -1
  13. package/dist/phases/output-contracts.js +47 -36
  14. package/dist/phases/pr-shared/agent-utils.d.ts +11 -3
  15. package/dist/phases/pr-shared/agent-utils.js +48 -4
  16. package/dist/phases/product-techniques/index.d.ts +52 -0
  17. package/dist/phases/product-techniques/index.js +268 -0
  18. package/dist/phases/product-techniques/mcp-server.d.ts +41 -0
  19. package/dist/phases/product-techniques/mcp-server.js +96 -0
  20. package/dist/phases/product-techniques/prompts.d.ts +19 -0
  21. package/dist/phases/product-techniques/prompts.js +66 -0
  22. package/dist/phases/product-techniques/types.d.ts +13 -0
  23. package/dist/phases/product-techniques/types.js +13 -0
  24. package/dist/phases/screen-flow/index.js +73 -17
  25. package/dist/phases/screen-flow/mcp-server.d.ts +195 -0
  26. package/dist/phases/screen-flow/mcp-server.js +262 -0
  27. package/dist/phases/screen-flow/prompts.js +3 -1
  28. package/dist/phases/screen-flow/theme.js +23 -12
  29. package/dist/phases/screen-flow/types.js +30 -15
  30. package/dist/services/branches.js +3 -3
  31. package/dist/services/phase-hooks/hook-executor.js +1 -1
  32. package/dist/services/phase-ratings.js +1 -1
  33. package/dist/services/product-logs.js +1 -1
  34. package/dist/services/pull-requests.js +3 -3
  35. package/package.json +1 -1
  36. package/vitest.config.ts +1 -0
@@ -0,0 +1,195 @@
1
+ /**
2
+ * In-process MCP server exposing a single tool — `submit_screen_flow` —
3
+ * that the Claude Agent SDK session calls to return the structured
4
+ * extraction.
5
+ *
6
+ * Using a tool call instead of parsing a fenced text block lets the SDK
7
+ * enforce the schema (via Zod) and lets the agent self-correct when
8
+ * validation fails — the validation error is returned to the agent as
9
+ * the tool result and it can re-call the tool with corrected data.
10
+ *
11
+ * The capture pattern: callers pass in a `ScreenFlowCaptureState`. The
12
+ * tool handler stores the validated args on `state.captured` and the
13
+ * orchestrator reads it after the SDK loop ends. If the agent never
14
+ * calls the tool, `state.captured` stays null and the caller can fall
15
+ * back to parsing the assistant text.
16
+ */
17
+ import { z } from 'zod';
18
+ import type { ScreenFlowExtraction } from './types.js';
19
+ export interface ScreenFlowCaptureState {
20
+ captured: ScreenFlowExtraction | null;
21
+ }
22
+ export declare function createScreenFlowCaptureState(): ScreenFlowCaptureState;
23
+ /** Optional sink for streaming progress messages from the agent. */
24
+ export type ScreenFlowProgressSink = (event: {
25
+ phase: 'detection' | 'routing' | 'screens' | 'transitions' | 'submission';
26
+ message: string;
27
+ }) => void;
28
+ export declare function validateConsistency(extraction: ScreenFlowExtraction): {
29
+ error: string | null;
30
+ };
31
+ /**
32
+ * Build the `submit_screen_flow` tool. Exported separately from the server
33
+ * so tests can exercise the handler directly without going through the
34
+ * MCP transport.
35
+ */
36
+ export declare function createSubmitScreenFlowTool(state: ScreenFlowCaptureState): import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
37
+ summary: z.ZodString;
38
+ nodes: z.ZodArray<z.ZodObject<{
39
+ slug: z.ZodString;
40
+ name: z.ZodString;
41
+ route: z.ZodOptional<z.ZodString>;
42
+ file: z.ZodOptional<z.ZodString>;
43
+ kind: z.ZodEnum<{
44
+ page: "page";
45
+ state: "state";
46
+ modal: "modal";
47
+ drawer: "drawer";
48
+ tab: "tab";
49
+ }>;
50
+ layout: z.ZodEnum<{
51
+ split: "split";
52
+ centered: "centered";
53
+ sidebar: "sidebar";
54
+ "list-detail": "list-detail";
55
+ tabs: "tabs";
56
+ stacked: "stacked";
57
+ }>;
58
+ header: z.ZodOptional<z.ZodObject<{
59
+ title: z.ZodString;
60
+ subtitle: z.ZodOptional<z.ZodString>;
61
+ back: z.ZodOptional<z.ZodBoolean>;
62
+ actions: z.ZodOptional<z.ZodArray<z.ZodObject<{
63
+ label: z.ZodString;
64
+ variant: z.ZodOptional<z.ZodEnum<{
65
+ primary: "primary";
66
+ secondary: "secondary";
67
+ ghost: "ghost";
68
+ destructive: "destructive";
69
+ }>>;
70
+ icon: z.ZodOptional<z.ZodString>;
71
+ }, z.core.$strip>>>;
72
+ }, z.core.$strip>>;
73
+ body: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
74
+ type: z.ZodLiteral<"form">;
75
+ fields: z.ZodArray<z.ZodObject<{
76
+ label: z.ZodString;
77
+ kind: z.ZodEnum<{
78
+ number: "number";
79
+ text: "text";
80
+ date: "date";
81
+ select: "select";
82
+ email: "email";
83
+ password: "password";
84
+ textarea: "textarea";
85
+ checkbox: "checkbox";
86
+ }>;
87
+ placeholder: z.ZodOptional<z.ZodString>;
88
+ value: z.ZodOptional<z.ZodString>;
89
+ required: z.ZodOptional<z.ZodBoolean>;
90
+ }, z.core.$strip>>;
91
+ submitLabel: z.ZodString;
92
+ secondaryLabel: z.ZodOptional<z.ZodString>;
93
+ }, z.core.$strip>, z.ZodObject<{
94
+ type: z.ZodLiteral<"list">;
95
+ items: z.ZodArray<z.ZodObject<{
96
+ title: z.ZodString;
97
+ subtitle: z.ZodOptional<z.ZodString>;
98
+ meta: z.ZodOptional<z.ZodString>;
99
+ icon: z.ZodOptional<z.ZodString>;
100
+ }, z.core.$strip>>;
101
+ emptyMessage: z.ZodOptional<z.ZodString>;
102
+ }, z.core.$strip>, z.ZodObject<{
103
+ type: z.ZodLiteral<"card-grid">;
104
+ cards: z.ZodArray<z.ZodObject<{
105
+ title: z.ZodString;
106
+ subtitle: z.ZodOptional<z.ZodString>;
107
+ meta: z.ZodOptional<z.ZodString>;
108
+ }, z.core.$strip>>;
109
+ columns: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<2>, z.ZodLiteral<3>, z.ZodLiteral<4>]>>;
110
+ }, z.core.$strip>, z.ZodObject<{
111
+ type: z.ZodLiteral<"table">;
112
+ columns: z.ZodArray<z.ZodString>;
113
+ rows: z.ZodArray<z.ZodArray<z.ZodString>>;
114
+ }, z.core.$strip>, z.ZodObject<{
115
+ type: z.ZodLiteral<"kanban">;
116
+ columns: z.ZodArray<z.ZodObject<{
117
+ title: z.ZodString;
118
+ cards: z.ZodArray<z.ZodObject<{
119
+ title: z.ZodString;
120
+ meta: z.ZodOptional<z.ZodString>;
121
+ }, z.core.$strip>>;
122
+ }, z.core.$strip>>;
123
+ }, z.core.$strip>, z.ZodObject<{
124
+ type: z.ZodLiteral<"text">;
125
+ content: z.ZodString;
126
+ }, z.core.$strip>, z.ZodObject<{
127
+ type: z.ZodLiteral<"image">;
128
+ alt: z.ZodString;
129
+ aspect: z.ZodOptional<z.ZodEnum<{
130
+ video: "video";
131
+ square: "square";
132
+ wide: "wide";
133
+ }>>;
134
+ }, z.core.$strip>, z.ZodObject<{
135
+ type: z.ZodLiteral<"chart">;
136
+ chartKind: z.ZodEnum<{
137
+ line: "line";
138
+ bar: "bar";
139
+ pie: "pie";
140
+ }>;
141
+ label: z.ZodOptional<z.ZodString>;
142
+ }, z.core.$strip>, z.ZodObject<{
143
+ type: z.ZodLiteral<"stats">;
144
+ items: z.ZodArray<z.ZodObject<{
145
+ label: z.ZodString;
146
+ value: z.ZodString;
147
+ delta: z.ZodOptional<z.ZodString>;
148
+ }, z.core.$strip>>;
149
+ }, z.core.$strip>, z.ZodObject<{
150
+ type: z.ZodLiteral<"empty-state">;
151
+ title: z.ZodString;
152
+ message: z.ZodOptional<z.ZodString>;
153
+ cta: z.ZodOptional<z.ZodString>;
154
+ }, z.core.$strip>, z.ZodObject<{
155
+ type: z.ZodLiteral<"tabs">;
156
+ tabs: z.ZodArray<z.ZodString>;
157
+ activeIndex: z.ZodOptional<z.ZodNumber>;
158
+ }, z.core.$strip>, z.ZodObject<{
159
+ type: z.ZodLiteral<"custom">;
160
+ label: z.ZodString;
161
+ }, z.core.$strip>], "type">>;
162
+ }, z.core.$strip>>;
163
+ edges: z.ZodArray<z.ZodObject<{
164
+ fromSlug: z.ZodString;
165
+ toSlug: z.ZodString;
166
+ triggerLabel: z.ZodString;
167
+ triggerFile: z.ZodOptional<z.ZodString>;
168
+ kind: z.ZodEnum<{
169
+ modal: "modal";
170
+ navigate: "navigate";
171
+ redirect: "redirect";
172
+ back: "back";
173
+ }>;
174
+ }, z.core.$strip>>;
175
+ }>;
176
+ /**
177
+ * Build the `record_progress` tool. A side-channel that lets the agent
178
+ * push a human-readable status message to the CLI / desktop UI so a
179
+ * multi-minute extraction doesn't look frozen between text emissions.
180
+ * Returning `{ ok: true }` keeps it cheap — it has no semantic effect
181
+ * on the extraction.
182
+ */
183
+ export declare function createRecordProgressTool(sink?: ScreenFlowProgressSink): import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
184
+ phase: z.ZodEnum<{
185
+ detection: "detection";
186
+ routing: "routing";
187
+ screens: "screens";
188
+ transitions: "transitions";
189
+ submission: "submission";
190
+ }>;
191
+ message: z.ZodString;
192
+ }>;
193
+ export declare function createScreenFlowMcpServer(state: ScreenFlowCaptureState, options?: {
194
+ onProgress?: ScreenFlowProgressSink;
195
+ }): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
@@ -0,0 +1,262 @@
1
+ /**
2
+ * In-process MCP server exposing a single tool — `submit_screen_flow` —
3
+ * that the Claude Agent SDK session calls to return the structured
4
+ * extraction.
5
+ *
6
+ * Using a tool call instead of parsing a fenced text block lets the SDK
7
+ * enforce the schema (via Zod) and lets the agent self-correct when
8
+ * validation fails — the validation error is returned to the agent as
9
+ * the tool result and it can re-call the tool with corrected data.
10
+ *
11
+ * The capture pattern: callers pass in a `ScreenFlowCaptureState`. The
12
+ * tool handler stores the validated args on `state.captured` and the
13
+ * orchestrator reads it after the SDK loop ends. If the agent never
14
+ * calls the tool, `state.captured` stays null and the caller can fall
15
+ * back to parsing the assistant text.
16
+ */
17
+ import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
18
+ import { z } from 'zod';
19
+ export function createScreenFlowCaptureState() {
20
+ return { captured: null };
21
+ }
22
+ // ---------------------------------------------------------------------------
23
+ // Zod schemas (mirror types.ts — kept in sync by tests)
24
+ // ---------------------------------------------------------------------------
25
+ const formFieldSchema = z.object({
26
+ label: z.string(),
27
+ kind: z.enum([
28
+ 'text',
29
+ 'email',
30
+ 'password',
31
+ 'textarea',
32
+ 'select',
33
+ 'checkbox',
34
+ 'date',
35
+ 'number',
36
+ ]),
37
+ placeholder: z.string().optional(),
38
+ value: z.string().optional(),
39
+ required: z.boolean().optional(),
40
+ });
41
+ const listItemSchema = z.object({
42
+ title: z.string(),
43
+ subtitle: z.string().optional(),
44
+ meta: z.string().optional(),
45
+ icon: z.string().optional(),
46
+ });
47
+ const cardItemSchema = z.object({
48
+ title: z.string(),
49
+ subtitle: z.string().optional(),
50
+ meta: z.string().optional(),
51
+ });
52
+ const kanbanColumnSchema = z.object({
53
+ title: z.string(),
54
+ cards: z.array(z.object({ title: z.string(), meta: z.string().optional() })),
55
+ });
56
+ const sectionSchema = z.discriminatedUnion('type', [
57
+ z.object({
58
+ type: z.literal('form'),
59
+ fields: z.array(formFieldSchema),
60
+ submitLabel: z.string(),
61
+ secondaryLabel: z.string().optional(),
62
+ }),
63
+ z.object({
64
+ type: z.literal('list'),
65
+ items: z.array(listItemSchema),
66
+ emptyMessage: z.string().optional(),
67
+ }),
68
+ z.object({
69
+ type: z.literal('card-grid'),
70
+ cards: z.array(cardItemSchema),
71
+ columns: z.union([z.literal(2), z.literal(3), z.literal(4)]).optional(),
72
+ }),
73
+ z.object({
74
+ type: z.literal('table'),
75
+ columns: z.array(z.string()),
76
+ rows: z.array(z.array(z.string())),
77
+ }),
78
+ z.object({
79
+ type: z.literal('kanban'),
80
+ columns: z.array(kanbanColumnSchema),
81
+ }),
82
+ z.object({
83
+ type: z.literal('text'),
84
+ content: z.string(),
85
+ }),
86
+ z.object({
87
+ type: z.literal('image'),
88
+ alt: z.string(),
89
+ aspect: z.enum(['video', 'square', 'wide']).optional(),
90
+ }),
91
+ z.object({
92
+ type: z.literal('chart'),
93
+ chartKind: z.enum(['line', 'bar', 'pie']),
94
+ label: z.string().optional(),
95
+ }),
96
+ z.object({
97
+ type: z.literal('stats'),
98
+ items: z.array(z.object({
99
+ label: z.string(),
100
+ value: z.string(),
101
+ delta: z.string().optional(),
102
+ })),
103
+ }),
104
+ z.object({
105
+ type: z.literal('empty-state'),
106
+ title: z.string(),
107
+ message: z.string().optional(),
108
+ cta: z.string().optional(),
109
+ }),
110
+ z.object({
111
+ type: z.literal('tabs'),
112
+ tabs: z.array(z.string()),
113
+ activeIndex: z.number().optional(),
114
+ }),
115
+ z.object({
116
+ type: z.literal('custom'),
117
+ label: z.string(),
118
+ }),
119
+ ]);
120
+ const screenActionSchema = z.object({
121
+ label: z.string(),
122
+ variant: z.enum(['primary', 'secondary', 'ghost', 'destructive']).optional(),
123
+ icon: z.string().optional(),
124
+ });
125
+ const screenHeaderSchema = z.object({
126
+ title: z.string(),
127
+ subtitle: z.string().optional(),
128
+ back: z.boolean().optional(),
129
+ actions: z.array(screenActionSchema).optional(),
130
+ });
131
+ const screenNodeSchema = z.object({
132
+ slug: z.string().min(1),
133
+ name: z.string().min(1),
134
+ route: z.string().optional(),
135
+ file: z.string().optional(),
136
+ kind: z.enum(['page', 'modal', 'drawer', 'tab', 'state']),
137
+ layout: z.enum([
138
+ 'centered',
139
+ 'sidebar',
140
+ 'split',
141
+ 'list-detail',
142
+ 'tabs',
143
+ 'stacked',
144
+ ]),
145
+ header: screenHeaderSchema.optional(),
146
+ body: z.array(sectionSchema),
147
+ });
148
+ const screenEdgeSchema = z.object({
149
+ fromSlug: z.string().min(1),
150
+ toSlug: z.string().min(1),
151
+ triggerLabel: z.string(),
152
+ triggerFile: z.string().optional(),
153
+ kind: z.enum(['navigate', 'modal', 'redirect', 'back']),
154
+ });
155
+ // ---------------------------------------------------------------------------
156
+ // Cross-field consistency (Zod can't express this)
157
+ // ---------------------------------------------------------------------------
158
+ export function validateConsistency(extraction) {
159
+ const slugs = new Set();
160
+ for (const node of extraction.nodes) {
161
+ if (slugs.has(node.slug)) {
162
+ return {
163
+ error: `Duplicate node slug "${node.slug}". Each node.slug MUST be unique within the flow. Re-call submit_screen_flow with deduplicated nodes.`,
164
+ };
165
+ }
166
+ slugs.add(node.slug);
167
+ }
168
+ for (const edge of extraction.edges) {
169
+ if (!slugs.has(edge.fromSlug)) {
170
+ return {
171
+ error: `Edge fromSlug "${edge.fromSlug}" → "${edge.toSlug}" does not match any node slug. Either add the missing node or drop the edge, then re-call submit_screen_flow.`,
172
+ };
173
+ }
174
+ if (!slugs.has(edge.toSlug)) {
175
+ return {
176
+ error: `Edge fromSlug "${edge.fromSlug}" → toSlug "${edge.toSlug}" does not match any node slug. Either add the missing node or drop the edge, then re-call submit_screen_flow.`,
177
+ };
178
+ }
179
+ }
180
+ return { error: null };
181
+ }
182
+ // ---------------------------------------------------------------------------
183
+ // Tool factory + server factory
184
+ // ---------------------------------------------------------------------------
185
+ /**
186
+ * Build the `submit_screen_flow` tool. Exported separately from the server
187
+ * so tests can exercise the handler directly without going through the
188
+ * MCP transport.
189
+ */
190
+ export function createSubmitScreenFlowTool(state) {
191
+ return tool('submit_screen_flow', [
192
+ 'Submit the final screen flow extraction. Call this EXACTLY once,',
193
+ 'when you have finished mapping every screen and transition. Pass the',
194
+ 'full structured flow as the argument. After this call succeeds, end',
195
+ 'your turn — do NOT also paste the same data as a fenced code block.',
196
+ 'If validation fails, the error message tells you what to fix; call',
197
+ 'the tool again with corrected data.',
198
+ ].join(' '), {
199
+ summary: z
200
+ .string()
201
+ .min(1)
202
+ .describe('1-3 sentence narrative of what kind of app this is and its primary user flows.'),
203
+ nodes: z
204
+ .array(screenNodeSchema)
205
+ .describe('Every user-facing screen, modal, drawer, tab, or named state. node.slug MUST be unique within the flow.'),
206
+ edges: z
207
+ .array(screenEdgeSchema)
208
+ .describe('Transitions between screens. Every fromSlug / toSlug MUST reference a slug present in nodes; drop edges whose endpoints you did not emit.'),
209
+ }, async (args) => {
210
+ const extraction = {
211
+ summary: args.summary,
212
+ nodes: args.nodes,
213
+ edges: args.edges,
214
+ };
215
+ const { error } = validateConsistency(extraction);
216
+ if (error) {
217
+ return {
218
+ content: [{ type: 'text', text: error }],
219
+ isError: true,
220
+ };
221
+ }
222
+ state.captured = extraction;
223
+ return {
224
+ content: [
225
+ {
226
+ type: 'text',
227
+ text: `Captured ${extraction.nodes.length} screens / ${extraction.edges.length} transitions. End your turn now.`,
228
+ },
229
+ ],
230
+ };
231
+ });
232
+ }
233
+ /**
234
+ * Build the `record_progress` tool. A side-channel that lets the agent
235
+ * push a human-readable status message to the CLI / desktop UI so a
236
+ * multi-minute extraction doesn't look frozen between text emissions.
237
+ * Returning `{ ok: true }` keeps it cheap — it has no semantic effect
238
+ * on the extraction.
239
+ */
240
+ export function createRecordProgressTool(sink) {
241
+ return tool('record_progress', 'Send a short status update to the user. Does not affect the extraction. Call it at each phase boundary (after detecting the framework, after enumerating routes, while mapping screens, when about to submit) so the user sees progress.', {
242
+ phase: z
243
+ .enum(['detection', 'routing', 'screens', 'transitions', 'submission'])
244
+ .describe('Which phase the message belongs to.'),
245
+ message: z.string().min(1).describe('Human-readable status update.'),
246
+ }, async (args) => {
247
+ sink?.({ phase: args.phase, message: args.message });
248
+ return {
249
+ content: [{ type: 'text', text: 'ok' }],
250
+ };
251
+ });
252
+ }
253
+ export function createScreenFlowMcpServer(state, options) {
254
+ return createSdkMcpServer({
255
+ name: 'screen-flow',
256
+ version: '1.0.0',
257
+ tools: [
258
+ createSubmitScreenFlowTool(state),
259
+ createRecordProgressTool(options?.onProgress),
260
+ ],
261
+ });
262
+ }
@@ -35,5 +35,7 @@ export function createScreenFlowUserPrompt(args) {
35
35
 
36
36
  Start by detecting the framework (check package.json / pubspec.yaml / Package.swift), then locate the router definition or pages directory. Read just enough source per screen to fill in a useful ScreenSchema — do not need to read everything.
37
37
 
38
- When done, emit the single \`screen_flow\` JSON block.`;
38
+ Call \`mcp__screen-flow__record_progress\` at each phase boundary so the user can see your progress (otherwise the CLI looks frozen).
39
+
40
+ When you are done, return the result by **calling the \`mcp__screen-flow__submit_screen_flow\` tool exactly once** with \`summary\`, \`nodes\`, and \`edges\` as arguments. Do not paste the JSON as a fenced text block — the tool call is the deliverable. If the tool returns an error, fix the issue it describes and call the tool again.`;
39
41
  }
@@ -94,19 +94,23 @@ function parseTailwindColors(source) {
94
94
  // either a single string ("primary: '#ff0066'") or an object containing
95
95
  // a 500 key (the Tailwind convention).
96
96
  const primaryString = matchColorEntry(source, 'primary');
97
- if (primaryString)
97
+ if (primaryString) {
98
98
  theme.primary = primaryString;
99
+ }
99
100
  const neutralString = matchColorEntry(source, 'neutral');
100
- if (neutralString)
101
+ if (neutralString) {
101
102
  theme.neutral = neutralString;
103
+ }
102
104
  // Pull a radius default if defined under theme.borderRadius
103
105
  const radiusMatch = source.match(/borderRadius\s*:\s*{[^}]*?(?:DEFAULT|md|lg)\s*:\s*['"]([^'"]+)['"]/);
104
- if (radiusMatch)
106
+ if (radiusMatch) {
105
107
  theme.radius = radiusMatch[1];
108
+ }
106
109
  // Pull the default sans font family
107
110
  const fontMatch = source.match(/fontFamily\s*:\s*{[^}]*?sans\s*:\s*\[?\s*['"]([^'"]+)['"]/);
108
- if (fontMatch)
111
+ if (fontMatch) {
109
112
  theme.font = fontMatch[1];
113
+ }
110
114
  return theme;
111
115
  }
112
116
  function matchColorEntry(source, key) {
@@ -132,22 +136,26 @@ function parseTokensJson(json) {
132
136
  const colors = (json.colors ?? json.color);
133
137
  if (colors && typeof colors === 'object') {
134
138
  const primary = colors.primary;
135
- if (typeof primary === 'string')
139
+ if (typeof primary === 'string') {
136
140
  theme.primary = primary;
141
+ }
137
142
  else if (primary && typeof primary === 'object' && '500' in primary) {
138
143
  theme.primary = primary['500'];
139
144
  }
140
145
  const neutral = colors.neutral;
141
- if (typeof neutral === 'string')
146
+ if (typeof neutral === 'string') {
142
147
  theme.neutral = neutral;
148
+ }
143
149
  else if (neutral && typeof neutral === 'object' && '500' in neutral) {
144
150
  theme.neutral = neutral['500'];
145
151
  }
146
152
  }
147
- if (typeof json.radius === 'string')
153
+ if (typeof json.radius === 'string') {
148
154
  theme.radius = json.radius;
149
- if (typeof json.font === 'string')
155
+ }
156
+ if (typeof json.font === 'string') {
150
157
  theme.font = json.font;
158
+ }
151
159
  return theme;
152
160
  }
153
161
  function parseGlobalCss(source) {
@@ -155,14 +163,17 @@ function parseGlobalCss(source) {
155
163
  const primaryMatch = source.match(/--primary\s*:\s*([^;]+);/);
156
164
  if (primaryMatch) {
157
165
  const value = primaryMatch[1].trim();
158
- if (isColorish(value))
166
+ if (isColorish(value)) {
159
167
  theme.primary = value;
160
- else
161
- theme.primary = `hsl(${value})`; // common shadcn pattern
168
+ }
169
+ else {
170
+ theme.primary = `hsl(${value})`;
171
+ } // common shadcn pattern
162
172
  }
163
173
  const radiusMatch = source.match(/--radius\s*:\s*([^;]+);/);
164
- if (radiusMatch)
174
+ if (radiusMatch) {
165
175
  theme.radius = radiusMatch[1].trim();
176
+ }
166
177
  return theme;
167
178
  }
168
179
  function isColorish(value) {
@@ -20,47 +20,62 @@ function isRecord(value) {
20
20
  return typeof value === 'object' && value !== null;
21
21
  }
22
22
  function isScreenSchema(value) {
23
- if (!isRecord(value))
23
+ if (!isRecord(value)) {
24
24
  return false;
25
- if (typeof value.slug !== 'string' || value.slug.length === 0)
25
+ }
26
+ if (typeof value.slug !== 'string' || value.slug.length === 0) {
26
27
  return false;
27
- if (typeof value.name !== 'string' || value.name.length === 0)
28
+ }
29
+ if (typeof value.name !== 'string' || value.name.length === 0) {
28
30
  return false;
31
+ }
29
32
  if (typeof value.kind !== 'string' || !SCREEN_KINDS.has(value.kind)) {
30
33
  return false;
31
34
  }
32
- if (typeof value.layout !== 'string')
35
+ if (typeof value.layout !== 'string') {
33
36
  return false;
34
- if (!Array.isArray(value.body))
37
+ }
38
+ if (!Array.isArray(value.body)) {
35
39
  return false;
40
+ }
36
41
  return true;
37
42
  }
38
43
  function isScreenEdge(value) {
39
- if (!isRecord(value))
44
+ if (!isRecord(value)) {
40
45
  return false;
41
- if (typeof value.fromSlug !== 'string')
46
+ }
47
+ if (typeof value.fromSlug !== 'string') {
42
48
  return false;
43
- if (typeof value.toSlug !== 'string')
49
+ }
50
+ if (typeof value.toSlug !== 'string') {
44
51
  return false;
45
- if (typeof value.triggerLabel !== 'string')
52
+ }
53
+ if (typeof value.triggerLabel !== 'string') {
46
54
  return false;
55
+ }
47
56
  if (typeof value.kind !== 'string' || !EDGE_KINDS.has(value.kind)) {
48
57
  return false;
49
58
  }
50
59
  return true;
51
60
  }
52
61
  export function isScreenFlowExtraction(value) {
53
- if (!isRecord(value))
62
+ if (!isRecord(value)) {
54
63
  return false;
55
- if (typeof value.summary !== 'string')
64
+ }
65
+ if (typeof value.summary !== 'string') {
56
66
  return false;
57
- if (!Array.isArray(value.nodes))
67
+ }
68
+ if (!Array.isArray(value.nodes)) {
58
69
  return false;
59
- if (!Array.isArray(value.edges))
70
+ }
71
+ if (!Array.isArray(value.edges)) {
60
72
  return false;
61
- if (!value.nodes.every(isScreenSchema))
73
+ }
74
+ if (!value.nodes.every(isScreenSchema)) {
62
75
  return false;
63
- if (!value.edges.every(isScreenEdge))
76
+ }
77
+ if (!value.edges.every(isScreenEdge)) {
64
78
  return false;
79
+ }
65
80
  return true;
66
81
  }
@@ -30,7 +30,7 @@ export async function getBranches(options) {
30
30
  // Fall through to MCP
31
31
  }
32
32
  }
33
- if (branches == null) {
33
+ if (!branches) {
34
34
  const result = (await callMcpEndpoint('branches/list', {
35
35
  issue_id: issueId,
36
36
  }));
@@ -79,7 +79,7 @@ export async function createBranches(options, branches) {
79
79
  // Fall through to MCP
80
80
  }
81
81
  }
82
- if (createdBranches == null) {
82
+ if (!createdBranches) {
83
83
  const result = (await callMcpEndpoint('branches/create', {
84
84
  issue_id: issueId,
85
85
  branches,
@@ -119,7 +119,7 @@ export async function updateBranch(branchId, updates, verbose) {
119
119
  // Fall through to MCP
120
120
  }
121
121
  }
122
- if (updated == null) {
122
+ if (!updated) {
123
123
  const result = (await callMcpEndpoint('branches/update', {
124
124
  branch_id: branchId,
125
125
  ...updates,
@@ -7,7 +7,7 @@ import { DEFAULT_MODEL } from '../../constants.js';
7
7
  import { logDebug } from '../../utils/logger.js';
8
8
  import { loadSkillFile } from './plugin-loader.js';
9
9
  const defaultDeps = {
10
- loadSkillFile: loadSkillFile,
10
+ loadSkillFile,
11
11
  queryFn: query,
12
12
  };
13
13
  // ---- Prompt building (pure) ----
@@ -116,7 +116,7 @@ export async function getPhaseRatings(issueId, phase, verbose) {
116
116
  // Fall through to MCP
117
117
  }
118
118
  }
119
- if (ratings == null) {
119
+ if (!ratings) {
120
120
  const result = await callMcpEndpoint('phase_ratings/list', {
121
121
  issue_id: issueId,
122
122
  phase,
@@ -46,7 +46,7 @@ export async function getPendingLogsByUser(productId, verbose) {
46
46
  // Fall through to MCP
47
47
  }
48
48
  }
49
- if (groups == null) {
49
+ if (!groups) {
50
50
  const result = (await callMcpEndpoint('product_logs/pending_by_user', {
51
51
  product_id: productId,
52
52
  }));