@gakim-digital/dexter-bridge 0.11.0 → 0.11.2
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/README.md +30 -17
- package/package.json +1 -1
- package/src/agent.js +265 -44
- package/src/api.js +34 -0
- package/src/config.js +5 -0
- package/src/framerAgentTools.js +1643 -192
- package/src/framerBehaviorContract.js +962 -0
- package/src/harnessMcpServer.js +91 -189
- package/src/logger.js +1 -0
- package/src/protocol.js +63 -14
- package/src/providers/codexAppServer.js +14 -6
package/src/framerAgentTools.js
CHANGED
|
@@ -1,20 +1,62 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
2
3
|
import { spawn } from 'node:child_process';
|
|
3
4
|
import { createRequire } from 'node:module';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
BEHAVIOR_ACTIVATIONS,
|
|
8
|
+
BEHAVIOR_LAYOUT_EFFECTS,
|
|
9
|
+
BEHAVIOR_MECHANISMS,
|
|
10
|
+
BEHAVIOR_PRESENTATIONS,
|
|
11
|
+
canonicalIdMappings,
|
|
12
|
+
evidenceNodeIds,
|
|
13
|
+
mergeBehaviorPlans,
|
|
14
|
+
normalizeBehaviorEvidence,
|
|
15
|
+
normalizeBehaviorPlan,
|
|
16
|
+
summarizeFramerChanges,
|
|
17
|
+
verifyBehaviorEvidence,
|
|
18
|
+
} from './framerBehaviorContract.js';
|
|
4
19
|
|
|
5
20
|
const require = createRequire(import.meta.url);
|
|
6
21
|
const MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
22
|
+
const MAX_MODEL_RESULT_BYTES = 120 * 1024;
|
|
7
23
|
const DEFAULT_COMMAND_TIMEOUT_MS = 5 * 60_000;
|
|
8
24
|
const SESSION_TIMEOUT_MS = 10 * 60_000;
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
25
|
+
const MAX_UNSAFE_WRITE_FAILURES = 2;
|
|
26
|
+
const MAX_LOW_LEVEL_WRITE_FAILURES = 3;
|
|
27
|
+
const MAX_GUIDANCE_BYTES = 180 * 1024;
|
|
28
|
+
const MAX_GUIDANCE_FILE_BYTES = 48 * 1024;
|
|
29
|
+
const PROJECT_ID_PATTERN = /^[A-Za-z0-9]{20}$/;
|
|
30
|
+
const PROJECT_SEGMENT_PATTERN =
|
|
31
|
+
/^(?:.+--)?([A-Za-z0-9]+)(?:-[A-Za-z0-9]+)?$/;
|
|
32
|
+
const FRAMER_PROJECT_HOST_PATTERN = /(^|\.)framer(?:local)?\.com$/i;
|
|
12
33
|
const PAGE_PATH_PATTERN = /^\/[^\0\r\n]{0,499}$/;
|
|
13
34
|
const UNSAFE_EXEC_PATTERN =
|
|
14
35
|
/\b(?:process|globalThis|global|require|import\s*\(|child_process|worker_threads|fs|os|net|tls|dgram|http|https|fetch|WebSocket|eval|Function)\b/;
|
|
15
36
|
const MUTATING_EXEC_PATTERN =
|
|
16
37
|
/\.(?:set|add|create|remove|delete|update|insert|move|clone|duplicate|upload|publish|deploy|switch|merge|rename|join|leave|apply)[A-Za-z0-9_]*\s*\(/;
|
|
17
|
-
|
|
38
|
+
const TASK_DOMAINS = [
|
|
39
|
+
'visual',
|
|
40
|
+
'content',
|
|
41
|
+
'links',
|
|
42
|
+
'interactions',
|
|
43
|
+
'responsive',
|
|
44
|
+
'data',
|
|
45
|
+
'code',
|
|
46
|
+
'structure',
|
|
47
|
+
];
|
|
48
|
+
const VERIFICATION_CHECKS = [
|
|
49
|
+
'structural',
|
|
50
|
+
'visual',
|
|
51
|
+
'links',
|
|
52
|
+
'interactions',
|
|
53
|
+
'responsive',
|
|
54
|
+
'code',
|
|
55
|
+
'data',
|
|
56
|
+
];
|
|
57
|
+
const GENERIC_VERIFICATION_CHECKS = VERIFICATION_CHECKS.filter(
|
|
58
|
+
(check) => check !== 'interactions',
|
|
59
|
+
);
|
|
18
60
|
const EMPTY_SCHEMA = {
|
|
19
61
|
type: 'object',
|
|
20
62
|
properties: {},
|
|
@@ -25,7 +67,7 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
25
67
|
{
|
|
26
68
|
name: 'progress_update',
|
|
27
69
|
description:
|
|
28
|
-
'Tell the user what you are
|
|
70
|
+
'Tell the user what you are about to do or are doing now in one or two natural first-person sentences. Name the specific page area, component, or behavior and the concrete change; optionally say what you will check next. Avoid generic updates such as "working on the next step", "thinking", "loading", or "making changes". Call this before the first Framer inspection or edit and at meaningful phase changes.',
|
|
29
71
|
inputSchema: {
|
|
30
72
|
type: 'object',
|
|
31
73
|
properties: {
|
|
@@ -38,8 +80,18 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
38
80
|
{
|
|
39
81
|
name: 'framer_instructions',
|
|
40
82
|
description:
|
|
41
|
-
'
|
|
42
|
-
inputSchema:
|
|
83
|
+
'Get concise Framer Agent guidance, or request targeted official API documentation when an exact method or type is unclear. Most runs should rely on the tool descriptions and call this only when needed.',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
queries: {
|
|
88
|
+
type: 'array',
|
|
89
|
+
maxItems: 8,
|
|
90
|
+
items: { type: 'string', minLength: 1, maxLength: 120 },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
additionalProperties: false,
|
|
94
|
+
},
|
|
43
95
|
},
|
|
44
96
|
{
|
|
45
97
|
name: 'framer_context',
|
|
@@ -61,11 +113,275 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
61
113
|
items: { type: 'object', additionalProperties: true },
|
|
62
114
|
},
|
|
63
115
|
pagePath: { type: 'string', maxLength: 500, default: '/' },
|
|
116
|
+
verifies: {
|
|
117
|
+
type: 'array',
|
|
118
|
+
uniqueItems: true,
|
|
119
|
+
maxItems: 6,
|
|
120
|
+
items: { type: 'string', enum: GENERIC_VERIFICATION_CHECKS },
|
|
121
|
+
description:
|
|
122
|
+
'Verification evidence this read is intended to establish after mutations. Interaction verification requires framer_verify_interactions.',
|
|
123
|
+
},
|
|
64
124
|
},
|
|
65
125
|
required: ['queries'],
|
|
66
126
|
additionalProperties: false,
|
|
67
127
|
},
|
|
68
128
|
},
|
|
129
|
+
{
|
|
130
|
+
name: 'framer_plan_task',
|
|
131
|
+
description:
|
|
132
|
+
'After inspecting the live Framer target and before the first mutation, declare your semantic understanding, observable behavior requirements, chosen implementation mechanisms, and completion evidence. Dexter does not classify the request or choose mechanisms for you.',
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
summary: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
minLength: 10,
|
|
139
|
+
maxLength: 1000,
|
|
140
|
+
},
|
|
141
|
+
domains: {
|
|
142
|
+
type: 'array',
|
|
143
|
+
minItems: 1,
|
|
144
|
+
uniqueItems: true,
|
|
145
|
+
items: { type: 'string', enum: TASK_DOMAINS },
|
|
146
|
+
},
|
|
147
|
+
verification: {
|
|
148
|
+
type: 'array',
|
|
149
|
+
minItems: 1,
|
|
150
|
+
uniqueItems: true,
|
|
151
|
+
items: { type: 'string', enum: VERIFICATION_CHECKS },
|
|
152
|
+
},
|
|
153
|
+
behaviors: {
|
|
154
|
+
type: 'array',
|
|
155
|
+
maxItems: 20,
|
|
156
|
+
description:
|
|
157
|
+
'Required for interaction work. Describe observable behavior independently from its implementation.',
|
|
158
|
+
items: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {
|
|
161
|
+
id: { type: 'string', minLength: 1, maxLength: 120 },
|
|
162
|
+
description: { type: 'string', minLength: 5, maxLength: 500 },
|
|
163
|
+
triggerDescription: {
|
|
164
|
+
type: 'string',
|
|
165
|
+
minLength: 3,
|
|
166
|
+
maxLength: 300,
|
|
167
|
+
},
|
|
168
|
+
activation: {
|
|
169
|
+
type: 'string',
|
|
170
|
+
enum: BEHAVIOR_ACTIVATIONS,
|
|
171
|
+
},
|
|
172
|
+
presentation: {
|
|
173
|
+
type: 'string',
|
|
174
|
+
enum: BEHAVIOR_PRESENTATIONS,
|
|
175
|
+
},
|
|
176
|
+
stateChange: { type: 'boolean' },
|
|
177
|
+
layoutEffect: {
|
|
178
|
+
type: 'string',
|
|
179
|
+
enum: BEHAVIOR_LAYOUT_EFFECTS,
|
|
180
|
+
},
|
|
181
|
+
reversible: { type: 'boolean' },
|
|
182
|
+
repeatable: { type: 'boolean' },
|
|
183
|
+
expectedTargets: {
|
|
184
|
+
type: 'integer',
|
|
185
|
+
minimum: 1,
|
|
186
|
+
maximum: 50,
|
|
187
|
+
},
|
|
188
|
+
expectedContent: {
|
|
189
|
+
type: 'array',
|
|
190
|
+
maxItems: 20,
|
|
191
|
+
items: { type: 'string', minLength: 1, maxLength: 500 },
|
|
192
|
+
},
|
|
193
|
+
initialState: {
|
|
194
|
+
type: 'string',
|
|
195
|
+
minLength: 1,
|
|
196
|
+
maxLength: 300,
|
|
197
|
+
},
|
|
198
|
+
activatedState: {
|
|
199
|
+
type: 'string',
|
|
200
|
+
minLength: 1,
|
|
201
|
+
maxLength: 300,
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
required: [
|
|
205
|
+
'id',
|
|
206
|
+
'description',
|
|
207
|
+
'triggerDescription',
|
|
208
|
+
'activation',
|
|
209
|
+
'presentation',
|
|
210
|
+
'stateChange',
|
|
211
|
+
'layoutEffect',
|
|
212
|
+
'reversible',
|
|
213
|
+
'repeatable',
|
|
214
|
+
'expectedTargets',
|
|
215
|
+
'initialState',
|
|
216
|
+
'activatedState',
|
|
217
|
+
],
|
|
218
|
+
additionalProperties: false,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
mechanisms: {
|
|
222
|
+
type: 'array',
|
|
223
|
+
maxItems: 20,
|
|
224
|
+
description:
|
|
225
|
+
'Required for every declared behavior. Choose the mechanism from live project capabilities and explain why it satisfies the behavior.',
|
|
226
|
+
items: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
behaviorId: { type: 'string', minLength: 1, maxLength: 120 },
|
|
230
|
+
mechanism: {
|
|
231
|
+
type: 'string',
|
|
232
|
+
enum: BEHAVIOR_MECHANISMS,
|
|
233
|
+
},
|
|
234
|
+
rationale: {
|
|
235
|
+
type: 'string',
|
|
236
|
+
minLength: 10,
|
|
237
|
+
maxLength: 800,
|
|
238
|
+
},
|
|
239
|
+
representativeFirst: { type: 'boolean', default: true },
|
|
240
|
+
},
|
|
241
|
+
required: ['behaviorId', 'mechanism', 'rationale'],
|
|
242
|
+
additionalProperties: false,
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
rejectedMechanisms: {
|
|
246
|
+
type: 'array',
|
|
247
|
+
maxItems: 30,
|
|
248
|
+
description:
|
|
249
|
+
'Mechanisms already disproven by user-observed failure or prior verification. Do not silently reuse them.',
|
|
250
|
+
items: {
|
|
251
|
+
type: 'object',
|
|
252
|
+
properties: {
|
|
253
|
+
behaviorId: { type: 'string', minLength: 1, maxLength: 120 },
|
|
254
|
+
behaviorSignature: {
|
|
255
|
+
type: 'string',
|
|
256
|
+
minLength: 1,
|
|
257
|
+
maxLength: 128,
|
|
258
|
+
},
|
|
259
|
+
mechanism: {
|
|
260
|
+
type: 'string',
|
|
261
|
+
enum: BEHAVIOR_MECHANISMS,
|
|
262
|
+
},
|
|
263
|
+
reason: { type: 'string', minLength: 5, maxLength: 800 },
|
|
264
|
+
source: {
|
|
265
|
+
type: 'string',
|
|
266
|
+
enum: ['user_reported_failure', 'verification_failure'],
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
required: ['mechanism', 'reason'],
|
|
270
|
+
additionalProperties: false,
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
required: ['summary', 'domains', 'verification'],
|
|
275
|
+
additionalProperties: false,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: 'framer_verify_interactions',
|
|
280
|
+
description:
|
|
281
|
+
'Verify the observable behavior contract from the live Framer tree. The runtime derives the required action and invariants from the planned behavior and mechanism; supply only canonical evidence node IDs. This is the only tool that can satisfy interaction verification.',
|
|
282
|
+
inputSchema: {
|
|
283
|
+
type: 'object',
|
|
284
|
+
properties: {
|
|
285
|
+
pagePath: { type: 'string', maxLength: 500, default: '/' },
|
|
286
|
+
checks: {
|
|
287
|
+
type: 'array',
|
|
288
|
+
minItems: 1,
|
|
289
|
+
maxItems: 20,
|
|
290
|
+
items: {
|
|
291
|
+
type: 'object',
|
|
292
|
+
properties: {
|
|
293
|
+
description: {
|
|
294
|
+
type: 'string',
|
|
295
|
+
minLength: 1,
|
|
296
|
+
maxLength: 300,
|
|
297
|
+
},
|
|
298
|
+
behaviorId: {
|
|
299
|
+
type: 'string',
|
|
300
|
+
minLength: 1,
|
|
301
|
+
maxLength: 120,
|
|
302
|
+
},
|
|
303
|
+
triggerNodeId: {
|
|
304
|
+
type: 'string',
|
|
305
|
+
minLength: 1,
|
|
306
|
+
maxLength: 200,
|
|
307
|
+
},
|
|
308
|
+
event: {
|
|
309
|
+
type: 'string',
|
|
310
|
+
minLength: 1,
|
|
311
|
+
maxLength: 80,
|
|
312
|
+
},
|
|
313
|
+
targetNodeId: {
|
|
314
|
+
type: 'string',
|
|
315
|
+
minLength: 1,
|
|
316
|
+
maxLength: 200,
|
|
317
|
+
},
|
|
318
|
+
componentNodeId: {
|
|
319
|
+
type: 'string',
|
|
320
|
+
minLength: 1,
|
|
321
|
+
maxLength: 200,
|
|
322
|
+
},
|
|
323
|
+
instanceNodeIds: {
|
|
324
|
+
type: 'array',
|
|
325
|
+
maxItems: 50,
|
|
326
|
+
uniqueItems: true,
|
|
327
|
+
items: {
|
|
328
|
+
type: 'string',
|
|
329
|
+
minLength: 1,
|
|
330
|
+
maxLength: 200,
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
openVariantNodeId: {
|
|
334
|
+
type: 'string',
|
|
335
|
+
minLength: 1,
|
|
336
|
+
maxLength: 200,
|
|
337
|
+
},
|
|
338
|
+
closedVariantNodeId: {
|
|
339
|
+
type: 'string',
|
|
340
|
+
minLength: 1,
|
|
341
|
+
maxLength: 200,
|
|
342
|
+
},
|
|
343
|
+
openAnswerNodeId: {
|
|
344
|
+
type: 'string',
|
|
345
|
+
minLength: 1,
|
|
346
|
+
maxLength: 200,
|
|
347
|
+
},
|
|
348
|
+
closedAnswerNodeId: {
|
|
349
|
+
type: 'string',
|
|
350
|
+
minLength: 1,
|
|
351
|
+
maxLength: 200,
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
required: [
|
|
355
|
+
'behaviorId',
|
|
356
|
+
'triggerNodeId',
|
|
357
|
+
'event',
|
|
358
|
+
],
|
|
359
|
+
additionalProperties: false,
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
required: ['checks'],
|
|
364
|
+
additionalProperties: false,
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: 'framer_query_images',
|
|
369
|
+
description:
|
|
370
|
+
'Search Framer Agent’s approved image source for real-world imagery. Use this only when the design needs a concrete photo or editorial image and no suitable user attachment or existing project image is available.',
|
|
371
|
+
inputSchema: {
|
|
372
|
+
type: 'object',
|
|
373
|
+
properties: {
|
|
374
|
+
query: { type: 'string', minLength: 2, maxLength: 200 },
|
|
375
|
+
count: { type: 'integer', minimum: 1, maximum: 8, default: 4 },
|
|
376
|
+
orientation: {
|
|
377
|
+
type: 'string',
|
|
378
|
+
enum: ['landscape', 'portrait', 'squarish'],
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
required: ['query'],
|
|
382
|
+
additionalProperties: false,
|
|
383
|
+
},
|
|
384
|
+
},
|
|
69
385
|
{
|
|
70
386
|
name: 'framer_apply_changes',
|
|
71
387
|
description:
|
|
@@ -88,6 +404,14 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
88
404
|
type: 'object',
|
|
89
405
|
properties: {
|
|
90
406
|
code: { type: 'string', minLength: 1, maxLength: 200_000 },
|
|
407
|
+
verifies: {
|
|
408
|
+
type: 'array',
|
|
409
|
+
uniqueItems: true,
|
|
410
|
+
maxItems: 6,
|
|
411
|
+
items: { type: 'string', enum: GENERIC_VERIFICATION_CHECKS },
|
|
412
|
+
description:
|
|
413
|
+
'Verification evidence this read is intended to establish after mutations. Interaction verification requires framer_verify_interactions.',
|
|
414
|
+
},
|
|
91
415
|
},
|
|
92
416
|
required: ['code'],
|
|
93
417
|
additionalProperties: false,
|
|
@@ -96,13 +420,20 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
96
420
|
{
|
|
97
421
|
name: 'framer_write',
|
|
98
422
|
description:
|
|
99
|
-
'Execute project-scoped JavaScript with the official Framer Agent connection for mutations not covered by applyChanges, such as code files, localization, redirects, assets, CMS, or branch operations. Publishing is disabled unless Dexter explicitly authorizes it.',
|
|
423
|
+
'Execute project-scoped JavaScript with the official Framer Agent connection for mutations not covered by applyChanges, such as code files, localization, redirects, assets, CMS, or branch operations. Publishing is disabled unless Dexter explicitly authorizes it. If a write is rejected as unsafe, do not retry JavaScript variants; switch to framer_apply_changes or report the concrete blocker.',
|
|
100
424
|
inputSchema: {
|
|
101
425
|
type: 'object',
|
|
102
426
|
properties: {
|
|
103
427
|
code: { type: 'string', minLength: 1, maxLength: 200_000 },
|
|
428
|
+
reason: {
|
|
429
|
+
type: 'string',
|
|
430
|
+
minLength: 10,
|
|
431
|
+
maxLength: 500,
|
|
432
|
+
description:
|
|
433
|
+
'Explain the specific Framer capability needed and why framer_apply_changes cannot perform it.',
|
|
434
|
+
},
|
|
104
435
|
},
|
|
105
|
-
required: ['code'],
|
|
436
|
+
required: ['code', 'reason'],
|
|
106
437
|
additionalProperties: false,
|
|
107
438
|
},
|
|
108
439
|
},
|
|
@@ -131,6 +462,67 @@ function boundedText(value, maximum) {
|
|
|
131
462
|
return `${text.slice(0, maximum)}\n...[output clipped]`;
|
|
132
463
|
}
|
|
133
464
|
|
|
465
|
+
function hasInstalledFramerSkill(env) {
|
|
466
|
+
const home = String(env?.HOME || '').trim();
|
|
467
|
+
if (!home) return false;
|
|
468
|
+
return [
|
|
469
|
+
path.join(home, '.agents', 'skills', 'framer', 'SKILL.md'),
|
|
470
|
+
path.join(home, '.claude', 'skills', 'framer', 'SKILL.md'),
|
|
471
|
+
].some((skillPath) => fs.existsSync(skillPath));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function framerSkillRoot(env) {
|
|
475
|
+
const home = String(env?.HOME || '').trim();
|
|
476
|
+
if (!home) return null;
|
|
477
|
+
return [
|
|
478
|
+
path.join(home, '.agents', 'skills', 'framer'),
|
|
479
|
+
path.join(home, '.claude', 'skills', 'framer'),
|
|
480
|
+
].find((root) => fs.existsSync(path.join(root, 'SKILL.md'))) || null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function installedFramerAgentVersion() {
|
|
484
|
+
try {
|
|
485
|
+
let current = path.dirname(resolveFramerAgentCli());
|
|
486
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
487
|
+
const packagePath = path.join(current, 'package.json');
|
|
488
|
+
if (fs.existsSync(packagePath)) {
|
|
489
|
+
const manifest = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
490
|
+
if (manifest?.name === '@framer/agent') {
|
|
491
|
+
return String(manifest.version || '').trim() || null;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const parent = path.dirname(current);
|
|
495
|
+
if (parent === current) break;
|
|
496
|
+
current = parent;
|
|
497
|
+
}
|
|
498
|
+
} catch {
|
|
499
|
+
// Version telemetry must never block a run.
|
|
500
|
+
}
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function readGuidanceFile(projectRoot, relativePath, remainingBytes) {
|
|
505
|
+
const normalized = relativePath.replaceAll('\\', '/');
|
|
506
|
+
if (
|
|
507
|
+
normalized.startsWith('/')
|
|
508
|
+
|| normalized.split('/').some((segment) => segment === '..')
|
|
509
|
+
) return null;
|
|
510
|
+
const absolutePath = path.resolve(projectRoot, normalized);
|
|
511
|
+
if (
|
|
512
|
+
absolutePath !== projectRoot
|
|
513
|
+
&& !absolutePath.startsWith(`${projectRoot}${path.sep}`)
|
|
514
|
+
) return null;
|
|
515
|
+
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
const maximum = Math.min(MAX_GUIDANCE_FILE_BYTES, remainingBytes);
|
|
519
|
+
if (maximum <= 0) return null;
|
|
520
|
+
return {
|
|
521
|
+
path: normalized,
|
|
522
|
+
content: boundedText(fs.readFileSync(absolutePath, 'utf8'), maximum),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
134
526
|
function normalizeProjectId(value) {
|
|
135
527
|
const projectId = String(value || '').trim();
|
|
136
528
|
if (!PROJECT_ID_PATTERN.test(projectId)) {
|
|
@@ -142,6 +534,55 @@ function normalizeProjectId(value) {
|
|
|
142
534
|
return projectId;
|
|
143
535
|
}
|
|
144
536
|
|
|
537
|
+
function projectIdFromUrl(url) {
|
|
538
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
539
|
+
if (
|
|
540
|
+
segments.length !== 2
|
|
541
|
+
|| segments[0]?.toLowerCase() !== 'projects'
|
|
542
|
+
|| !segments[1]
|
|
543
|
+
) return '';
|
|
544
|
+
try {
|
|
545
|
+
const projectId = decodeURIComponent(segments[1])
|
|
546
|
+
.match(PROJECT_SEGMENT_PATTERN)?.[1] || '';
|
|
547
|
+
return PROJECT_ID_PATTERN.test(projectId) ? projectId : '';
|
|
548
|
+
} catch {
|
|
549
|
+
return '';
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function normalizeProjectUrl(value, expectedProjectId) {
|
|
554
|
+
let url;
|
|
555
|
+
try {
|
|
556
|
+
url = new URL(String(value || '').trim());
|
|
557
|
+
} catch {
|
|
558
|
+
throw toolError(
|
|
559
|
+
'FRAMER_AGENT_PROJECT_URL_INVALID',
|
|
560
|
+
'The exact Framer project URL is missing or invalid.',
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
if (
|
|
564
|
+
url.protocol !== 'https:'
|
|
565
|
+
|| !FRAMER_PROJECT_HOST_PATTERN.test(url.hostname)
|
|
566
|
+
|| url.username
|
|
567
|
+
|| url.password
|
|
568
|
+
) {
|
|
569
|
+
throw toolError(
|
|
570
|
+
'FRAMER_AGENT_PROJECT_URL_INVALID',
|
|
571
|
+
'The exact Framer project URL is missing or invalid.',
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
const projectId = projectIdFromUrl(url);
|
|
575
|
+
if (!projectId || projectId !== expectedProjectId) {
|
|
576
|
+
throw toolError(
|
|
577
|
+
'FRAMER_AGENT_PROJECT_MISMATCH',
|
|
578
|
+
'The Framer project URL does not match the project selected in Dexter.',
|
|
579
|
+
409,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
url.hash = '';
|
|
583
|
+
return url.toString();
|
|
584
|
+
}
|
|
585
|
+
|
|
145
586
|
function normalizePagePath(value) {
|
|
146
587
|
const pagePath = String(value || '/').trim() || '/';
|
|
147
588
|
if (!PAGE_PATH_PATTERN.test(pagePath)) {
|
|
@@ -164,6 +605,156 @@ function uniqueStrings(values, maximum = 500) {
|
|
|
164
605
|
).slice(0, maximum);
|
|
165
606
|
}
|
|
166
607
|
|
|
608
|
+
function normalizeVerificationValues(values, allowed) {
|
|
609
|
+
return uniqueStrings(Array.isArray(values) ? values : [])
|
|
610
|
+
.filter((value) => allowed.includes(value));
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function normalizeTaskPlan(rawArguments, inheritedRejectedMechanisms = []) {
|
|
614
|
+
const summary = String(rawArguments.summary || '').trim().slice(0, 1000);
|
|
615
|
+
const domains = normalizeVerificationValues(
|
|
616
|
+
rawArguments.domains,
|
|
617
|
+
TASK_DOMAINS,
|
|
618
|
+
);
|
|
619
|
+
const requestedVerification = normalizeVerificationValues(
|
|
620
|
+
rawArguments.verification,
|
|
621
|
+
VERIFICATION_CHECKS,
|
|
622
|
+
);
|
|
623
|
+
if (summary.length < 10 || domains.length === 0) {
|
|
624
|
+
throw toolError(
|
|
625
|
+
'FRAMER_HARNESS_PLAN_INVALID',
|
|
626
|
+
'Describe the understood task and declare at least one task domain.',
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
const verification = new Set(['structural', ...requestedVerification]);
|
|
630
|
+
if (
|
|
631
|
+
domains.some((domain) =>
|
|
632
|
+
['visual', 'interactions', 'responsive'].includes(domain))
|
|
633
|
+
) verification.add('visual');
|
|
634
|
+
if (domains.includes('links')) verification.add('links');
|
|
635
|
+
if (domains.includes('interactions')) verification.add('interactions');
|
|
636
|
+
if (domains.includes('responsive')) verification.add('responsive');
|
|
637
|
+
if (domains.includes('code')) verification.add('code');
|
|
638
|
+
if (domains.includes('data')) verification.add('data');
|
|
639
|
+
const behaviorPlan = normalizeBehaviorPlan(
|
|
640
|
+
rawArguments,
|
|
641
|
+
inheritedRejectedMechanisms,
|
|
642
|
+
);
|
|
643
|
+
if (
|
|
644
|
+
domains.includes('interactions')
|
|
645
|
+
&& behaviorPlan.behaviors.length === 0
|
|
646
|
+
) {
|
|
647
|
+
throw toolError(
|
|
648
|
+
'FRAMER_BEHAVIOR_CONTRACT_REQUIRED',
|
|
649
|
+
'Interaction work requires observable behaviors and one compatible mechanism decision per behavior.',
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
if (behaviorPlan.behaviors.length > 0) {
|
|
653
|
+
if (!domains.includes('interactions')) domains.push('interactions');
|
|
654
|
+
verification.add('interactions');
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
summary,
|
|
658
|
+
domains,
|
|
659
|
+
verification: [...verification],
|
|
660
|
+
...behaviorPlan,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function workingPreferencesFromAssignment(assignment) {
|
|
665
|
+
const context = isRecord(assignment?.context) ? assignment.context : {};
|
|
666
|
+
const grant = isRecord(context.contextGrant) ? context.contextGrant : {};
|
|
667
|
+
const supplied = isRecord(context.workingPreferences)
|
|
668
|
+
? context.workingPreferences
|
|
669
|
+
: {};
|
|
670
|
+
const selectedNodeIds = uniqueStrings([
|
|
671
|
+
...(Array.isArray(context.selectedNodeIds)
|
|
672
|
+
? context.selectedNodeIds
|
|
673
|
+
: []),
|
|
674
|
+
...(Array.isArray(grant.selectedNodeIds)
|
|
675
|
+
? grant.selectedNodeIds
|
|
676
|
+
: []),
|
|
677
|
+
]);
|
|
678
|
+
const selectionFirst =
|
|
679
|
+
supplied.focus === 'selection-first'
|
|
680
|
+
|| (
|
|
681
|
+
selectedNodeIds.length > 0
|
|
682
|
+
&& grant.writeScope === 'selection'
|
|
683
|
+
);
|
|
684
|
+
return {
|
|
685
|
+
mode: selectionFirst ? 'selection-first' : 'standard',
|
|
686
|
+
writeScope: String(
|
|
687
|
+
supplied.writeScope || grant.writeScope || 'none',
|
|
688
|
+
).trim() || 'none',
|
|
689
|
+
selectionFirst,
|
|
690
|
+
preferBatchedMutations: supplied.preferBatchedMutations !== false,
|
|
691
|
+
avoidRepeatedInspection: supplied.avoidRepeatedInspection !== false,
|
|
692
|
+
expandScopeOnlyWhenNeeded:
|
|
693
|
+
supplied.expandScopeOnlyWhenNeeded !== false,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function toolDefinitionsForPreferences(requestedTools, preferences) {
|
|
698
|
+
return FRAMER_AGENT_TOOL_DEFINITIONS
|
|
699
|
+
.filter((tool) => requestedTools.has(tool.name))
|
|
700
|
+
.map((tool) => {
|
|
701
|
+
if (!preferences.selectionFirst) return tool;
|
|
702
|
+
if (tool.name === 'framer_context') {
|
|
703
|
+
return {
|
|
704
|
+
...tool,
|
|
705
|
+
description:
|
|
706
|
+
'Read broad project context only when the selected subtree lacks a concrete dependency required for the request. Do not use this as the default first inspection.',
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
if (tool.name === 'framer_read_project') {
|
|
710
|
+
return {
|
|
711
|
+
...tool,
|
|
712
|
+
description:
|
|
713
|
+
'Run supported Framer readProject queries, especially screenshots. For node-tree inspection, prefer framer_read with framer.agent.getNode({ id }), getNodes({ ids }), serialize, or other focused node APIs. Batch related queries when practical.',
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
if (tool.name === 'framer_apply_changes') {
|
|
717
|
+
return {
|
|
718
|
+
...tool,
|
|
719
|
+
description:
|
|
720
|
+
'Apply related changes inside the authorized selection in coherent batches when practical. Read the returned diagnostics, verify the result, and make any further repair batches the work requires.',
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
if (tool.name === 'framer_read') {
|
|
724
|
+
return {
|
|
725
|
+
...tool,
|
|
726
|
+
description:
|
|
727
|
+
'Use focused JavaScript for node-tree inspection and Framer capabilities not covered by readProject. Start with the selected nodes, reuse prior evidence, and expand only when a concrete dependency requires it.',
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
if (tool.name === 'framer_write') {
|
|
731
|
+
return {
|
|
732
|
+
...tool,
|
|
733
|
+
description:
|
|
734
|
+
'Use project-scoped JavaScript only when framer_apply_changes cannot perform the required authorized capability. Explain that limitation in reason, prefer cohesive operations, and keep mutations within the authorized write scope. Do not retry JavaScript variants after a safety rejection.',
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
return tool;
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function boundedStructuredValue(value, maximum = MAX_MODEL_RESULT_BYTES) {
|
|
742
|
+
let serialized;
|
|
743
|
+
try {
|
|
744
|
+
serialized = JSON.stringify(value);
|
|
745
|
+
} catch {
|
|
746
|
+
return {
|
|
747
|
+
truncated: true,
|
|
748
|
+
output: '[Framer result could not be serialized.]',
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
if (Buffer.byteLength(serialized, 'utf8') <= maximum) return value;
|
|
752
|
+
return {
|
|
753
|
+
truncated: true,
|
|
754
|
+
output: boundedText(serialized, maximum),
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
167
758
|
function authoritativeNodeIdsFromAssignment(assignment) {
|
|
168
759
|
const context = isRecord(assignment?.context) ? assignment.context : {};
|
|
169
760
|
const grant = isRecord(context.contextGrant) ? context.contextGrant : {};
|
|
@@ -237,89 +828,6 @@ function abortError() {
|
|
|
237
828
|
});
|
|
238
829
|
}
|
|
239
830
|
|
|
240
|
-
function wait(delayMs, signal) {
|
|
241
|
-
return new Promise((resolve, reject) => {
|
|
242
|
-
if (signal?.aborted) {
|
|
243
|
-
reject(abortError());
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
const onAbort = () => {
|
|
247
|
-
clearTimeout(timer);
|
|
248
|
-
reject(abortError());
|
|
249
|
-
};
|
|
250
|
-
const timer = setTimeout(() => {
|
|
251
|
-
signal?.removeEventListener('abort', onAbort);
|
|
252
|
-
resolve();
|
|
253
|
-
}, Math.max(0, delayMs));
|
|
254
|
-
timer.unref?.();
|
|
255
|
-
signal?.addEventListener('abort', onAbort, { once: true });
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function authorizationUrl(value) {
|
|
260
|
-
let parsed;
|
|
261
|
-
try {
|
|
262
|
-
parsed = new URL(String(value || ''));
|
|
263
|
-
} catch {
|
|
264
|
-
throw toolError(
|
|
265
|
-
'FRAMER_AGENT_AUTHORIZATION_URL_INVALID',
|
|
266
|
-
'Dexter received an invalid Framer authorization URL.',
|
|
267
|
-
502,
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
271
|
-
const trusted =
|
|
272
|
-
hostname === 'framer.com'
|
|
273
|
-
|| hostname.endsWith('.framer.com')
|
|
274
|
-
|| hostname === 'framerlocal.com'
|
|
275
|
-
|| hostname.endsWith('.framerlocal.com');
|
|
276
|
-
if (
|
|
277
|
-
parsed.protocol !== 'https:'
|
|
278
|
-
|| parsed.username
|
|
279
|
-
|| parsed.password
|
|
280
|
-
|| !trusted
|
|
281
|
-
) {
|
|
282
|
-
throw toolError(
|
|
283
|
-
'FRAMER_AGENT_AUTHORIZATION_URL_INVALID',
|
|
284
|
-
'Dexter blocked an untrusted Framer authorization URL.',
|
|
285
|
-
502,
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
return parsed.toString();
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export function openFramerAuthorizationUrl(
|
|
292
|
-
value,
|
|
293
|
-
{
|
|
294
|
-
platform = process.platform,
|
|
295
|
-
spawnImpl = spawn,
|
|
296
|
-
} = {},
|
|
297
|
-
) {
|
|
298
|
-
const url = authorizationUrl(value);
|
|
299
|
-
const command =
|
|
300
|
-
platform === 'darwin'
|
|
301
|
-
? '/usr/bin/open'
|
|
302
|
-
: platform === 'win32'
|
|
303
|
-
? 'rundll32.exe'
|
|
304
|
-
: 'xdg-open';
|
|
305
|
-
const args =
|
|
306
|
-
platform === 'win32'
|
|
307
|
-
? ['url.dll,FileProtocolHandler', url]
|
|
308
|
-
: [url];
|
|
309
|
-
return new Promise((resolve, reject) => {
|
|
310
|
-
const child = spawnImpl(command, args, {
|
|
311
|
-
stdio: 'ignore',
|
|
312
|
-
detached: true,
|
|
313
|
-
windowsHide: true,
|
|
314
|
-
});
|
|
315
|
-
child.once?.('error', (error) => reject(error));
|
|
316
|
-
child.once?.('spawn', () => {
|
|
317
|
-
child.unref?.();
|
|
318
|
-
resolve(true);
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
|
|
323
831
|
function parseStructuredOutput(value) {
|
|
324
832
|
const text = String(value || '').trim();
|
|
325
833
|
if (!text) return null;
|
|
@@ -470,8 +978,7 @@ function sessionIdFromOutput(value) {
|
|
|
470
978
|
function isAuthorizationFailure(error) {
|
|
471
979
|
return (
|
|
472
980
|
error?.code === 'FRAMER_AGENT_AUTHORIZATION_REQUIRED'
|
|
473
|
-
||
|
|
474
|
-
|| /authoriz|log in|login|api key|project not found|session expired/i.test(
|
|
981
|
+
|| /authoriz|log in|login|api key|does not have access|session expired/i.test(
|
|
475
982
|
String(error?.message || ''),
|
|
476
983
|
)
|
|
477
984
|
);
|
|
@@ -481,7 +988,14 @@ function projectConnectionError(error) {
|
|
|
481
988
|
if (/project not found/i.test(String(error?.message || ''))) {
|
|
482
989
|
return toolError(
|
|
483
990
|
'FRAMER_AGENT_PROJECT_NOT_FOUND',
|
|
484
|
-
'
|
|
991
|
+
'The project key passed Server API verification, but Framer Agent could not find the authoritative project ID. Reopen the project in Framer, reconnect it in Dexter, and try again.',
|
|
992
|
+
409,
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
if (isAuthorizationFailure(error)) {
|
|
996
|
+
return toolError(
|
|
997
|
+
'FRAMER_AGENT_AUTHORIZATION_REQUIRED',
|
|
998
|
+
'The project key passed Server API verification, but Framer Agent rejected it while creating the editing session. Reconnect the open project in Dexter with a current key from General → API Keys, then try again.',
|
|
485
999
|
409,
|
|
486
1000
|
);
|
|
487
1001
|
}
|
|
@@ -496,20 +1010,48 @@ export function createFramerAgentToolRuntime({
|
|
|
496
1010
|
onActivity,
|
|
497
1011
|
runCli = runFramerAgentCli,
|
|
498
1012
|
authorizeProject,
|
|
499
|
-
|
|
500
|
-
authorizationTimeoutMs = AUTHORIZATION_TIMEOUT_MS,
|
|
1013
|
+
verifyProjectAuthorization,
|
|
501
1014
|
} = {}) {
|
|
502
1015
|
const projectId = normalizeProjectId(assignment?.context?.framerProjectId);
|
|
1016
|
+
const projectUrl = normalizeProjectUrl(
|
|
1017
|
+
assignment?.context?.framerProjectUrl,
|
|
1018
|
+
projectId,
|
|
1019
|
+
);
|
|
1020
|
+
const sessionCacheKey = projectId;
|
|
503
1021
|
const requestedTools = Array.isArray(assignment?.toolProtocol?.tools)
|
|
504
1022
|
? assignment.toolProtocol.tools
|
|
505
1023
|
: FRAMER_AGENT_TOOL_DEFINITIONS.map((tool) => tool.name);
|
|
506
1024
|
const allowedToolNames = new Set(
|
|
507
1025
|
requestedTools.filter((name) => TOOL_BY_NAME.has(name)),
|
|
508
1026
|
);
|
|
509
|
-
const
|
|
510
|
-
|
|
1027
|
+
const workingPreferences = workingPreferencesFromAssignment(assignment);
|
|
1028
|
+
const definitions = toolDefinitionsForPreferences(
|
|
1029
|
+
allowedToolNames,
|
|
1030
|
+
workingPreferences,
|
|
511
1031
|
);
|
|
1032
|
+
const exposedToolNames = new Set(
|
|
1033
|
+
definitions.map((definition) => definition.name),
|
|
1034
|
+
);
|
|
1035
|
+
const missingToolNames = uniqueStrings(requestedTools)
|
|
1036
|
+
.filter((name) => !exposedToolNames.has(name));
|
|
1037
|
+
if (missingToolNames.length > 0) {
|
|
1038
|
+
throw toolError(
|
|
1039
|
+
'FRAMER_HARNESS_TOOL_CATALOG_INCOMPLETE',
|
|
1040
|
+
`Dexter could not start because this bridge does not expose the required Framer tools: ${missingToolNames.join(', ')}.`,
|
|
1041
|
+
500,
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
512
1044
|
const allowPublishing = assignment?.context?.permissions?.publish === true;
|
|
1045
|
+
const inheritedRejectedMechanisms =
|
|
1046
|
+
assignment?.context?.repairContext?.rejectedMechanisms
|
|
1047
|
+
|| assignment?.repairContext?.rejectedMechanisms
|
|
1048
|
+
|| [];
|
|
1049
|
+
const authorizedInstanceContainerIds = uniqueStrings(
|
|
1050
|
+
assignment?.context?.permissions?.scopedComponentArtifacts
|
|
1051
|
+
?.instanceContainerNodeIds
|
|
1052
|
+
|| assignment?.context?.contextGrant?.writableNodeIds
|
|
1053
|
+
|| [],
|
|
1054
|
+
);
|
|
513
1055
|
const activePagePath = normalizePagePath(
|
|
514
1056
|
assignment?.context?.activePagePath,
|
|
515
1057
|
);
|
|
@@ -523,9 +1065,53 @@ export function createFramerAgentToolRuntime({
|
|
|
523
1065
|
let closed = false;
|
|
524
1066
|
let serial = Promise.resolve();
|
|
525
1067
|
let mutationCalls = 0;
|
|
1068
|
+
let applyChangesCalls = 0;
|
|
1069
|
+
let lowLevelWriteCalls = 0;
|
|
1070
|
+
let imageSearchCalls = 0;
|
|
526
1071
|
let readCalls = 0;
|
|
1072
|
+
let progressCalls = 0;
|
|
1073
|
+
let toolCallCount = 0;
|
|
1074
|
+
const toolCalls = [];
|
|
1075
|
+
let repeatedFailureFingerprint = null;
|
|
1076
|
+
let repeatedFailureCount = 0;
|
|
1077
|
+
let repeatedFailureStops = 0;
|
|
1078
|
+
let unsafeWriteFailures = 0;
|
|
1079
|
+
let lowLevelWriteFailures = 0;
|
|
1080
|
+
let lowLevelWriteDisabled = false;
|
|
1081
|
+
let lowLevelWriteBlockedCalls = 0;
|
|
1082
|
+
let instructionFallbacks = 0;
|
|
1083
|
+
let instructionLookupUnavailable = false;
|
|
527
1084
|
let branchChange = null;
|
|
528
1085
|
let lastInspectionAt = null;
|
|
1086
|
+
let lastMutationAt = null;
|
|
1087
|
+
let screenshotCalls = 0;
|
|
1088
|
+
let interactionVerificationCalls = 0;
|
|
1089
|
+
let interactionVerification = {
|
|
1090
|
+
completed: false,
|
|
1091
|
+
verificationLevel: 'unverified',
|
|
1092
|
+
checks: [],
|
|
1093
|
+
behaviors: [],
|
|
1094
|
+
verifiedAt: null,
|
|
1095
|
+
};
|
|
1096
|
+
const representativeVerifiedBehaviorIds = new Set();
|
|
1097
|
+
const repeatedBehaviorVerificationFailures = new Map();
|
|
1098
|
+
const mutationReceipts = [];
|
|
1099
|
+
let screenshotBeforeMutation = false;
|
|
1100
|
+
let lastScreenshotSequence = 0;
|
|
1101
|
+
let taskPlan = null;
|
|
1102
|
+
const verificationEvidence = new Map();
|
|
1103
|
+
let guidanceMetadata = {
|
|
1104
|
+
loaded: false,
|
|
1105
|
+
contentHash: null,
|
|
1106
|
+
projectContextHash: null,
|
|
1107
|
+
files: [],
|
|
1108
|
+
bytes: 0,
|
|
1109
|
+
};
|
|
1110
|
+
let promptMetadata = {
|
|
1111
|
+
doctrineLoaded: false,
|
|
1112
|
+
doctrineVersion: null,
|
|
1113
|
+
};
|
|
1114
|
+
const framerAgentVersion = installedFramerAgentVersion();
|
|
529
1115
|
let operationSequence = 0;
|
|
530
1116
|
let lastMutationSequence = 0;
|
|
531
1117
|
let lastInspectionSequence = 0;
|
|
@@ -553,6 +1139,7 @@ export function createFramerAgentToolRuntime({
|
|
|
553
1139
|
|
|
554
1140
|
const ensureHostedSetup = async () => {
|
|
555
1141
|
if (env.DEXTER_HOSTED_FRAMER_AUTO_SETUP !== 'true') return;
|
|
1142
|
+
if (hasInstalledFramerSkill(env)) return;
|
|
556
1143
|
if (!hostedSetupPromise) {
|
|
557
1144
|
reportActivity({
|
|
558
1145
|
kind: 'status_update',
|
|
@@ -578,7 +1165,7 @@ export function createFramerAgentToolRuntime({
|
|
|
578
1165
|
);
|
|
579
1166
|
};
|
|
580
1167
|
|
|
581
|
-
const
|
|
1168
|
+
const resolveProjectAuthorization = async () => {
|
|
582
1169
|
if (typeof authorizeProject !== 'function') {
|
|
583
1170
|
throw toolError(
|
|
584
1171
|
'FRAMER_AGENT_AUTHORIZATION_REQUIRED',
|
|
@@ -589,7 +1176,7 @@ export function createFramerAgentToolRuntime({
|
|
|
589
1176
|
let authorization = await authorizeProject({
|
|
590
1177
|
projectId,
|
|
591
1178
|
initiate: true,
|
|
592
|
-
forceRefresh,
|
|
1179
|
+
forceRefresh: false,
|
|
593
1180
|
signal: abortController.signal,
|
|
594
1181
|
});
|
|
595
1182
|
if (authorization?.framerProjectId !== projectId) {
|
|
@@ -600,52 +1187,17 @@ export function createFramerAgentToolRuntime({
|
|
|
600
1187
|
);
|
|
601
1188
|
}
|
|
602
1189
|
if (authorization?.status === 'authorization_required') {
|
|
603
|
-
const url = authorizationUrl(authorization.authUrl);
|
|
604
1190
|
reportActivity({
|
|
605
1191
|
kind: 'status_update',
|
|
606
1192
|
source: 'framer-agent',
|
|
607
1193
|
message:
|
|
608
|
-
'
|
|
1194
|
+
'This Framer project needs a project API key before Dexter can continue.',
|
|
609
1195
|
});
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
'FRAMER_AGENT_AUTHORIZATION_OPEN_FAILED',
|
|
615
|
-
'Dexter could not open Framer authorization in the browser. Reconnect this project from Dexter settings.',
|
|
616
|
-
409,
|
|
617
|
-
);
|
|
618
|
-
}
|
|
619
|
-
const configuredExpiry = new Date(
|
|
620
|
-
String(authorization.expiresAt || ''),
|
|
621
|
-
).getTime();
|
|
622
|
-
const deadline = Math.min(
|
|
623
|
-
Number.isFinite(configuredExpiry)
|
|
624
|
-
? configuredExpiry
|
|
625
|
-
: Date.now() + authorizationTimeoutMs,
|
|
626
|
-
Date.now() + authorizationTimeoutMs,
|
|
1196
|
+
throw toolError(
|
|
1197
|
+
'FRAMER_AGENT_AUTHORIZATION_REQUIRED',
|
|
1198
|
+
'Connect this Framer project in Dexter by adding a project API key, then retry.',
|
|
1199
|
+
409,
|
|
627
1200
|
);
|
|
628
|
-
while (
|
|
629
|
-
authorization?.status === 'authorization_required'
|
|
630
|
-
&& Date.now() < deadline
|
|
631
|
-
) {
|
|
632
|
-
await wait(
|
|
633
|
-
Math.max(
|
|
634
|
-
500,
|
|
635
|
-
Math.min(
|
|
636
|
-
5_000,
|
|
637
|
-
Number(authorization.pollIntervalMs) || AUTHORIZATION_POLL_MS,
|
|
638
|
-
),
|
|
639
|
-
),
|
|
640
|
-
abortController.signal,
|
|
641
|
-
);
|
|
642
|
-
authorization = await authorizeProject({
|
|
643
|
-
projectId,
|
|
644
|
-
initiate: false,
|
|
645
|
-
forceRefresh: false,
|
|
646
|
-
signal: abortController.signal,
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
1201
|
}
|
|
650
1202
|
const apiKey = String(authorization?.apiKey || '').trim();
|
|
651
1203
|
if (authorization?.status !== 'ready' || !apiKey) {
|
|
@@ -655,13 +1207,21 @@ export function createFramerAgentToolRuntime({
|
|
|
655
1207
|
409,
|
|
656
1208
|
);
|
|
657
1209
|
}
|
|
658
|
-
|
|
1210
|
+
const authorizedProjectUrl = normalizeProjectUrl(
|
|
1211
|
+
authorization.framerProjectUrl,
|
|
1212
|
+
projectId,
|
|
1213
|
+
);
|
|
1214
|
+
return { ...authorization, apiKey, framerProjectUrl: authorizedProjectUrl };
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
const installProjectAuthorization = async (authorization) => {
|
|
1218
|
+
await executeCli(['project', 'auth', projectId, authorization.apiKey], {
|
|
659
1219
|
timeoutMs: 30_000,
|
|
660
1220
|
});
|
|
661
1221
|
reportActivity({
|
|
662
1222
|
kind: 'status_update',
|
|
663
1223
|
source: 'framer-agent',
|
|
664
|
-
message: 'This Framer project is authorized for the
|
|
1224
|
+
message: 'This Framer project is authorized for the coding harness.',
|
|
665
1225
|
});
|
|
666
1226
|
};
|
|
667
1227
|
|
|
@@ -671,14 +1231,55 @@ export function createFramerAgentToolRuntime({
|
|
|
671
1231
|
});
|
|
672
1232
|
};
|
|
673
1233
|
|
|
1234
|
+
const reportRejectedAuthorization = async (authorization, error) => {
|
|
1235
|
+
if (
|
|
1236
|
+
typeof verifyProjectAuthorization !== 'function'
|
|
1237
|
+
|| !authorization?.credentialVersion
|
|
1238
|
+
) return;
|
|
1239
|
+
await verifyProjectAuthorization({
|
|
1240
|
+
status: 'rejected',
|
|
1241
|
+
framerProjectId: projectId,
|
|
1242
|
+
framerProjectUrl: projectUrl,
|
|
1243
|
+
credentialVersion: authorization.credentialVersion,
|
|
1244
|
+
errorCode: String(error?.code || 'FRAMER_AGENT_AUTHORIZATION_REJECTED')
|
|
1245
|
+
.slice(0, 120),
|
|
1246
|
+
signal: abortController.signal,
|
|
1247
|
+
}).catch(() => undefined);
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
const verifyConnectedProject = async (activeSessionId, authorization) => {
|
|
1251
|
+
const result = await executeCli(['exec', '-s', activeSessionId], {
|
|
1252
|
+
input: [
|
|
1253
|
+
'const [project, branch] = await Promise.all([',
|
|
1254
|
+
' framer.getProjectInfo(),',
|
|
1255
|
+
' framer.agent.getActiveBranch(),',
|
|
1256
|
+
']);',
|
|
1257
|
+
'console.log(JSON.stringify({ projectName: project?.name ?? null, projectUrl: branch?.url ?? null }));',
|
|
1258
|
+
].join('\n'),
|
|
1259
|
+
timeoutMs: 30_000,
|
|
1260
|
+
});
|
|
1261
|
+
const observed = parseStructuredOutput(result.stdout);
|
|
1262
|
+
const observedProjectUrl = normalizeProjectUrl(
|
|
1263
|
+
observed?.projectUrl,
|
|
1264
|
+
projectId,
|
|
1265
|
+
);
|
|
1266
|
+
if (
|
|
1267
|
+
typeof verifyProjectAuthorization === 'function'
|
|
1268
|
+
&& authorization?.credentialVersion
|
|
1269
|
+
) {
|
|
1270
|
+
await verifyProjectAuthorization({
|
|
1271
|
+
status: 'verified',
|
|
1272
|
+
framerProjectId: projectId,
|
|
1273
|
+
framerProjectUrl: observedProjectUrl,
|
|
1274
|
+
credentialVersion: authorization.credentialVersion,
|
|
1275
|
+
signal: abortController.signal,
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
|
|
674
1280
|
const ensureSession = async () => {
|
|
675
1281
|
await ensureHostedSetup();
|
|
676
1282
|
if (sessionId) return sessionId;
|
|
677
|
-
const cached = sessionsByProject.get(projectId);
|
|
678
|
-
if (cached) {
|
|
679
|
-
sessionId = cached;
|
|
680
|
-
return sessionId;
|
|
681
|
-
}
|
|
682
1283
|
if (sessionPromise) return sessionPromise;
|
|
683
1284
|
sessionPromise = (async () => {
|
|
684
1285
|
reportActivity({
|
|
@@ -686,23 +1287,49 @@ export function createFramerAgentToolRuntime({
|
|
|
686
1287
|
source: 'framer-agent',
|
|
687
1288
|
message: 'Connecting the coding harness to this Framer project.',
|
|
688
1289
|
});
|
|
689
|
-
|
|
690
|
-
|
|
1290
|
+
let authorization = null;
|
|
1291
|
+
if (typeof authorizeProject === 'function') {
|
|
1292
|
+
authorization = await resolveProjectAuthorization();
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const cached = sessionsByProject.get(sessionCacheKey);
|
|
1296
|
+
if (
|
|
1297
|
+
cached
|
|
1298
|
+
&& (
|
|
1299
|
+
typeof authorizeProject !== 'function'
|
|
1300
|
+
|| (
|
|
1301
|
+
authorization?.verificationStatus === 'verified'
|
|
1302
|
+
&& cached.credentialVersion === authorization.credentialVersion
|
|
1303
|
+
)
|
|
1304
|
+
)
|
|
1305
|
+
) {
|
|
1306
|
+
sessionId = cached.sessionId;
|
|
1307
|
+
return sessionId;
|
|
1308
|
+
}
|
|
1309
|
+
if (cached) {
|
|
1310
|
+
await executeCli(['session', 'destroy', cached.sessionId], {
|
|
1311
|
+
timeoutMs: 10_000,
|
|
1312
|
+
}).catch(() => undefined);
|
|
1313
|
+
sessionsByProject.delete(sessionCacheKey);
|
|
1314
|
+
}
|
|
1315
|
+
if (!authorization && !(await projectIsAuthorizedLocally())) {
|
|
1316
|
+
throw toolError(
|
|
1317
|
+
'FRAMER_AGENT_AUTHORIZATION_REQUIRED',
|
|
1318
|
+
'This Framer project needs to be connected before Dexter can use it.',
|
|
1319
|
+
409,
|
|
1320
|
+
);
|
|
691
1321
|
}
|
|
692
1322
|
let created;
|
|
693
1323
|
try {
|
|
1324
|
+
if (authorization) {
|
|
1325
|
+
await installProjectAuthorization(authorization);
|
|
1326
|
+
}
|
|
694
1327
|
created = await createSession();
|
|
695
1328
|
} catch (error) {
|
|
696
1329
|
if (isAuthorizationFailure(error)) {
|
|
697
|
-
await
|
|
698
|
-
try {
|
|
699
|
-
created = await createSession();
|
|
700
|
-
} catch (retryError) {
|
|
701
|
-
throw projectConnectionError(retryError);
|
|
702
|
-
}
|
|
703
|
-
} else {
|
|
704
|
-
throw projectConnectionError(error);
|
|
1330
|
+
await reportRejectedAuthorization(authorization, error);
|
|
705
1331
|
}
|
|
1332
|
+
throw projectConnectionError(error);
|
|
706
1333
|
}
|
|
707
1334
|
sessionId = sessionIdFromOutput(created.stdout);
|
|
708
1335
|
if (!sessionId) {
|
|
@@ -721,7 +1348,25 @@ export function createFramerAgentToolRuntime({
|
|
|
721
1348
|
503,
|
|
722
1349
|
);
|
|
723
1350
|
}
|
|
724
|
-
|
|
1351
|
+
try {
|
|
1352
|
+
await verifyConnectedProject(sessionId, authorization);
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
await executeCli(['session', 'destroy', sessionId], {
|
|
1355
|
+
timeoutMs: 10_000,
|
|
1356
|
+
}).catch(() => undefined);
|
|
1357
|
+
sessionId = null;
|
|
1358
|
+
if (
|
|
1359
|
+
isAuthorizationFailure(error)
|
|
1360
|
+
|| error?.code === 'FRAMER_AGENT_PROJECT_MISMATCH'
|
|
1361
|
+
) {
|
|
1362
|
+
await reportRejectedAuthorization(authorization, error);
|
|
1363
|
+
}
|
|
1364
|
+
throw projectConnectionError(error);
|
|
1365
|
+
}
|
|
1366
|
+
sessionsByProject.set(sessionCacheKey, {
|
|
1367
|
+
sessionId,
|
|
1368
|
+
credentialVersion: authorization?.credentialVersion || null,
|
|
1369
|
+
});
|
|
725
1370
|
reportActivity({
|
|
726
1371
|
kind: 'status_update',
|
|
727
1372
|
source: 'framer-agent',
|
|
@@ -734,6 +1379,86 @@ export function createFramerAgentToolRuntime({
|
|
|
734
1379
|
return sessionPromise;
|
|
735
1380
|
};
|
|
736
1381
|
|
|
1382
|
+
const loadProjectGuidance = async () => {
|
|
1383
|
+
await ensureSession();
|
|
1384
|
+
const skillRoot = framerSkillRoot(env);
|
|
1385
|
+
if (!skillRoot) return { content: '', ...guidanceMetadata };
|
|
1386
|
+
const projectRoot = path.join(skillRoot, 'projects', projectId);
|
|
1387
|
+
if (!fs.existsSync(projectRoot)) {
|
|
1388
|
+
return { content: '', ...guidanceMetadata };
|
|
1389
|
+
}
|
|
1390
|
+
const requestedFiles = [
|
|
1391
|
+
{ root: skillRoot, path: 'SKILL.md', label: 'SKILL.md' },
|
|
1392
|
+
{ root: projectRoot, path: 'index.md', label: 'project/index.md' },
|
|
1393
|
+
{
|
|
1394
|
+
root: projectRoot,
|
|
1395
|
+
path: 'project-inventory.md',
|
|
1396
|
+
label: 'project/project-inventory.md',
|
|
1397
|
+
},
|
|
1398
|
+
{ root: projectRoot, path: 'prompt/core-principles.md' },
|
|
1399
|
+
{ root: projectRoot, path: 'prompt/core-examples.md' },
|
|
1400
|
+
{ root: projectRoot, path: 'prompt/implementation-strategy.md' },
|
|
1401
|
+
{ root: projectRoot, path: 'prompt/updating-the-project.md' },
|
|
1402
|
+
{ root: projectRoot, path: 'prompt/tools.md' },
|
|
1403
|
+
{ root: projectRoot, path: 'prompt/overview.md' },
|
|
1404
|
+
{ root: projectRoot, path: 'prompt/guardrails.md' },
|
|
1405
|
+
{ root: projectRoot, path: 'prompt/critical-reminders.md' },
|
|
1406
|
+
...(visualVerificationRequired
|
|
1407
|
+
? [
|
|
1408
|
+
{ root: projectRoot, path: 'prompt/design-rules.md' },
|
|
1409
|
+
{ root: projectRoot, path: 'prompt/how-projects-work.md' },
|
|
1410
|
+
]
|
|
1411
|
+
: []),
|
|
1412
|
+
];
|
|
1413
|
+
const files = [];
|
|
1414
|
+
let remainingBytes = MAX_GUIDANCE_BYTES;
|
|
1415
|
+
for (const requestedFile of requestedFiles) {
|
|
1416
|
+
const loaded = readGuidanceFile(
|
|
1417
|
+
requestedFile.root,
|
|
1418
|
+
requestedFile.path,
|
|
1419
|
+
remainingBytes,
|
|
1420
|
+
);
|
|
1421
|
+
if (!loaded) continue;
|
|
1422
|
+
files.push({
|
|
1423
|
+
...loaded,
|
|
1424
|
+
path: requestedFile.label || requestedFile.path,
|
|
1425
|
+
});
|
|
1426
|
+
remainingBytes -= Buffer.byteLength(loaded.content, 'utf8');
|
|
1427
|
+
if (remainingBytes <= 0) break;
|
|
1428
|
+
}
|
|
1429
|
+
const content = files
|
|
1430
|
+
.map((file) => `## ${file.path}\n\n${file.content}`)
|
|
1431
|
+
.join('\n\n');
|
|
1432
|
+
const inventory = files.find(
|
|
1433
|
+
(file) => file.path.endsWith('project-inventory.md'),
|
|
1434
|
+
)?.content || '';
|
|
1435
|
+
guidanceMetadata = {
|
|
1436
|
+
loaded: Boolean(content),
|
|
1437
|
+
contentHash: content
|
|
1438
|
+
? crypto.createHash('sha256').update(content).digest('hex')
|
|
1439
|
+
: null,
|
|
1440
|
+
projectContextHash: inventory
|
|
1441
|
+
? crypto.createHash('sha256').update(inventory).digest('hex')
|
|
1442
|
+
: null,
|
|
1443
|
+
files: files.map((file) => file.path),
|
|
1444
|
+
bytes: Buffer.byteLength(content, 'utf8'),
|
|
1445
|
+
};
|
|
1446
|
+
return { content, ...guidanceMetadata };
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1449
|
+
const configurePromptContext = ({
|
|
1450
|
+
doctrineLoaded = false,
|
|
1451
|
+
doctrineVersion = null,
|
|
1452
|
+
} = {}) => {
|
|
1453
|
+
promptMetadata = {
|
|
1454
|
+
doctrineLoaded: doctrineLoaded === true,
|
|
1455
|
+
doctrineVersion:
|
|
1456
|
+
typeof doctrineVersion === 'string' && doctrineVersion.trim()
|
|
1457
|
+
? doctrineVersion.trim().slice(0, 160)
|
|
1458
|
+
: null,
|
|
1459
|
+
};
|
|
1460
|
+
};
|
|
1461
|
+
|
|
737
1462
|
const execCode = async (code, { timeoutMs } = {}) => {
|
|
738
1463
|
let activeSessionId = await ensureSession();
|
|
739
1464
|
try {
|
|
@@ -750,7 +1475,7 @@ export function createFramerAgentToolRuntime({
|
|
|
750
1475
|
) {
|
|
751
1476
|
throw error;
|
|
752
1477
|
}
|
|
753
|
-
sessionsByProject.delete(
|
|
1478
|
+
sessionsByProject.delete(sessionCacheKey);
|
|
754
1479
|
sessionId = null;
|
|
755
1480
|
activeSessionId = await ensureSession();
|
|
756
1481
|
return executeCli(['exec', '-s', activeSessionId], {
|
|
@@ -816,7 +1541,9 @@ export function createFramerAgentToolRuntime({
|
|
|
816
1541
|
const normalizedResult = (result) => {
|
|
817
1542
|
const parsed = parseStructuredOutput(result.stdout);
|
|
818
1543
|
return {
|
|
819
|
-
...(parsed === null
|
|
1544
|
+
...(parsed === null
|
|
1545
|
+
? { output: boundedText(result.stdout, MAX_MODEL_RESULT_BYTES) }
|
|
1546
|
+
: { result: boundedStructuredValue(parsed) }),
|
|
820
1547
|
...(result.stderr.trim()
|
|
821
1548
|
? { diagnostics: boundedText(result.stderr, 20_000) }
|
|
822
1549
|
: {}),
|
|
@@ -824,19 +1551,315 @@ export function createFramerAgentToolRuntime({
|
|
|
824
1551
|
};
|
|
825
1552
|
};
|
|
826
1553
|
|
|
827
|
-
const markInspection = () => {
|
|
1554
|
+
const markInspection = ({ visual = false, verifies = [] } = {}) => {
|
|
828
1555
|
operationSequence += 1;
|
|
829
1556
|
lastInspectionSequence = operationSequence;
|
|
830
1557
|
lastInspectionAt = new Date().toISOString();
|
|
1558
|
+
verificationEvidence.set('structural', operationSequence);
|
|
1559
|
+
if (visual) {
|
|
1560
|
+
screenshotCalls += 1;
|
|
1561
|
+
lastScreenshotSequence = operationSequence;
|
|
1562
|
+
verificationEvidence.set('visual', operationSequence);
|
|
1563
|
+
if (lastMutationSequence === 0) screenshotBeforeMutation = true;
|
|
1564
|
+
}
|
|
1565
|
+
for (const check of normalizeVerificationValues(
|
|
1566
|
+
verifies,
|
|
1567
|
+
GENERIC_VERIFICATION_CHECKS,
|
|
1568
|
+
)) {
|
|
1569
|
+
verificationEvidence.set(check, operationSequence);
|
|
1570
|
+
}
|
|
831
1571
|
};
|
|
832
1572
|
|
|
833
1573
|
const markMutation = () => {
|
|
834
1574
|
operationSequence += 1;
|
|
835
1575
|
lastMutationSequence = operationSequence;
|
|
1576
|
+
lastMutationAt = new Date().toISOString();
|
|
836
1577
|
};
|
|
837
1578
|
|
|
1579
|
+
const requiredVerificationChecks = () =>
|
|
1580
|
+
taskPlan?.verification || [];
|
|
1581
|
+
const taskPlanSnapshot = () => {
|
|
1582
|
+
if (!taskPlan) return null;
|
|
1583
|
+
return {
|
|
1584
|
+
summary: taskPlan.summary,
|
|
1585
|
+
domains: [...taskPlan.domains],
|
|
1586
|
+
verification: [...taskPlan.verification],
|
|
1587
|
+
...(taskPlan.behaviors.length > 0
|
|
1588
|
+
? {
|
|
1589
|
+
behaviors: taskPlan.behaviors.map((behavior) => ({
|
|
1590
|
+
...behavior,
|
|
1591
|
+
expectedContent: [...behavior.expectedContent],
|
|
1592
|
+
})),
|
|
1593
|
+
mechanisms: taskPlan.mechanisms.map((decision) => ({
|
|
1594
|
+
...decision,
|
|
1595
|
+
})),
|
|
1596
|
+
rejectedMechanisms: taskPlan.rejectedMechanisms.map((entry) => ({
|
|
1597
|
+
...entry,
|
|
1598
|
+
})),
|
|
1599
|
+
compatibility: taskPlan.compatibility.map((entry) => ({
|
|
1600
|
+
...entry,
|
|
1601
|
+
requiredCapabilities: [...entry.requiredCapabilities],
|
|
1602
|
+
})),
|
|
1603
|
+
}
|
|
1604
|
+
: {}),
|
|
1605
|
+
};
|
|
1606
|
+
};
|
|
838
1607
|
const synchronized = () =>
|
|
839
1608
|
mutationCalls === 0 || lastInspectionSequence > lastMutationSequence;
|
|
1609
|
+
const verificationCheckCompleted = (check) => {
|
|
1610
|
+
if (mutationCalls === 0) return true;
|
|
1611
|
+
if (check === 'structural') return synchronized();
|
|
1612
|
+
if (check === 'visual') {
|
|
1613
|
+
return (
|
|
1614
|
+
screenshotBeforeMutation
|
|
1615
|
+
&& lastScreenshotSequence > lastMutationSequence
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
return Number(verificationEvidence.get(check) || 0) > lastMutationSequence;
|
|
1619
|
+
};
|
|
1620
|
+
const visualVerificationRequired = () =>
|
|
1621
|
+
applyChangesCalls > 0
|
|
1622
|
+
|| requiredVerificationChecks().includes('visual');
|
|
1623
|
+
const visualVerificationCompleted = () =>
|
|
1624
|
+
!visualVerificationRequired()
|
|
1625
|
+
|| mutationCalls === 0
|
|
1626
|
+
|| (
|
|
1627
|
+
screenshotBeforeMutation
|
|
1628
|
+
&& lastScreenshotSequence > lastMutationSequence
|
|
1629
|
+
);
|
|
1630
|
+
const missingVerificationChecks = () => {
|
|
1631
|
+
if (mutationCalls === 0) return [];
|
|
1632
|
+
const missing = [];
|
|
1633
|
+
if (!taskPlan) missing.push('task-plan');
|
|
1634
|
+
const required = new Set([
|
|
1635
|
+
'structural',
|
|
1636
|
+
...requiredVerificationChecks(),
|
|
1637
|
+
...(applyChangesCalls > 0 ? ['visual'] : []),
|
|
1638
|
+
]);
|
|
1639
|
+
for (const check of required) {
|
|
1640
|
+
if (!verificationCheckCompleted(check)) missing.push(check);
|
|
1641
|
+
}
|
|
1642
|
+
return missing;
|
|
1643
|
+
};
|
|
1644
|
+
const verification = () => ({
|
|
1645
|
+
ownedBy: 'harness',
|
|
1646
|
+
required: mutationCalls > 0,
|
|
1647
|
+
completed: missingVerificationChecks().length === 0,
|
|
1648
|
+
structuralCompleted: synchronized(),
|
|
1649
|
+
visualRequired: visualVerificationRequired() && mutationCalls > 0,
|
|
1650
|
+
visualCompleted: visualVerificationCompleted(),
|
|
1651
|
+
screenshotCalls,
|
|
1652
|
+
...(requiredVerificationChecks().includes('interactions')
|
|
1653
|
+
|| interactionVerificationCalls > 0
|
|
1654
|
+
? {
|
|
1655
|
+
interactionVerificationCalls,
|
|
1656
|
+
interactionVerification: {
|
|
1657
|
+
completed: verificationCheckCompleted('interactions'),
|
|
1658
|
+
verificationLevel:
|
|
1659
|
+
verificationCheckCompleted('interactions')
|
|
1660
|
+
? interactionVerification.verificationLevel
|
|
1661
|
+
: 'unverified',
|
|
1662
|
+
checks: interactionVerification.checks.map((check) => ({
|
|
1663
|
+
...check,
|
|
1664
|
+
failures: [...check.failures],
|
|
1665
|
+
})),
|
|
1666
|
+
behaviors: interactionVerification.behaviors.map((behavior) => ({
|
|
1667
|
+
...behavior,
|
|
1668
|
+
failures: [...behavior.failures],
|
|
1669
|
+
})),
|
|
1670
|
+
verifiedAt: interactionVerification.verifiedAt,
|
|
1671
|
+
},
|
|
1672
|
+
}
|
|
1673
|
+
: {}),
|
|
1674
|
+
taskPlan: taskPlanSnapshot(),
|
|
1675
|
+
requiredChecks: [
|
|
1676
|
+
...new Set([
|
|
1677
|
+
'structural',
|
|
1678
|
+
...requiredVerificationChecks(),
|
|
1679
|
+
...(applyChangesCalls > 0 ? ['visual'] : []),
|
|
1680
|
+
]),
|
|
1681
|
+
],
|
|
1682
|
+
completedChecks: [
|
|
1683
|
+
...new Set([
|
|
1684
|
+
'structural',
|
|
1685
|
+
...requiredVerificationChecks(),
|
|
1686
|
+
...(applyChangesCalls > 0 ? ['visual'] : []),
|
|
1687
|
+
]),
|
|
1688
|
+
].filter(verificationCheckCompleted),
|
|
1689
|
+
missingChecks: missingVerificationChecks(),
|
|
1690
|
+
lastMutationAt,
|
|
1691
|
+
lastInspectionAt,
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
const inspectFinalState = async () => {
|
|
1695
|
+
await serial;
|
|
1696
|
+
return {
|
|
1697
|
+
synchronized: synchronized(),
|
|
1698
|
+
sourceHash: null,
|
|
1699
|
+
changed: mutationCalls > 0,
|
|
1700
|
+
mutationCalls,
|
|
1701
|
+
readCalls,
|
|
1702
|
+
branch: branchChange,
|
|
1703
|
+
lastInspectionAt,
|
|
1704
|
+
lastMutationAt,
|
|
1705
|
+
taskPlan: taskPlanSnapshot(),
|
|
1706
|
+
verification: verification(),
|
|
1707
|
+
};
|
|
1708
|
+
};
|
|
1709
|
+
|
|
1710
|
+
const finalizeCompletion = (completion) => {
|
|
1711
|
+
if (
|
|
1712
|
+
completion?.status !== 'completed'
|
|
1713
|
+
|| mutationCalls === 0
|
|
1714
|
+
|| verification().completed
|
|
1715
|
+
) {
|
|
1716
|
+
return completion;
|
|
1717
|
+
}
|
|
1718
|
+
const missing = missingVerificationChecks().map((check) => {
|
|
1719
|
+
if (check === 'task-plan') return 'a harness task and verification plan';
|
|
1720
|
+
if (check === 'structural') return 'a final structural read';
|
|
1721
|
+
if (check === 'visual') return 'before-and-after screenshot verification';
|
|
1722
|
+
return `post-mutation ${check} verification`;
|
|
1723
|
+
});
|
|
1724
|
+
const reason =
|
|
1725
|
+
`The harness ended before completing ${missing.join(' and ')}.`;
|
|
1726
|
+
return {
|
|
1727
|
+
...completion,
|
|
1728
|
+
status: 'failed',
|
|
1729
|
+
summary: `${String(completion?.summary || '').trim()} ${reason}`.trim(),
|
|
1730
|
+
checks: [
|
|
1731
|
+
...(Array.isArray(completion?.checks) ? completion.checks : []),
|
|
1732
|
+
{
|
|
1733
|
+
command: 'Harness-owned final verification',
|
|
1734
|
+
status: 'failed',
|
|
1735
|
+
output: reason,
|
|
1736
|
+
},
|
|
1737
|
+
],
|
|
1738
|
+
blockedReason: null,
|
|
1739
|
+
};
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
const verificationRepairPrompt = () => {
|
|
1743
|
+
const current = verification();
|
|
1744
|
+
if (current.completed || mutationCalls === 0) return null;
|
|
1745
|
+
const missing = [];
|
|
1746
|
+
for (const check of current.missingChecks || []) {
|
|
1747
|
+
if (check === 'task-plan') {
|
|
1748
|
+
missing.push(
|
|
1749
|
+
'declare the task domains and verification plan with framer_plan_task',
|
|
1750
|
+
);
|
|
1751
|
+
} else if (check === 'structural') {
|
|
1752
|
+
missing.push('perform a focused live read after the latest mutation');
|
|
1753
|
+
} else if (check === 'visual') {
|
|
1754
|
+
missing.push(
|
|
1755
|
+
screenshotBeforeMutation
|
|
1756
|
+
? 'capture and inspect the affected canvas after the latest mutation'
|
|
1757
|
+
: 'report that the required baseline screenshot was missed; do not claim visual verification',
|
|
1758
|
+
);
|
|
1759
|
+
} else {
|
|
1760
|
+
missing.push(
|
|
1761
|
+
check === 'interactions'
|
|
1762
|
+
? 'call framer_verify_interactions with canonical evidence node IDs for every declared behavior and chosen mechanism'
|
|
1763
|
+
: `perform a focused read tagged with verifies:["${check}"] after the latest mutation`,
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
return [
|
|
1768
|
+
'Continue the same Framer task from its current project state.',
|
|
1769
|
+
'Do not redo successful work.',
|
|
1770
|
+
`Before returning, ${missing.join(' and ')}.`,
|
|
1771
|
+
'Repair only concrete problems you observe, then return the required JSON outcome.',
|
|
1772
|
+
].join(' ');
|
|
1773
|
+
};
|
|
1774
|
+
|
|
1775
|
+
const builtInInstructions = (documentationUnavailable = false) => ({
|
|
1776
|
+
stdout: JSON.stringify({
|
|
1777
|
+
source: documentationUnavailable
|
|
1778
|
+
? 'built-in-fallback'
|
|
1779
|
+
: 'built-in',
|
|
1780
|
+
guidance: [
|
|
1781
|
+
'Use framer_read with framer.agent.getNode({ id }) or getNodes({ ids }) for node inspection.',
|
|
1782
|
+
'Use framer_read_project for supported project queries such as screenshots.',
|
|
1783
|
+
'Use framer_query_images for approved stock imagery only after checking user attachments and suitable existing project images.',
|
|
1784
|
+
'Use framer_apply_changes for layout and styling. Read its diagnostics, verify the result, and repair concrete issues.',
|
|
1785
|
+
'If framer_write is rejected as unsafe, do not retry JavaScript variants. Switch to framer_apply_changes or report the concrete blocker.',
|
|
1786
|
+
'Batch related work when practical, reuse evidence already collected, and stop when the requested outcome is satisfied.',
|
|
1787
|
+
],
|
|
1788
|
+
documentationUnavailable,
|
|
1789
|
+
}),
|
|
1790
|
+
stderr: '',
|
|
1791
|
+
});
|
|
1792
|
+
|
|
1793
|
+
const reserveToolCall = (name, category) => {
|
|
1794
|
+
toolCallCount += 1;
|
|
1795
|
+
if (category === 'progress') progressCalls += 1;
|
|
1796
|
+
if (category === 'read') readCalls += 1;
|
|
1797
|
+
const call = {
|
|
1798
|
+
sequence: toolCallCount,
|
|
1799
|
+
name,
|
|
1800
|
+
category,
|
|
1801
|
+
status: 'failed',
|
|
1802
|
+
durationMs: 0,
|
|
1803
|
+
};
|
|
1804
|
+
toolCalls.push(call);
|
|
1805
|
+
return call;
|
|
1806
|
+
};
|
|
1807
|
+
|
|
1808
|
+
const callFingerprint = (name, rawArguments) =>
|
|
1809
|
+
crypto
|
|
1810
|
+
.createHash('sha256')
|
|
1811
|
+
.update(`${name}\n${JSON.stringify(rawArguments)}`)
|
|
1812
|
+
.digest('hex');
|
|
1813
|
+
|
|
1814
|
+
const requireTaskPlanForMutation = ({ canvas = false } = {}) => {
|
|
1815
|
+
if (!taskPlan) {
|
|
1816
|
+
throw toolError(
|
|
1817
|
+
'FRAMER_HARNESS_PLAN_REQUIRED',
|
|
1818
|
+
'Inspect the live Framer target, then call framer_plan_task before making a mutation.',
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
if (
|
|
1822
|
+
(canvas || taskPlan.verification.includes('visual'))
|
|
1823
|
+
&& !screenshotBeforeMutation
|
|
1824
|
+
) {
|
|
1825
|
+
throw toolError(
|
|
1826
|
+
'FRAMER_HARNESS_BASELINE_SCREENSHOT_REQUIRED',
|
|
1827
|
+
'Capture and inspect a screenshot of the affected canvas before the first visual mutation.',
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
const rejectedSelectedMechanisms = taskPlan.behaviors.filter((behavior) => {
|
|
1831
|
+
const selected = taskPlan.mechanisms.find((decision) =>
|
|
1832
|
+
decision.behaviorId === behavior.id);
|
|
1833
|
+
return selected && taskPlan.rejectedMechanisms.some((rejected) =>
|
|
1834
|
+
rejected.mechanism === selected.mechanism
|
|
1835
|
+
&& (
|
|
1836
|
+
rejected.behaviorId === behavior.id
|
|
1837
|
+
|| rejected.behaviorSignature === behavior.signature
|
|
1838
|
+
));
|
|
1839
|
+
});
|
|
1840
|
+
if (rejectedSelectedMechanisms.length > 0) {
|
|
1841
|
+
throw toolError(
|
|
1842
|
+
'FRAMER_BEHAVIOR_MECHANISM_REPLAN_REQUIRED',
|
|
1843
|
+
`Choose a different mechanism for ${rejectedSelectedMechanisms.map((behavior) => behavior.id).join(', ')} before making another mutation.`,
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
if (mutationCalls > 0) {
|
|
1847
|
+
const pendingRepresentative = taskPlan.behaviors
|
|
1848
|
+
.filter((behavior) => behavior.expectedTargets > 1)
|
|
1849
|
+
.filter((behavior) =>
|
|
1850
|
+
taskPlan.mechanisms.find((decision) =>
|
|
1851
|
+
decision.behaviorId === behavior.id
|
|
1852
|
+
&& decision.representativeFirst))
|
|
1853
|
+
.filter((behavior) =>
|
|
1854
|
+
!representativeVerifiedBehaviorIds.has(behavior.id));
|
|
1855
|
+
if (pendingRepresentative.length > 0) {
|
|
1856
|
+
throw toolError(
|
|
1857
|
+
'FRAMER_BEHAVIOR_REPRESENTATIVE_VERIFICATION_REQUIRED',
|
|
1858
|
+
`Verify one representative implementation for ${pendingRepresentative.map((behavior) => behavior.id).join(', ')} before batching further mutations.`,
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
};
|
|
840
1863
|
|
|
841
1864
|
async function invokeUnsafe(name, rawArguments = {}) {
|
|
842
1865
|
if (closed) {
|
|
@@ -857,6 +1880,25 @@ export function createFramerAgentToolRuntime({
|
|
|
857
1880
|
'Framer Agent tool arguments must be an object.',
|
|
858
1881
|
);
|
|
859
1882
|
}
|
|
1883
|
+
const mutation =
|
|
1884
|
+
name === 'framer_apply_changes' || name === 'framer_write';
|
|
1885
|
+
const category =
|
|
1886
|
+
name === 'framer_plan_task'
|
|
1887
|
+
? 'plan'
|
|
1888
|
+
: mutation
|
|
1889
|
+
? 'mutation'
|
|
1890
|
+
: 'read';
|
|
1891
|
+
const fingerprint = callFingerprint(name, rawArguments);
|
|
1892
|
+
if (
|
|
1893
|
+
repeatedFailureFingerprint === fingerprint
|
|
1894
|
+
&& repeatedFailureCount >= 5
|
|
1895
|
+
) {
|
|
1896
|
+
repeatedFailureStops += 1;
|
|
1897
|
+
throw toolError(
|
|
1898
|
+
'FRAMER_AGENT_REPEATED_CALL_LOOP',
|
|
1899
|
+
'The same Framer tool call has failed five times in a row. Change the approach or report the concrete blocker.',
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
860
1902
|
if (name === 'progress_update') {
|
|
861
1903
|
const message = String(rawArguments.message || '').trim().slice(0, 500);
|
|
862
1904
|
if (!message) {
|
|
@@ -865,15 +1907,20 @@ export function createFramerAgentToolRuntime({
|
|
|
865
1907
|
'A progress message is required.',
|
|
866
1908
|
);
|
|
867
1909
|
}
|
|
1910
|
+
const toolCall = reserveToolCall(name, 'progress');
|
|
868
1911
|
reportActivity({
|
|
869
1912
|
kind: 'status_update',
|
|
870
1913
|
source: 'harness',
|
|
871
1914
|
message,
|
|
872
1915
|
});
|
|
1916
|
+
repeatedFailureFingerprint = null;
|
|
1917
|
+
repeatedFailureCount = 0;
|
|
1918
|
+
toolCall.status = 'succeeded';
|
|
873
1919
|
return { acknowledged: true };
|
|
874
1920
|
}
|
|
875
1921
|
|
|
876
1922
|
const startedAt = Date.now();
|
|
1923
|
+
const toolCall = reserveToolCall(name, category);
|
|
877
1924
|
reportActivity({
|
|
878
1925
|
kind: 'tool_active',
|
|
879
1926
|
source: 'framer-agent',
|
|
@@ -882,17 +1929,48 @@ export function createFramerAgentToolRuntime({
|
|
|
882
1929
|
message:
|
|
883
1930
|
name === 'framer_apply_changes' || name === 'framer_write'
|
|
884
1931
|
? 'Updating the Framer project'
|
|
1932
|
+
: name === 'framer_plan_task'
|
|
1933
|
+
? 'Planning the Framer work and verification'
|
|
885
1934
|
: 'Inspecting the Framer project',
|
|
886
1935
|
});
|
|
887
1936
|
try {
|
|
888
1937
|
let result;
|
|
889
|
-
if (name === '
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
'
|
|
1938
|
+
if (name === 'framer_write' && lowLevelWriteDisabled) {
|
|
1939
|
+
lowLevelWriteBlockedCalls += 1;
|
|
1940
|
+
throw toolError(
|
|
1941
|
+
'FRAMER_AGENT_WRITE_FALLBACK_REQUIRED',
|
|
1942
|
+
'Low-level JavaScript writes are disabled for this run after repeated safety rejections. Use framer_apply_changes, inspect the project, or report the concrete blocker.',
|
|
893
1943
|
);
|
|
1944
|
+
} else if (name === 'framer_instructions') {
|
|
1945
|
+
const queries = Array.isArray(rawArguments.queries)
|
|
1946
|
+
? uniqueStrings(rawArguments.queries, 8)
|
|
1947
|
+
: [];
|
|
1948
|
+
if (queries.length > 0 && !instructionLookupUnavailable) {
|
|
1949
|
+
try {
|
|
1950
|
+
result = await executeCli(
|
|
1951
|
+
['docs', ...queries],
|
|
1952
|
+
{ timeoutMs: 30_000 },
|
|
1953
|
+
);
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
instructionFallbacks += 1;
|
|
1956
|
+
instructionLookupUnavailable = true;
|
|
1957
|
+
trace?.warn?.('framer_agent_docs_fallback', {
|
|
1958
|
+
code: error?.code,
|
|
1959
|
+
message: error?.message,
|
|
1960
|
+
});
|
|
1961
|
+
reportActivity({
|
|
1962
|
+
kind: 'status_update',
|
|
1963
|
+
source: 'framer-agent',
|
|
1964
|
+
message:
|
|
1965
|
+
'Targeted Framer documentation is unavailable, so I am continuing with the built-in harness guidance.',
|
|
1966
|
+
});
|
|
1967
|
+
result = builtInInstructions(true);
|
|
1968
|
+
}
|
|
1969
|
+
} else {
|
|
1970
|
+
if (queries.length > 0) instructionFallbacks += 1;
|
|
1971
|
+
result = builtInInstructions(queries.length > 0);
|
|
1972
|
+
}
|
|
894
1973
|
} else if (name === 'framer_context') {
|
|
895
|
-
readCalls += 1;
|
|
896
1974
|
result = await execCode(
|
|
897
1975
|
[
|
|
898
1976
|
'const [context, branch] = await Promise.all([',
|
|
@@ -903,8 +1981,210 @@ export function createFramerAgentToolRuntime({
|
|
|
903
1981
|
].join('\n'),
|
|
904
1982
|
);
|
|
905
1983
|
markInspection();
|
|
1984
|
+
} else if (name === 'framer_plan_task') {
|
|
1985
|
+
if (lastInspectionSequence === 0) {
|
|
1986
|
+
throw toolError(
|
|
1987
|
+
'FRAMER_HARNESS_INSPECTION_REQUIRED',
|
|
1988
|
+
'Inspect the live Framer target before declaring the task and verification plan.',
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
const nextPlan = normalizeTaskPlan(
|
|
1992
|
+
rawArguments,
|
|
1993
|
+
inheritedRejectedMechanisms,
|
|
1994
|
+
);
|
|
1995
|
+
if (
|
|
1996
|
+
mutationCalls > 0
|
|
1997
|
+
&& nextPlan.verification.includes('visual')
|
|
1998
|
+
&& !screenshotBeforeMutation
|
|
1999
|
+
) {
|
|
2000
|
+
throw toolError(
|
|
2001
|
+
'FRAMER_HARNESS_BASELINE_SCREENSHOT_REQUIRED',
|
|
2002
|
+
'Visual verification cannot be added after mutations when no baseline screenshot was captured.',
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
const previousMechanisms = new Map(
|
|
2006
|
+
(taskPlan?.mechanisms || []).map((decision) => [
|
|
2007
|
+
decision.behaviorId,
|
|
2008
|
+
decision.mechanism,
|
|
2009
|
+
]),
|
|
2010
|
+
);
|
|
2011
|
+
const behaviorPlan = mergeBehaviorPlans(taskPlan, nextPlan);
|
|
2012
|
+
taskPlan = {
|
|
2013
|
+
summary: nextPlan.summary,
|
|
2014
|
+
domains: uniqueStrings([
|
|
2015
|
+
...(taskPlan?.domains || []),
|
|
2016
|
+
...nextPlan.domains,
|
|
2017
|
+
]),
|
|
2018
|
+
verification: uniqueStrings([
|
|
2019
|
+
...(taskPlan?.verification || []),
|
|
2020
|
+
...nextPlan.verification,
|
|
2021
|
+
]),
|
|
2022
|
+
...behaviorPlan,
|
|
2023
|
+
};
|
|
2024
|
+
for (const decision of taskPlan.mechanisms) {
|
|
2025
|
+
const prior = previousMechanisms.get(decision.behaviorId);
|
|
2026
|
+
if (prior && prior !== decision.mechanism) {
|
|
2027
|
+
representativeVerifiedBehaviorIds.delete(decision.behaviorId);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
result = {
|
|
2031
|
+
stdout: JSON.stringify({
|
|
2032
|
+
plan: {
|
|
2033
|
+
summary: taskPlan.summary,
|
|
2034
|
+
domains: [...taskPlan.domains],
|
|
2035
|
+
verification: [...taskPlan.verification],
|
|
2036
|
+
behaviors: taskPlan.behaviors.map((behavior) => ({
|
|
2037
|
+
...behavior,
|
|
2038
|
+
expectedContent: [...behavior.expectedContent],
|
|
2039
|
+
})),
|
|
2040
|
+
mechanisms: taskPlan.mechanisms.map((decision) => ({
|
|
2041
|
+
...decision,
|
|
2042
|
+
})),
|
|
2043
|
+
compatibility: taskPlan.compatibility.map((entry) => ({
|
|
2044
|
+
...entry,
|
|
2045
|
+
requiredCapabilities: [...entry.requiredCapabilities],
|
|
2046
|
+
})),
|
|
2047
|
+
rejectedMechanisms: taskPlan.rejectedMechanisms.map((entry) => ({
|
|
2048
|
+
...entry,
|
|
2049
|
+
})),
|
|
2050
|
+
},
|
|
2051
|
+
instruction:
|
|
2052
|
+
'Implement one representative first when required, verify it, then complete coverage and gather every derived verification result after the final mutation.',
|
|
2053
|
+
}),
|
|
2054
|
+
stderr: '',
|
|
2055
|
+
};
|
|
2056
|
+
} else if (name === 'framer_verify_interactions') {
|
|
2057
|
+
if (mutationCalls === 0) {
|
|
2058
|
+
throw toolError(
|
|
2059
|
+
'FRAMER_HARNESS_INTERACTION_MUTATION_REQUIRED',
|
|
2060
|
+
'Verify interactions only after the relevant Framer mutation has succeeded.',
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
if (!taskPlan?.behaviors?.length) {
|
|
2064
|
+
throw toolError(
|
|
2065
|
+
'FRAMER_BEHAVIOR_CONTRACT_REQUIRED',
|
|
2066
|
+
'Declare observable behaviors and compatible mechanisms with framer_plan_task before interaction verification.',
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
const checks = normalizeBehaviorEvidence(
|
|
2070
|
+
rawArguments.checks,
|
|
2071
|
+
taskPlan,
|
|
2072
|
+
);
|
|
2073
|
+
const pagePath = normalizePagePath(rawArguments.pagePath);
|
|
2074
|
+
const requestedIds = evidenceNodeIds(checks);
|
|
2075
|
+
const liveResult = await execCode(
|
|
2076
|
+
[
|
|
2077
|
+
`const requestedIds = ${JSON.stringify(requestedIds)};`,
|
|
2078
|
+
'const nodes = await Promise.all(requestedIds.map(async (id) => ({',
|
|
2079
|
+
' id,',
|
|
2080
|
+
' node: await framer.agent.serialize({ id, depth: 8 },',
|
|
2081
|
+
` { pagePath: ${JSON.stringify(pagePath)} }).catch(() => null),`,
|
|
2082
|
+
' parentId: await framer.agent.getParentNode({ id },',
|
|
2083
|
+
` { pagePath: ${JSON.stringify(pagePath)} }).then((node) => node?.id ?? null).catch(() => null),`,
|
|
2084
|
+
' ancestorIds: await framer.agent.getAncestors({ id },',
|
|
2085
|
+
` { pagePath: ${JSON.stringify(pagePath)} }).then((nodes) => Array.isArray(nodes) ? nodes.map((node) => node?.id).filter(Boolean) : []).catch(() => []),`,
|
|
2086
|
+
'})));',
|
|
2087
|
+
'console.log(JSON.stringify({ nodes }, null, 2));',
|
|
2088
|
+
].join('\n'),
|
|
2089
|
+
);
|
|
2090
|
+
const parsed = parseStructuredOutput(liveResult.stdout);
|
|
2091
|
+
const nodesById = new Map(
|
|
2092
|
+
(Array.isArray(parsed?.nodes) ? parsed.nodes : [])
|
|
2093
|
+
.filter((entry) => isRecord(entry) && typeof entry.id === 'string')
|
|
2094
|
+
.map((entry) => [entry.id, {
|
|
2095
|
+
node: entry.node || null,
|
|
2096
|
+
parentId:
|
|
2097
|
+
typeof entry.parentId === 'string'
|
|
2098
|
+
? entry.parentId
|
|
2099
|
+
: null,
|
|
2100
|
+
ancestorIds: Array.isArray(entry.ancestorIds)
|
|
2101
|
+
? entry.ancestorIds
|
|
2102
|
+
: [],
|
|
2103
|
+
}]),
|
|
2104
|
+
);
|
|
2105
|
+
const verified = verifyBehaviorEvidence({
|
|
2106
|
+
plan: taskPlan,
|
|
2107
|
+
checks,
|
|
2108
|
+
snapshots: nodesById,
|
|
2109
|
+
context: {
|
|
2110
|
+
lowLevelWriteCalls,
|
|
2111
|
+
authorizedInstanceContainerIds,
|
|
2112
|
+
},
|
|
2113
|
+
});
|
|
2114
|
+
markInspection();
|
|
2115
|
+
interactionVerificationCalls += 1;
|
|
2116
|
+
interactionVerification = {
|
|
2117
|
+
...verified,
|
|
2118
|
+
verifiedAt: new Date().toISOString(),
|
|
2119
|
+
};
|
|
2120
|
+
for (const behaviorResult of verified.behaviors) {
|
|
2121
|
+
const failedChecks = verified.checks.filter((check) =>
|
|
2122
|
+
check.behaviorId === behaviorResult.behaviorId && !check.passed);
|
|
2123
|
+
if (failedChecks.length === 0) {
|
|
2124
|
+
repeatedBehaviorVerificationFailures.delete(
|
|
2125
|
+
behaviorResult.behaviorId,
|
|
2126
|
+
);
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
2129
|
+
const failureFingerprint = crypto
|
|
2130
|
+
.createHash('sha256')
|
|
2131
|
+
.update(JSON.stringify({
|
|
2132
|
+
mechanism: behaviorResult.mechanism,
|
|
2133
|
+
failures: failedChecks
|
|
2134
|
+
.flatMap((check) => check.failures)
|
|
2135
|
+
.sort(),
|
|
2136
|
+
}))
|
|
2137
|
+
.digest('hex');
|
|
2138
|
+
const prior = repeatedBehaviorVerificationFailures.get(
|
|
2139
|
+
behaviorResult.behaviorId,
|
|
2140
|
+
);
|
|
2141
|
+
const count =
|
|
2142
|
+
prior?.fingerprint === failureFingerprint
|
|
2143
|
+
? prior.count + 1
|
|
2144
|
+
: 1;
|
|
2145
|
+
repeatedBehaviorVerificationFailures.set(
|
|
2146
|
+
behaviorResult.behaviorId,
|
|
2147
|
+
{ fingerprint: failureFingerprint, count },
|
|
2148
|
+
);
|
|
2149
|
+
if (count < 2 || !behaviorResult.mechanism) continue;
|
|
2150
|
+
const alreadyRejected = taskPlan.rejectedMechanisms.some((rejected) =>
|
|
2151
|
+
rejected.mechanism === behaviorResult.mechanism
|
|
2152
|
+
&& (
|
|
2153
|
+
rejected.behaviorId === behaviorResult.behaviorId
|
|
2154
|
+
|| rejected.behaviorSignature
|
|
2155
|
+
=== behaviorResult.behaviorSignature
|
|
2156
|
+
));
|
|
2157
|
+
if (alreadyRejected) continue;
|
|
2158
|
+
taskPlan = {
|
|
2159
|
+
...taskPlan,
|
|
2160
|
+
rejectedMechanisms: [
|
|
2161
|
+
...taskPlan.rejectedMechanisms,
|
|
2162
|
+
{
|
|
2163
|
+
behaviorId: behaviorResult.behaviorId,
|
|
2164
|
+
behaviorSignature: behaviorResult.behaviorSignature,
|
|
2165
|
+
mechanism: behaviorResult.mechanism,
|
|
2166
|
+
reason:
|
|
2167
|
+
`Repeated structural verification failed: ${failedChecks
|
|
2168
|
+
.flatMap((check) => check.failures)
|
|
2169
|
+
.join('; ')
|
|
2170
|
+
.slice(0, 700)}`,
|
|
2171
|
+
source: 'verification_failure',
|
|
2172
|
+
},
|
|
2173
|
+
],
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
verified.representativeBehaviorIds.forEach((behaviorId) =>
|
|
2177
|
+
representativeVerifiedBehaviorIds.add(behaviorId));
|
|
2178
|
+
if (interactionVerification.completed) {
|
|
2179
|
+
verificationEvidence.set('interactions', operationSequence);
|
|
2180
|
+
} else {
|
|
2181
|
+
verificationEvidence.delete('interactions');
|
|
2182
|
+
}
|
|
2183
|
+
result = {
|
|
2184
|
+
stdout: JSON.stringify(interactionVerification),
|
|
2185
|
+
stderr: liveResult.stderr,
|
|
2186
|
+
};
|
|
906
2187
|
} else if (name === 'framer_read_project') {
|
|
907
|
-
readCalls += 1;
|
|
908
2188
|
const queries = Array.isArray(rawArguments.queries)
|
|
909
2189
|
? rawArguments.queries.slice(0, 50)
|
|
910
2190
|
: [];
|
|
@@ -921,7 +2201,41 @@ export function createFramerAgentToolRuntime({
|
|
|
921
2201
|
'console.log(JSON.stringify(result, null, 2));',
|
|
922
2202
|
].join('\n'),
|
|
923
2203
|
);
|
|
924
|
-
markInspection(
|
|
2204
|
+
markInspection({
|
|
2205
|
+
visual: queries.some((query) => query.type === 'screenshot'),
|
|
2206
|
+
verifies: rawArguments.verifies,
|
|
2207
|
+
});
|
|
2208
|
+
} else if (name === 'framer_query_images') {
|
|
2209
|
+
const query = String(rawArguments.query || '').trim();
|
|
2210
|
+
if (query.length < 2) {
|
|
2211
|
+
throw toolError(
|
|
2212
|
+
'FRAMER_AGENT_IMAGE_QUERY_INVALID',
|
|
2213
|
+
'A specific image-search query is required.',
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
const count = Math.max(
|
|
2217
|
+
1,
|
|
2218
|
+
Math.min(8, Math.floor(Number(rawArguments.count) || 4)),
|
|
2219
|
+
);
|
|
2220
|
+
const orientation = ['landscape', 'portrait', 'squarish']
|
|
2221
|
+
.includes(rawArguments.orientation)
|
|
2222
|
+
? rawArguments.orientation
|
|
2223
|
+
: undefined;
|
|
2224
|
+
result = await execCode(
|
|
2225
|
+
[
|
|
2226
|
+
'const result = await framer.agent.queryImages({',
|
|
2227
|
+
' source: "unsplash",',
|
|
2228
|
+
` query: ${JSON.stringify(query)},`,
|
|
2229
|
+
` count: ${count},`,
|
|
2230
|
+
...(orientation
|
|
2231
|
+
? [` orientation: ${JSON.stringify(orientation)},`]
|
|
2232
|
+
: []),
|
|
2233
|
+
'});',
|
|
2234
|
+
'console.log(JSON.stringify(result, null, 2));',
|
|
2235
|
+
].join('\n'),
|
|
2236
|
+
{ timeoutMs: 30_000 },
|
|
2237
|
+
);
|
|
2238
|
+
imageSearchCalls += 1;
|
|
925
2239
|
} else if (name === 'framer_apply_changes') {
|
|
926
2240
|
const changes = String(rawArguments.changes || '').trim();
|
|
927
2241
|
if (!changes) {
|
|
@@ -930,6 +2244,7 @@ export function createFramerAgentToolRuntime({
|
|
|
930
2244
|
'Framer Agent changes are required.',
|
|
931
2245
|
);
|
|
932
2246
|
}
|
|
2247
|
+
requireTaskPlanForMutation({ canvas: true });
|
|
933
2248
|
const pagePath = normalizePagePath(rawArguments.pagePath);
|
|
934
2249
|
result = await execCode(
|
|
935
2250
|
[
|
|
@@ -938,31 +2253,58 @@ export function createFramerAgentToolRuntime({
|
|
|
938
2253
|
].join('\n'),
|
|
939
2254
|
);
|
|
940
2255
|
mutationCalls += 1;
|
|
2256
|
+
applyChangesCalls += 1;
|
|
941
2257
|
markMutation();
|
|
2258
|
+
const parsedResult = parseStructuredOutput(result.stdout);
|
|
2259
|
+
mutationReceipts.push({
|
|
2260
|
+
sequence: operationSequence,
|
|
2261
|
+
kind: 'framer_apply_changes',
|
|
2262
|
+
pagePath,
|
|
2263
|
+
...summarizeFramerChanges(changes),
|
|
2264
|
+
renamedIds: canonicalIdMappings(parsedResult),
|
|
2265
|
+
});
|
|
942
2266
|
} else if (name === 'framer_read') {
|
|
943
|
-
readCalls += 1;
|
|
944
2267
|
result = await execCode(
|
|
945
2268
|
validateCode(rawArguments.code, {
|
|
946
2269
|
write: false,
|
|
947
2270
|
allowPublishing,
|
|
948
2271
|
}),
|
|
949
2272
|
);
|
|
950
|
-
markInspection();
|
|
2273
|
+
markInspection({ verifies: rawArguments.verifies });
|
|
951
2274
|
} else if (name === 'framer_write') {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
)
|
|
2275
|
+
const code = validateCode(rawArguments.code, {
|
|
2276
|
+
write: true,
|
|
2277
|
+
allowPublishing,
|
|
2278
|
+
});
|
|
2279
|
+
const reason = String(rawArguments.reason || '').trim();
|
|
2280
|
+
if (reason.length < 10) {
|
|
2281
|
+
throw toolError(
|
|
2282
|
+
'FRAMER_AGENT_WRITE_REASON_REQUIRED',
|
|
2283
|
+
'Explain why this operation cannot be completed with framer_apply_changes before using framer_write.',
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
requireTaskPlanForMutation();
|
|
2287
|
+
result = await execCode(code);
|
|
958
2288
|
mutationCalls += 1;
|
|
2289
|
+
lowLevelWriteCalls += 1;
|
|
959
2290
|
markMutation();
|
|
2291
|
+
mutationReceipts.push({
|
|
2292
|
+
sequence: operationSequence,
|
|
2293
|
+
kind: 'framer_write',
|
|
2294
|
+
reason: reason.slice(0, 500),
|
|
2295
|
+
codeHash: crypto.createHash('sha256').update(code).digest('hex'),
|
|
2296
|
+
});
|
|
960
2297
|
} else {
|
|
961
2298
|
throw toolError(
|
|
962
2299
|
'FRAMER_AGENT_TOOL_UNKNOWN',
|
|
963
2300
|
`Unknown Framer Agent tool: ${name}`,
|
|
964
2301
|
);
|
|
965
2302
|
}
|
|
2303
|
+
const durationMs = Date.now() - startedAt;
|
|
2304
|
+
repeatedFailureFingerprint = null;
|
|
2305
|
+
repeatedFailureCount = 0;
|
|
2306
|
+
toolCall.status = 'succeeded';
|
|
2307
|
+
toolCall.durationMs = durationMs;
|
|
966
2308
|
reportActivity({
|
|
967
2309
|
kind: 'tool_active',
|
|
968
2310
|
source: 'framer-agent',
|
|
@@ -972,10 +2314,54 @@ export function createFramerAgentToolRuntime({
|
|
|
972
2314
|
name === 'framer_apply_changes' || name === 'framer_write'
|
|
973
2315
|
? 'Updated the Framer project'
|
|
974
2316
|
: 'Finished inspecting the Framer project',
|
|
975
|
-
durationMs
|
|
2317
|
+
durationMs,
|
|
976
2318
|
});
|
|
977
2319
|
return normalizedResult(result);
|
|
978
2320
|
} catch (error) {
|
|
2321
|
+
if (
|
|
2322
|
+
name === 'framer_write'
|
|
2323
|
+
&& error?.code === 'FRAMER_AGENT_CODE_UNSAFE'
|
|
2324
|
+
) {
|
|
2325
|
+
unsafeWriteFailures += 1;
|
|
2326
|
+
if (
|
|
2327
|
+
!lowLevelWriteDisabled
|
|
2328
|
+
&& unsafeWriteFailures >= MAX_UNSAFE_WRITE_FAILURES
|
|
2329
|
+
) {
|
|
2330
|
+
lowLevelWriteDisabled = true;
|
|
2331
|
+
reportActivity({
|
|
2332
|
+
kind: 'status_update',
|
|
2333
|
+
source: 'framer-agent',
|
|
2334
|
+
message:
|
|
2335
|
+
'Repeated low-level writes were rejected by the safety boundary, so I switched the run to the safer Framer change path.',
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
if (
|
|
2340
|
+
name === 'framer_write'
|
|
2341
|
+
&& error?.code === 'FRAMER_AGENT_COMMAND_FAILED'
|
|
2342
|
+
) {
|
|
2343
|
+
lowLevelWriteFailures += 1;
|
|
2344
|
+
if (
|
|
2345
|
+
!lowLevelWriteDisabled
|
|
2346
|
+
&& lowLevelWriteFailures >= MAX_LOW_LEVEL_WRITE_FAILURES
|
|
2347
|
+
) {
|
|
2348
|
+
lowLevelWriteDisabled = true;
|
|
2349
|
+
reportActivity({
|
|
2350
|
+
kind: 'status_update',
|
|
2351
|
+
source: 'framer-agent',
|
|
2352
|
+
message:
|
|
2353
|
+
'Repeated low-level Framer writes failed, so I switched the run back to semantic Framer changes and focused inspection.',
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
if (repeatedFailureFingerprint === fingerprint) {
|
|
2358
|
+
repeatedFailureCount += 1;
|
|
2359
|
+
} else {
|
|
2360
|
+
repeatedFailureFingerprint = fingerprint;
|
|
2361
|
+
repeatedFailureCount = 1;
|
|
2362
|
+
}
|
|
2363
|
+
toolCall.durationMs = Date.now() - startedAt;
|
|
2364
|
+
toolCall.errorCode = error?.code || 'FRAMER_AGENT_TOOL_FAILED';
|
|
979
2365
|
trace?.warn?.('framer_agent_tool_failed', {
|
|
980
2366
|
name,
|
|
981
2367
|
code: error?.code,
|
|
@@ -1061,31 +2447,96 @@ export function createFramerAgentToolRuntime({
|
|
|
1061
2447
|
definitions,
|
|
1062
2448
|
toolNames: definitions.map((definition) => definition.name),
|
|
1063
2449
|
preflight: validateAuthoritativeNodeIds,
|
|
2450
|
+
loadProjectGuidance,
|
|
2451
|
+
configurePromptContext,
|
|
1064
2452
|
invoke,
|
|
1065
2453
|
startGateway,
|
|
1066
|
-
synchronize:
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
}),
|
|
1071
|
-
inspect: async () => ({
|
|
1072
|
-
synchronized: synchronized(),
|
|
1073
|
-
sourceHash: null,
|
|
1074
|
-
changed: mutationCalls > 0,
|
|
1075
|
-
mutationCalls,
|
|
1076
|
-
readCalls,
|
|
1077
|
-
branch: branchChange,
|
|
1078
|
-
lastInspectionAt,
|
|
1079
|
-
}),
|
|
2454
|
+
synchronize: inspectFinalState,
|
|
2455
|
+
inspect: inspectFinalState,
|
|
2456
|
+
finalizeCompletion,
|
|
2457
|
+
verificationRepairPrompt,
|
|
1080
2458
|
sourceHash: () => null,
|
|
1081
2459
|
hasPendingChanges: () => false,
|
|
1082
2460
|
directWorkspace: true,
|
|
1083
2461
|
summary: () => ({
|
|
1084
2462
|
projectId,
|
|
2463
|
+
projectUrl,
|
|
1085
2464
|
mutationCalls,
|
|
2465
|
+
applyChangesCalls,
|
|
2466
|
+
lowLevelWriteCalls,
|
|
2467
|
+
imageSearchCalls,
|
|
2468
|
+
interactionVerificationCalls,
|
|
2469
|
+
mutationReceipts: mutationReceipts.map((receipt) => ({
|
|
2470
|
+
...receipt,
|
|
2471
|
+
...(Array.isArray(receipt.commands)
|
|
2472
|
+
? {
|
|
2473
|
+
commands: receipt.commands.map((command) => ({
|
|
2474
|
+
...command,
|
|
2475
|
+
...(Array.isArray(command.eventActions)
|
|
2476
|
+
? {
|
|
2477
|
+
eventActions: command.eventActions.map((action) => ({
|
|
2478
|
+
...action,
|
|
2479
|
+
})),
|
|
2480
|
+
}
|
|
2481
|
+
: {}),
|
|
2482
|
+
})),
|
|
2483
|
+
}
|
|
2484
|
+
: {}),
|
|
2485
|
+
...(receipt.renamedIds
|
|
2486
|
+
? { renamedIds: { ...receipt.renamedIds } }
|
|
2487
|
+
: {}),
|
|
2488
|
+
})),
|
|
1086
2489
|
readCalls,
|
|
2490
|
+
progressCalls,
|
|
2491
|
+
toolCallCount,
|
|
2492
|
+
toolDurationMs: toolCalls.reduce(
|
|
2493
|
+
(total, call) => total + call.durationMs,
|
|
2494
|
+
0,
|
|
2495
|
+
),
|
|
2496
|
+
toolCalls: toolCalls.map((call) => ({ ...call })),
|
|
2497
|
+
workingPreferences: {
|
|
2498
|
+
focus: workingPreferences.mode,
|
|
2499
|
+
writeScope: workingPreferences.writeScope,
|
|
2500
|
+
preferBatchedMutations:
|
|
2501
|
+
workingPreferences.preferBatchedMutations,
|
|
2502
|
+
avoidRepeatedInspection:
|
|
2503
|
+
workingPreferences.avoidRepeatedInspection,
|
|
2504
|
+
expandScopeOnlyWhenNeeded:
|
|
2505
|
+
workingPreferences.expandScopeOnlyWhenNeeded,
|
|
2506
|
+
},
|
|
2507
|
+
loopProtection: {
|
|
2508
|
+
identicalFailureLimit: 5,
|
|
2509
|
+
blockedCalls: repeatedFailureStops,
|
|
2510
|
+
unsafeWriteFailureLimit: MAX_UNSAFE_WRITE_FAILURES,
|
|
2511
|
+
unsafeWriteFailures,
|
|
2512
|
+
lowLevelWriteFailureLimit: MAX_LOW_LEVEL_WRITE_FAILURES,
|
|
2513
|
+
lowLevelWriteFailures,
|
|
2514
|
+
lowLevelWriteDisabled,
|
|
2515
|
+
lowLevelWriteBlockedCalls,
|
|
2516
|
+
instructionFallbacks,
|
|
2517
|
+
},
|
|
1087
2518
|
branch: branchChange,
|
|
1088
2519
|
lastInspectionAt,
|
|
2520
|
+
lastMutationAt,
|
|
2521
|
+
taskPlan: taskPlanSnapshot(),
|
|
2522
|
+
verification: verification(),
|
|
2523
|
+
framerAgentVersion,
|
|
2524
|
+
framerGuidanceLoaded: guidanceMetadata.loaded,
|
|
2525
|
+
doctrineLoaded: promptMetadata.doctrineLoaded,
|
|
2526
|
+
guidanceHash: guidanceMetadata.contentHash,
|
|
2527
|
+
projectContextHash: guidanceMetadata.projectContextHash,
|
|
2528
|
+
visualVerificationRequired:
|
|
2529
|
+
verification().visualRequired,
|
|
2530
|
+
visualVerificationCompleted:
|
|
2531
|
+
verification().visualCompleted,
|
|
2532
|
+
guidance: {
|
|
2533
|
+
loaded: guidanceMetadata.loaded,
|
|
2534
|
+
contentHash: guidanceMetadata.contentHash,
|
|
2535
|
+
projectContextHash: guidanceMetadata.projectContextHash,
|
|
2536
|
+
files: [...guidanceMetadata.files],
|
|
2537
|
+
bytes: guidanceMetadata.bytes,
|
|
2538
|
+
},
|
|
2539
|
+
doctrine: { ...promptMetadata },
|
|
1089
2540
|
}),
|
|
1090
2541
|
fatalInfrastructureError: () => null,
|
|
1091
2542
|
waitForFatalInfrastructure: () => new Promise(() => undefined),
|