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.
- package/dist/api/cross-product.js +0 -1
- package/dist/api/issues/issue-utils.js +0 -1
- package/dist/api/issues/update-issue.js +1 -1
- package/dist/commands/agent-workflow/chat-worker.js +1 -1
- package/dist/commands/checklists/index.js +1 -1
- package/dist/commands/product-techniques/index.d.ts +15 -0
- package/dist/commands/product-techniques/index.js +37 -0
- package/dist/commands/workflow/executors/phase-executor.js +1 -1
- package/dist/index.js +24 -1
- package/dist/phases/analyze-logs/index.js +1 -1
- package/dist/phases/bug-fixing/context-fetcher.js +4 -2
- package/dist/phases/find-features/index.js +1 -1
- package/dist/phases/output-contracts.js +47 -36
- package/dist/phases/pr-shared/agent-utils.d.ts +11 -3
- package/dist/phases/pr-shared/agent-utils.js +48 -4
- package/dist/phases/product-techniques/index.d.ts +52 -0
- package/dist/phases/product-techniques/index.js +268 -0
- package/dist/phases/product-techniques/mcp-server.d.ts +41 -0
- package/dist/phases/product-techniques/mcp-server.js +96 -0
- package/dist/phases/product-techniques/prompts.d.ts +19 -0
- package/dist/phases/product-techniques/prompts.js +66 -0
- package/dist/phases/product-techniques/types.d.ts +13 -0
- package/dist/phases/product-techniques/types.js +13 -0
- package/dist/phases/screen-flow/index.js +73 -17
- package/dist/phases/screen-flow/mcp-server.d.ts +195 -0
- package/dist/phases/screen-flow/mcp-server.js +262 -0
- package/dist/phases/screen-flow/prompts.js +3 -1
- package/dist/phases/screen-flow/theme.js +23 -12
- package/dist/phases/screen-flow/types.js +30 -15
- package/dist/services/branches.js +3 -3
- package/dist/services/phase-hooks/hook-executor.js +1 -1
- package/dist/services/phase-ratings.js +1 -1
- package/dist/services/product-logs.js +1 -1
- package/dist/services/pull-requests.js +3 -3
- package/package.json +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
25
|
+
}
|
|
26
|
+
if (typeof value.slug !== 'string' || value.slug.length === 0) {
|
|
26
27
|
return false;
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
+
}
|
|
47
|
+
if (typeof value.fromSlug !== 'string') {
|
|
42
48
|
return false;
|
|
43
|
-
|
|
49
|
+
}
|
|
50
|
+
if (typeof value.toSlug !== 'string') {
|
|
44
51
|
return false;
|
|
45
|
-
|
|
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
|
-
|
|
64
|
+
}
|
|
65
|
+
if (typeof value.summary !== 'string') {
|
|
56
66
|
return false;
|
|
57
|
-
|
|
67
|
+
}
|
|
68
|
+
if (!Array.isArray(value.nodes)) {
|
|
58
69
|
return false;
|
|
59
|
-
|
|
70
|
+
}
|
|
71
|
+
if (!Array.isArray(value.edges)) {
|
|
60
72
|
return false;
|
|
61
|
-
|
|
73
|
+
}
|
|
74
|
+
if (!value.nodes.every(isScreenSchema)) {
|
|
62
75
|
return false;
|
|
63
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
10
|
+
loadSkillFile,
|
|
11
11
|
queryFn: query,
|
|
12
12
|
};
|
|
13
13
|
// ---- Prompt building (pure) ----
|