@gakim-digital/dexter-bridge 0.11.0 → 0.11.1
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 +260 -39
- package/src/api.js +34 -0
- package/src/config.js +5 -0
- package/src/framerAgentTools.js +1631 -192
- package/src/framerBehaviorContract.js +962 -0
- 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,36 @@ 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
|
);
|
|
512
1032
|
const allowPublishing = assignment?.context?.permissions?.publish === true;
|
|
1033
|
+
const inheritedRejectedMechanisms =
|
|
1034
|
+
assignment?.context?.repairContext?.rejectedMechanisms
|
|
1035
|
+
|| assignment?.repairContext?.rejectedMechanisms
|
|
1036
|
+
|| [];
|
|
1037
|
+
const authorizedInstanceContainerIds = uniqueStrings(
|
|
1038
|
+
assignment?.context?.permissions?.scopedComponentArtifacts
|
|
1039
|
+
?.instanceContainerNodeIds
|
|
1040
|
+
|| assignment?.context?.contextGrant?.writableNodeIds
|
|
1041
|
+
|| [],
|
|
1042
|
+
);
|
|
513
1043
|
const activePagePath = normalizePagePath(
|
|
514
1044
|
assignment?.context?.activePagePath,
|
|
515
1045
|
);
|
|
@@ -523,9 +1053,53 @@ export function createFramerAgentToolRuntime({
|
|
|
523
1053
|
let closed = false;
|
|
524
1054
|
let serial = Promise.resolve();
|
|
525
1055
|
let mutationCalls = 0;
|
|
1056
|
+
let applyChangesCalls = 0;
|
|
1057
|
+
let lowLevelWriteCalls = 0;
|
|
1058
|
+
let imageSearchCalls = 0;
|
|
526
1059
|
let readCalls = 0;
|
|
1060
|
+
let progressCalls = 0;
|
|
1061
|
+
let toolCallCount = 0;
|
|
1062
|
+
const toolCalls = [];
|
|
1063
|
+
let repeatedFailureFingerprint = null;
|
|
1064
|
+
let repeatedFailureCount = 0;
|
|
1065
|
+
let repeatedFailureStops = 0;
|
|
1066
|
+
let unsafeWriteFailures = 0;
|
|
1067
|
+
let lowLevelWriteFailures = 0;
|
|
1068
|
+
let lowLevelWriteDisabled = false;
|
|
1069
|
+
let lowLevelWriteBlockedCalls = 0;
|
|
1070
|
+
let instructionFallbacks = 0;
|
|
1071
|
+
let instructionLookupUnavailable = false;
|
|
527
1072
|
let branchChange = null;
|
|
528
1073
|
let lastInspectionAt = null;
|
|
1074
|
+
let lastMutationAt = null;
|
|
1075
|
+
let screenshotCalls = 0;
|
|
1076
|
+
let interactionVerificationCalls = 0;
|
|
1077
|
+
let interactionVerification = {
|
|
1078
|
+
completed: false,
|
|
1079
|
+
verificationLevel: 'unverified',
|
|
1080
|
+
checks: [],
|
|
1081
|
+
behaviors: [],
|
|
1082
|
+
verifiedAt: null,
|
|
1083
|
+
};
|
|
1084
|
+
const representativeVerifiedBehaviorIds = new Set();
|
|
1085
|
+
const repeatedBehaviorVerificationFailures = new Map();
|
|
1086
|
+
const mutationReceipts = [];
|
|
1087
|
+
let screenshotBeforeMutation = false;
|
|
1088
|
+
let lastScreenshotSequence = 0;
|
|
1089
|
+
let taskPlan = null;
|
|
1090
|
+
const verificationEvidence = new Map();
|
|
1091
|
+
let guidanceMetadata = {
|
|
1092
|
+
loaded: false,
|
|
1093
|
+
contentHash: null,
|
|
1094
|
+
projectContextHash: null,
|
|
1095
|
+
files: [],
|
|
1096
|
+
bytes: 0,
|
|
1097
|
+
};
|
|
1098
|
+
let promptMetadata = {
|
|
1099
|
+
doctrineLoaded: false,
|
|
1100
|
+
doctrineVersion: null,
|
|
1101
|
+
};
|
|
1102
|
+
const framerAgentVersion = installedFramerAgentVersion();
|
|
529
1103
|
let operationSequence = 0;
|
|
530
1104
|
let lastMutationSequence = 0;
|
|
531
1105
|
let lastInspectionSequence = 0;
|
|
@@ -553,6 +1127,7 @@ export function createFramerAgentToolRuntime({
|
|
|
553
1127
|
|
|
554
1128
|
const ensureHostedSetup = async () => {
|
|
555
1129
|
if (env.DEXTER_HOSTED_FRAMER_AUTO_SETUP !== 'true') return;
|
|
1130
|
+
if (hasInstalledFramerSkill(env)) return;
|
|
556
1131
|
if (!hostedSetupPromise) {
|
|
557
1132
|
reportActivity({
|
|
558
1133
|
kind: 'status_update',
|
|
@@ -578,7 +1153,7 @@ export function createFramerAgentToolRuntime({
|
|
|
578
1153
|
);
|
|
579
1154
|
};
|
|
580
1155
|
|
|
581
|
-
const
|
|
1156
|
+
const resolveProjectAuthorization = async () => {
|
|
582
1157
|
if (typeof authorizeProject !== 'function') {
|
|
583
1158
|
throw toolError(
|
|
584
1159
|
'FRAMER_AGENT_AUTHORIZATION_REQUIRED',
|
|
@@ -589,7 +1164,7 @@ export function createFramerAgentToolRuntime({
|
|
|
589
1164
|
let authorization = await authorizeProject({
|
|
590
1165
|
projectId,
|
|
591
1166
|
initiate: true,
|
|
592
|
-
forceRefresh,
|
|
1167
|
+
forceRefresh: false,
|
|
593
1168
|
signal: abortController.signal,
|
|
594
1169
|
});
|
|
595
1170
|
if (authorization?.framerProjectId !== projectId) {
|
|
@@ -600,52 +1175,17 @@ export function createFramerAgentToolRuntime({
|
|
|
600
1175
|
);
|
|
601
1176
|
}
|
|
602
1177
|
if (authorization?.status === 'authorization_required') {
|
|
603
|
-
const url = authorizationUrl(authorization.authUrl);
|
|
604
1178
|
reportActivity({
|
|
605
1179
|
kind: 'status_update',
|
|
606
1180
|
source: 'framer-agent',
|
|
607
1181
|
message:
|
|
608
|
-
'
|
|
1182
|
+
'This Framer project needs a project API key before Dexter can continue.',
|
|
609
1183
|
});
|
|
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,
|
|
1184
|
+
throw toolError(
|
|
1185
|
+
'FRAMER_AGENT_AUTHORIZATION_REQUIRED',
|
|
1186
|
+
'Connect this Framer project in Dexter by adding a project API key, then retry.',
|
|
1187
|
+
409,
|
|
627
1188
|
);
|
|
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
1189
|
}
|
|
650
1190
|
const apiKey = String(authorization?.apiKey || '').trim();
|
|
651
1191
|
if (authorization?.status !== 'ready' || !apiKey) {
|
|
@@ -655,13 +1195,21 @@ export function createFramerAgentToolRuntime({
|
|
|
655
1195
|
409,
|
|
656
1196
|
);
|
|
657
1197
|
}
|
|
658
|
-
|
|
1198
|
+
const authorizedProjectUrl = normalizeProjectUrl(
|
|
1199
|
+
authorization.framerProjectUrl,
|
|
1200
|
+
projectId,
|
|
1201
|
+
);
|
|
1202
|
+
return { ...authorization, apiKey, framerProjectUrl: authorizedProjectUrl };
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
const installProjectAuthorization = async (authorization) => {
|
|
1206
|
+
await executeCli(['project', 'auth', projectId, authorization.apiKey], {
|
|
659
1207
|
timeoutMs: 30_000,
|
|
660
1208
|
});
|
|
661
1209
|
reportActivity({
|
|
662
1210
|
kind: 'status_update',
|
|
663
1211
|
source: 'framer-agent',
|
|
664
|
-
message: 'This Framer project is authorized for the
|
|
1212
|
+
message: 'This Framer project is authorized for the coding harness.',
|
|
665
1213
|
});
|
|
666
1214
|
};
|
|
667
1215
|
|
|
@@ -671,14 +1219,55 @@ export function createFramerAgentToolRuntime({
|
|
|
671
1219
|
});
|
|
672
1220
|
};
|
|
673
1221
|
|
|
1222
|
+
const reportRejectedAuthorization = async (authorization, error) => {
|
|
1223
|
+
if (
|
|
1224
|
+
typeof verifyProjectAuthorization !== 'function'
|
|
1225
|
+
|| !authorization?.credentialVersion
|
|
1226
|
+
) return;
|
|
1227
|
+
await verifyProjectAuthorization({
|
|
1228
|
+
status: 'rejected',
|
|
1229
|
+
framerProjectId: projectId,
|
|
1230
|
+
framerProjectUrl: projectUrl,
|
|
1231
|
+
credentialVersion: authorization.credentialVersion,
|
|
1232
|
+
errorCode: String(error?.code || 'FRAMER_AGENT_AUTHORIZATION_REJECTED')
|
|
1233
|
+
.slice(0, 120),
|
|
1234
|
+
signal: abortController.signal,
|
|
1235
|
+
}).catch(() => undefined);
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
const verifyConnectedProject = async (activeSessionId, authorization) => {
|
|
1239
|
+
const result = await executeCli(['exec', '-s', activeSessionId], {
|
|
1240
|
+
input: [
|
|
1241
|
+
'const [project, branch] = await Promise.all([',
|
|
1242
|
+
' framer.getProjectInfo(),',
|
|
1243
|
+
' framer.agent.getActiveBranch(),',
|
|
1244
|
+
']);',
|
|
1245
|
+
'console.log(JSON.stringify({ projectName: project?.name ?? null, projectUrl: branch?.url ?? null }));',
|
|
1246
|
+
].join('\n'),
|
|
1247
|
+
timeoutMs: 30_000,
|
|
1248
|
+
});
|
|
1249
|
+
const observed = parseStructuredOutput(result.stdout);
|
|
1250
|
+
const observedProjectUrl = normalizeProjectUrl(
|
|
1251
|
+
observed?.projectUrl,
|
|
1252
|
+
projectId,
|
|
1253
|
+
);
|
|
1254
|
+
if (
|
|
1255
|
+
typeof verifyProjectAuthorization === 'function'
|
|
1256
|
+
&& authorization?.credentialVersion
|
|
1257
|
+
) {
|
|
1258
|
+
await verifyProjectAuthorization({
|
|
1259
|
+
status: 'verified',
|
|
1260
|
+
framerProjectId: projectId,
|
|
1261
|
+
framerProjectUrl: observedProjectUrl,
|
|
1262
|
+
credentialVersion: authorization.credentialVersion,
|
|
1263
|
+
signal: abortController.signal,
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
|
|
674
1268
|
const ensureSession = async () => {
|
|
675
1269
|
await ensureHostedSetup();
|
|
676
1270
|
if (sessionId) return sessionId;
|
|
677
|
-
const cached = sessionsByProject.get(projectId);
|
|
678
|
-
if (cached) {
|
|
679
|
-
sessionId = cached;
|
|
680
|
-
return sessionId;
|
|
681
|
-
}
|
|
682
1271
|
if (sessionPromise) return sessionPromise;
|
|
683
1272
|
sessionPromise = (async () => {
|
|
684
1273
|
reportActivity({
|
|
@@ -686,23 +1275,49 @@ export function createFramerAgentToolRuntime({
|
|
|
686
1275
|
source: 'framer-agent',
|
|
687
1276
|
message: 'Connecting the coding harness to this Framer project.',
|
|
688
1277
|
});
|
|
689
|
-
|
|
690
|
-
|
|
1278
|
+
let authorization = null;
|
|
1279
|
+
if (typeof authorizeProject === 'function') {
|
|
1280
|
+
authorization = await resolveProjectAuthorization();
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
const cached = sessionsByProject.get(sessionCacheKey);
|
|
1284
|
+
if (
|
|
1285
|
+
cached
|
|
1286
|
+
&& (
|
|
1287
|
+
typeof authorizeProject !== 'function'
|
|
1288
|
+
|| (
|
|
1289
|
+
authorization?.verificationStatus === 'verified'
|
|
1290
|
+
&& cached.credentialVersion === authorization.credentialVersion
|
|
1291
|
+
)
|
|
1292
|
+
)
|
|
1293
|
+
) {
|
|
1294
|
+
sessionId = cached.sessionId;
|
|
1295
|
+
return sessionId;
|
|
1296
|
+
}
|
|
1297
|
+
if (cached) {
|
|
1298
|
+
await executeCli(['session', 'destroy', cached.sessionId], {
|
|
1299
|
+
timeoutMs: 10_000,
|
|
1300
|
+
}).catch(() => undefined);
|
|
1301
|
+
sessionsByProject.delete(sessionCacheKey);
|
|
1302
|
+
}
|
|
1303
|
+
if (!authorization && !(await projectIsAuthorizedLocally())) {
|
|
1304
|
+
throw toolError(
|
|
1305
|
+
'FRAMER_AGENT_AUTHORIZATION_REQUIRED',
|
|
1306
|
+
'This Framer project needs to be connected before Dexter can use it.',
|
|
1307
|
+
409,
|
|
1308
|
+
);
|
|
691
1309
|
}
|
|
692
1310
|
let created;
|
|
693
1311
|
try {
|
|
1312
|
+
if (authorization) {
|
|
1313
|
+
await installProjectAuthorization(authorization);
|
|
1314
|
+
}
|
|
694
1315
|
created = await createSession();
|
|
695
1316
|
} catch (error) {
|
|
696
1317
|
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);
|
|
1318
|
+
await reportRejectedAuthorization(authorization, error);
|
|
705
1319
|
}
|
|
1320
|
+
throw projectConnectionError(error);
|
|
706
1321
|
}
|
|
707
1322
|
sessionId = sessionIdFromOutput(created.stdout);
|
|
708
1323
|
if (!sessionId) {
|
|
@@ -721,7 +1336,25 @@ export function createFramerAgentToolRuntime({
|
|
|
721
1336
|
503,
|
|
722
1337
|
);
|
|
723
1338
|
}
|
|
724
|
-
|
|
1339
|
+
try {
|
|
1340
|
+
await verifyConnectedProject(sessionId, authorization);
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
await executeCli(['session', 'destroy', sessionId], {
|
|
1343
|
+
timeoutMs: 10_000,
|
|
1344
|
+
}).catch(() => undefined);
|
|
1345
|
+
sessionId = null;
|
|
1346
|
+
if (
|
|
1347
|
+
isAuthorizationFailure(error)
|
|
1348
|
+
|| error?.code === 'FRAMER_AGENT_PROJECT_MISMATCH'
|
|
1349
|
+
) {
|
|
1350
|
+
await reportRejectedAuthorization(authorization, error);
|
|
1351
|
+
}
|
|
1352
|
+
throw projectConnectionError(error);
|
|
1353
|
+
}
|
|
1354
|
+
sessionsByProject.set(sessionCacheKey, {
|
|
1355
|
+
sessionId,
|
|
1356
|
+
credentialVersion: authorization?.credentialVersion || null,
|
|
1357
|
+
});
|
|
725
1358
|
reportActivity({
|
|
726
1359
|
kind: 'status_update',
|
|
727
1360
|
source: 'framer-agent',
|
|
@@ -734,6 +1367,86 @@ export function createFramerAgentToolRuntime({
|
|
|
734
1367
|
return sessionPromise;
|
|
735
1368
|
};
|
|
736
1369
|
|
|
1370
|
+
const loadProjectGuidance = async () => {
|
|
1371
|
+
await ensureSession();
|
|
1372
|
+
const skillRoot = framerSkillRoot(env);
|
|
1373
|
+
if (!skillRoot) return { content: '', ...guidanceMetadata };
|
|
1374
|
+
const projectRoot = path.join(skillRoot, 'projects', projectId);
|
|
1375
|
+
if (!fs.existsSync(projectRoot)) {
|
|
1376
|
+
return { content: '', ...guidanceMetadata };
|
|
1377
|
+
}
|
|
1378
|
+
const requestedFiles = [
|
|
1379
|
+
{ root: skillRoot, path: 'SKILL.md', label: 'SKILL.md' },
|
|
1380
|
+
{ root: projectRoot, path: 'index.md', label: 'project/index.md' },
|
|
1381
|
+
{
|
|
1382
|
+
root: projectRoot,
|
|
1383
|
+
path: 'project-inventory.md',
|
|
1384
|
+
label: 'project/project-inventory.md',
|
|
1385
|
+
},
|
|
1386
|
+
{ root: projectRoot, path: 'prompt/core-principles.md' },
|
|
1387
|
+
{ root: projectRoot, path: 'prompt/core-examples.md' },
|
|
1388
|
+
{ root: projectRoot, path: 'prompt/implementation-strategy.md' },
|
|
1389
|
+
{ root: projectRoot, path: 'prompt/updating-the-project.md' },
|
|
1390
|
+
{ root: projectRoot, path: 'prompt/tools.md' },
|
|
1391
|
+
{ root: projectRoot, path: 'prompt/overview.md' },
|
|
1392
|
+
{ root: projectRoot, path: 'prompt/guardrails.md' },
|
|
1393
|
+
{ root: projectRoot, path: 'prompt/critical-reminders.md' },
|
|
1394
|
+
...(visualVerificationRequired
|
|
1395
|
+
? [
|
|
1396
|
+
{ root: projectRoot, path: 'prompt/design-rules.md' },
|
|
1397
|
+
{ root: projectRoot, path: 'prompt/how-projects-work.md' },
|
|
1398
|
+
]
|
|
1399
|
+
: []),
|
|
1400
|
+
];
|
|
1401
|
+
const files = [];
|
|
1402
|
+
let remainingBytes = MAX_GUIDANCE_BYTES;
|
|
1403
|
+
for (const requestedFile of requestedFiles) {
|
|
1404
|
+
const loaded = readGuidanceFile(
|
|
1405
|
+
requestedFile.root,
|
|
1406
|
+
requestedFile.path,
|
|
1407
|
+
remainingBytes,
|
|
1408
|
+
);
|
|
1409
|
+
if (!loaded) continue;
|
|
1410
|
+
files.push({
|
|
1411
|
+
...loaded,
|
|
1412
|
+
path: requestedFile.label || requestedFile.path,
|
|
1413
|
+
});
|
|
1414
|
+
remainingBytes -= Buffer.byteLength(loaded.content, 'utf8');
|
|
1415
|
+
if (remainingBytes <= 0) break;
|
|
1416
|
+
}
|
|
1417
|
+
const content = files
|
|
1418
|
+
.map((file) => `## ${file.path}\n\n${file.content}`)
|
|
1419
|
+
.join('\n\n');
|
|
1420
|
+
const inventory = files.find(
|
|
1421
|
+
(file) => file.path.endsWith('project-inventory.md'),
|
|
1422
|
+
)?.content || '';
|
|
1423
|
+
guidanceMetadata = {
|
|
1424
|
+
loaded: Boolean(content),
|
|
1425
|
+
contentHash: content
|
|
1426
|
+
? crypto.createHash('sha256').update(content).digest('hex')
|
|
1427
|
+
: null,
|
|
1428
|
+
projectContextHash: inventory
|
|
1429
|
+
? crypto.createHash('sha256').update(inventory).digest('hex')
|
|
1430
|
+
: null,
|
|
1431
|
+
files: files.map((file) => file.path),
|
|
1432
|
+
bytes: Buffer.byteLength(content, 'utf8'),
|
|
1433
|
+
};
|
|
1434
|
+
return { content, ...guidanceMetadata };
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
const configurePromptContext = ({
|
|
1438
|
+
doctrineLoaded = false,
|
|
1439
|
+
doctrineVersion = null,
|
|
1440
|
+
} = {}) => {
|
|
1441
|
+
promptMetadata = {
|
|
1442
|
+
doctrineLoaded: doctrineLoaded === true,
|
|
1443
|
+
doctrineVersion:
|
|
1444
|
+
typeof doctrineVersion === 'string' && doctrineVersion.trim()
|
|
1445
|
+
? doctrineVersion.trim().slice(0, 160)
|
|
1446
|
+
: null,
|
|
1447
|
+
};
|
|
1448
|
+
};
|
|
1449
|
+
|
|
737
1450
|
const execCode = async (code, { timeoutMs } = {}) => {
|
|
738
1451
|
let activeSessionId = await ensureSession();
|
|
739
1452
|
try {
|
|
@@ -750,7 +1463,7 @@ export function createFramerAgentToolRuntime({
|
|
|
750
1463
|
) {
|
|
751
1464
|
throw error;
|
|
752
1465
|
}
|
|
753
|
-
sessionsByProject.delete(
|
|
1466
|
+
sessionsByProject.delete(sessionCacheKey);
|
|
754
1467
|
sessionId = null;
|
|
755
1468
|
activeSessionId = await ensureSession();
|
|
756
1469
|
return executeCli(['exec', '-s', activeSessionId], {
|
|
@@ -816,7 +1529,9 @@ export function createFramerAgentToolRuntime({
|
|
|
816
1529
|
const normalizedResult = (result) => {
|
|
817
1530
|
const parsed = parseStructuredOutput(result.stdout);
|
|
818
1531
|
return {
|
|
819
|
-
...(parsed === null
|
|
1532
|
+
...(parsed === null
|
|
1533
|
+
? { output: boundedText(result.stdout, MAX_MODEL_RESULT_BYTES) }
|
|
1534
|
+
: { result: boundedStructuredValue(parsed) }),
|
|
820
1535
|
...(result.stderr.trim()
|
|
821
1536
|
? { diagnostics: boundedText(result.stderr, 20_000) }
|
|
822
1537
|
: {}),
|
|
@@ -824,19 +1539,315 @@ export function createFramerAgentToolRuntime({
|
|
|
824
1539
|
};
|
|
825
1540
|
};
|
|
826
1541
|
|
|
827
|
-
const markInspection = () => {
|
|
1542
|
+
const markInspection = ({ visual = false, verifies = [] } = {}) => {
|
|
828
1543
|
operationSequence += 1;
|
|
829
1544
|
lastInspectionSequence = operationSequence;
|
|
830
1545
|
lastInspectionAt = new Date().toISOString();
|
|
1546
|
+
verificationEvidence.set('structural', operationSequence);
|
|
1547
|
+
if (visual) {
|
|
1548
|
+
screenshotCalls += 1;
|
|
1549
|
+
lastScreenshotSequence = operationSequence;
|
|
1550
|
+
verificationEvidence.set('visual', operationSequence);
|
|
1551
|
+
if (lastMutationSequence === 0) screenshotBeforeMutation = true;
|
|
1552
|
+
}
|
|
1553
|
+
for (const check of normalizeVerificationValues(
|
|
1554
|
+
verifies,
|
|
1555
|
+
GENERIC_VERIFICATION_CHECKS,
|
|
1556
|
+
)) {
|
|
1557
|
+
verificationEvidence.set(check, operationSequence);
|
|
1558
|
+
}
|
|
831
1559
|
};
|
|
832
1560
|
|
|
833
1561
|
const markMutation = () => {
|
|
834
1562
|
operationSequence += 1;
|
|
835
1563
|
lastMutationSequence = operationSequence;
|
|
1564
|
+
lastMutationAt = new Date().toISOString();
|
|
836
1565
|
};
|
|
837
1566
|
|
|
1567
|
+
const requiredVerificationChecks = () =>
|
|
1568
|
+
taskPlan?.verification || [];
|
|
1569
|
+
const taskPlanSnapshot = () => {
|
|
1570
|
+
if (!taskPlan) return null;
|
|
1571
|
+
return {
|
|
1572
|
+
summary: taskPlan.summary,
|
|
1573
|
+
domains: [...taskPlan.domains],
|
|
1574
|
+
verification: [...taskPlan.verification],
|
|
1575
|
+
...(taskPlan.behaviors.length > 0
|
|
1576
|
+
? {
|
|
1577
|
+
behaviors: taskPlan.behaviors.map((behavior) => ({
|
|
1578
|
+
...behavior,
|
|
1579
|
+
expectedContent: [...behavior.expectedContent],
|
|
1580
|
+
})),
|
|
1581
|
+
mechanisms: taskPlan.mechanisms.map((decision) => ({
|
|
1582
|
+
...decision,
|
|
1583
|
+
})),
|
|
1584
|
+
rejectedMechanisms: taskPlan.rejectedMechanisms.map((entry) => ({
|
|
1585
|
+
...entry,
|
|
1586
|
+
})),
|
|
1587
|
+
compatibility: taskPlan.compatibility.map((entry) => ({
|
|
1588
|
+
...entry,
|
|
1589
|
+
requiredCapabilities: [...entry.requiredCapabilities],
|
|
1590
|
+
})),
|
|
1591
|
+
}
|
|
1592
|
+
: {}),
|
|
1593
|
+
};
|
|
1594
|
+
};
|
|
838
1595
|
const synchronized = () =>
|
|
839
1596
|
mutationCalls === 0 || lastInspectionSequence > lastMutationSequence;
|
|
1597
|
+
const verificationCheckCompleted = (check) => {
|
|
1598
|
+
if (mutationCalls === 0) return true;
|
|
1599
|
+
if (check === 'structural') return synchronized();
|
|
1600
|
+
if (check === 'visual') {
|
|
1601
|
+
return (
|
|
1602
|
+
screenshotBeforeMutation
|
|
1603
|
+
&& lastScreenshotSequence > lastMutationSequence
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
return Number(verificationEvidence.get(check) || 0) > lastMutationSequence;
|
|
1607
|
+
};
|
|
1608
|
+
const visualVerificationRequired = () =>
|
|
1609
|
+
applyChangesCalls > 0
|
|
1610
|
+
|| requiredVerificationChecks().includes('visual');
|
|
1611
|
+
const visualVerificationCompleted = () =>
|
|
1612
|
+
!visualVerificationRequired()
|
|
1613
|
+
|| mutationCalls === 0
|
|
1614
|
+
|| (
|
|
1615
|
+
screenshotBeforeMutation
|
|
1616
|
+
&& lastScreenshotSequence > lastMutationSequence
|
|
1617
|
+
);
|
|
1618
|
+
const missingVerificationChecks = () => {
|
|
1619
|
+
if (mutationCalls === 0) return [];
|
|
1620
|
+
const missing = [];
|
|
1621
|
+
if (!taskPlan) missing.push('task-plan');
|
|
1622
|
+
const required = new Set([
|
|
1623
|
+
'structural',
|
|
1624
|
+
...requiredVerificationChecks(),
|
|
1625
|
+
...(applyChangesCalls > 0 ? ['visual'] : []),
|
|
1626
|
+
]);
|
|
1627
|
+
for (const check of required) {
|
|
1628
|
+
if (!verificationCheckCompleted(check)) missing.push(check);
|
|
1629
|
+
}
|
|
1630
|
+
return missing;
|
|
1631
|
+
};
|
|
1632
|
+
const verification = () => ({
|
|
1633
|
+
ownedBy: 'harness',
|
|
1634
|
+
required: mutationCalls > 0,
|
|
1635
|
+
completed: missingVerificationChecks().length === 0,
|
|
1636
|
+
structuralCompleted: synchronized(),
|
|
1637
|
+
visualRequired: visualVerificationRequired() && mutationCalls > 0,
|
|
1638
|
+
visualCompleted: visualVerificationCompleted(),
|
|
1639
|
+
screenshotCalls,
|
|
1640
|
+
...(requiredVerificationChecks().includes('interactions')
|
|
1641
|
+
|| interactionVerificationCalls > 0
|
|
1642
|
+
? {
|
|
1643
|
+
interactionVerificationCalls,
|
|
1644
|
+
interactionVerification: {
|
|
1645
|
+
completed: verificationCheckCompleted('interactions'),
|
|
1646
|
+
verificationLevel:
|
|
1647
|
+
verificationCheckCompleted('interactions')
|
|
1648
|
+
? interactionVerification.verificationLevel
|
|
1649
|
+
: 'unverified',
|
|
1650
|
+
checks: interactionVerification.checks.map((check) => ({
|
|
1651
|
+
...check,
|
|
1652
|
+
failures: [...check.failures],
|
|
1653
|
+
})),
|
|
1654
|
+
behaviors: interactionVerification.behaviors.map((behavior) => ({
|
|
1655
|
+
...behavior,
|
|
1656
|
+
failures: [...behavior.failures],
|
|
1657
|
+
})),
|
|
1658
|
+
verifiedAt: interactionVerification.verifiedAt,
|
|
1659
|
+
},
|
|
1660
|
+
}
|
|
1661
|
+
: {}),
|
|
1662
|
+
taskPlan: taskPlanSnapshot(),
|
|
1663
|
+
requiredChecks: [
|
|
1664
|
+
...new Set([
|
|
1665
|
+
'structural',
|
|
1666
|
+
...requiredVerificationChecks(),
|
|
1667
|
+
...(applyChangesCalls > 0 ? ['visual'] : []),
|
|
1668
|
+
]),
|
|
1669
|
+
],
|
|
1670
|
+
completedChecks: [
|
|
1671
|
+
...new Set([
|
|
1672
|
+
'structural',
|
|
1673
|
+
...requiredVerificationChecks(),
|
|
1674
|
+
...(applyChangesCalls > 0 ? ['visual'] : []),
|
|
1675
|
+
]),
|
|
1676
|
+
].filter(verificationCheckCompleted),
|
|
1677
|
+
missingChecks: missingVerificationChecks(),
|
|
1678
|
+
lastMutationAt,
|
|
1679
|
+
lastInspectionAt,
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
const inspectFinalState = async () => {
|
|
1683
|
+
await serial;
|
|
1684
|
+
return {
|
|
1685
|
+
synchronized: synchronized(),
|
|
1686
|
+
sourceHash: null,
|
|
1687
|
+
changed: mutationCalls > 0,
|
|
1688
|
+
mutationCalls,
|
|
1689
|
+
readCalls,
|
|
1690
|
+
branch: branchChange,
|
|
1691
|
+
lastInspectionAt,
|
|
1692
|
+
lastMutationAt,
|
|
1693
|
+
taskPlan: taskPlanSnapshot(),
|
|
1694
|
+
verification: verification(),
|
|
1695
|
+
};
|
|
1696
|
+
};
|
|
1697
|
+
|
|
1698
|
+
const finalizeCompletion = (completion) => {
|
|
1699
|
+
if (
|
|
1700
|
+
completion?.status !== 'completed'
|
|
1701
|
+
|| mutationCalls === 0
|
|
1702
|
+
|| verification().completed
|
|
1703
|
+
) {
|
|
1704
|
+
return completion;
|
|
1705
|
+
}
|
|
1706
|
+
const missing = missingVerificationChecks().map((check) => {
|
|
1707
|
+
if (check === 'task-plan') return 'a harness task and verification plan';
|
|
1708
|
+
if (check === 'structural') return 'a final structural read';
|
|
1709
|
+
if (check === 'visual') return 'before-and-after screenshot verification';
|
|
1710
|
+
return `post-mutation ${check} verification`;
|
|
1711
|
+
});
|
|
1712
|
+
const reason =
|
|
1713
|
+
`The harness ended before completing ${missing.join(' and ')}.`;
|
|
1714
|
+
return {
|
|
1715
|
+
...completion,
|
|
1716
|
+
status: 'failed',
|
|
1717
|
+
summary: `${String(completion?.summary || '').trim()} ${reason}`.trim(),
|
|
1718
|
+
checks: [
|
|
1719
|
+
...(Array.isArray(completion?.checks) ? completion.checks : []),
|
|
1720
|
+
{
|
|
1721
|
+
command: 'Harness-owned final verification',
|
|
1722
|
+
status: 'failed',
|
|
1723
|
+
output: reason,
|
|
1724
|
+
},
|
|
1725
|
+
],
|
|
1726
|
+
blockedReason: null,
|
|
1727
|
+
};
|
|
1728
|
+
};
|
|
1729
|
+
|
|
1730
|
+
const verificationRepairPrompt = () => {
|
|
1731
|
+
const current = verification();
|
|
1732
|
+
if (current.completed || mutationCalls === 0) return null;
|
|
1733
|
+
const missing = [];
|
|
1734
|
+
for (const check of current.missingChecks || []) {
|
|
1735
|
+
if (check === 'task-plan') {
|
|
1736
|
+
missing.push(
|
|
1737
|
+
'declare the task domains and verification plan with framer_plan_task',
|
|
1738
|
+
);
|
|
1739
|
+
} else if (check === 'structural') {
|
|
1740
|
+
missing.push('perform a focused live read after the latest mutation');
|
|
1741
|
+
} else if (check === 'visual') {
|
|
1742
|
+
missing.push(
|
|
1743
|
+
screenshotBeforeMutation
|
|
1744
|
+
? 'capture and inspect the affected canvas after the latest mutation'
|
|
1745
|
+
: 'report that the required baseline screenshot was missed; do not claim visual verification',
|
|
1746
|
+
);
|
|
1747
|
+
} else {
|
|
1748
|
+
missing.push(
|
|
1749
|
+
check === 'interactions'
|
|
1750
|
+
? 'call framer_verify_interactions with canonical evidence node IDs for every declared behavior and chosen mechanism'
|
|
1751
|
+
: `perform a focused read tagged with verifies:["${check}"] after the latest mutation`,
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
return [
|
|
1756
|
+
'Continue the same Framer task from its current project state.',
|
|
1757
|
+
'Do not redo successful work.',
|
|
1758
|
+
`Before returning, ${missing.join(' and ')}.`,
|
|
1759
|
+
'Repair only concrete problems you observe, then return the required JSON outcome.',
|
|
1760
|
+
].join(' ');
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
const builtInInstructions = (documentationUnavailable = false) => ({
|
|
1764
|
+
stdout: JSON.stringify({
|
|
1765
|
+
source: documentationUnavailable
|
|
1766
|
+
? 'built-in-fallback'
|
|
1767
|
+
: 'built-in',
|
|
1768
|
+
guidance: [
|
|
1769
|
+
'Use framer_read with framer.agent.getNode({ id }) or getNodes({ ids }) for node inspection.',
|
|
1770
|
+
'Use framer_read_project for supported project queries such as screenshots.',
|
|
1771
|
+
'Use framer_query_images for approved stock imagery only after checking user attachments and suitable existing project images.',
|
|
1772
|
+
'Use framer_apply_changes for layout and styling. Read its diagnostics, verify the result, and repair concrete issues.',
|
|
1773
|
+
'If framer_write is rejected as unsafe, do not retry JavaScript variants. Switch to framer_apply_changes or report the concrete blocker.',
|
|
1774
|
+
'Batch related work when practical, reuse evidence already collected, and stop when the requested outcome is satisfied.',
|
|
1775
|
+
],
|
|
1776
|
+
documentationUnavailable,
|
|
1777
|
+
}),
|
|
1778
|
+
stderr: '',
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1781
|
+
const reserveToolCall = (name, category) => {
|
|
1782
|
+
toolCallCount += 1;
|
|
1783
|
+
if (category === 'progress') progressCalls += 1;
|
|
1784
|
+
if (category === 'read') readCalls += 1;
|
|
1785
|
+
const call = {
|
|
1786
|
+
sequence: toolCallCount,
|
|
1787
|
+
name,
|
|
1788
|
+
category,
|
|
1789
|
+
status: 'failed',
|
|
1790
|
+
durationMs: 0,
|
|
1791
|
+
};
|
|
1792
|
+
toolCalls.push(call);
|
|
1793
|
+
return call;
|
|
1794
|
+
};
|
|
1795
|
+
|
|
1796
|
+
const callFingerprint = (name, rawArguments) =>
|
|
1797
|
+
crypto
|
|
1798
|
+
.createHash('sha256')
|
|
1799
|
+
.update(`${name}\n${JSON.stringify(rawArguments)}`)
|
|
1800
|
+
.digest('hex');
|
|
1801
|
+
|
|
1802
|
+
const requireTaskPlanForMutation = ({ canvas = false } = {}) => {
|
|
1803
|
+
if (!taskPlan) {
|
|
1804
|
+
throw toolError(
|
|
1805
|
+
'FRAMER_HARNESS_PLAN_REQUIRED',
|
|
1806
|
+
'Inspect the live Framer target, then call framer_plan_task before making a mutation.',
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
if (
|
|
1810
|
+
(canvas || taskPlan.verification.includes('visual'))
|
|
1811
|
+
&& !screenshotBeforeMutation
|
|
1812
|
+
) {
|
|
1813
|
+
throw toolError(
|
|
1814
|
+
'FRAMER_HARNESS_BASELINE_SCREENSHOT_REQUIRED',
|
|
1815
|
+
'Capture and inspect a screenshot of the affected canvas before the first visual mutation.',
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
const rejectedSelectedMechanisms = taskPlan.behaviors.filter((behavior) => {
|
|
1819
|
+
const selected = taskPlan.mechanisms.find((decision) =>
|
|
1820
|
+
decision.behaviorId === behavior.id);
|
|
1821
|
+
return selected && taskPlan.rejectedMechanisms.some((rejected) =>
|
|
1822
|
+
rejected.mechanism === selected.mechanism
|
|
1823
|
+
&& (
|
|
1824
|
+
rejected.behaviorId === behavior.id
|
|
1825
|
+
|| rejected.behaviorSignature === behavior.signature
|
|
1826
|
+
));
|
|
1827
|
+
});
|
|
1828
|
+
if (rejectedSelectedMechanisms.length > 0) {
|
|
1829
|
+
throw toolError(
|
|
1830
|
+
'FRAMER_BEHAVIOR_MECHANISM_REPLAN_REQUIRED',
|
|
1831
|
+
`Choose a different mechanism for ${rejectedSelectedMechanisms.map((behavior) => behavior.id).join(', ')} before making another mutation.`,
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
if (mutationCalls > 0) {
|
|
1835
|
+
const pendingRepresentative = taskPlan.behaviors
|
|
1836
|
+
.filter((behavior) => behavior.expectedTargets > 1)
|
|
1837
|
+
.filter((behavior) =>
|
|
1838
|
+
taskPlan.mechanisms.find((decision) =>
|
|
1839
|
+
decision.behaviorId === behavior.id
|
|
1840
|
+
&& decision.representativeFirst))
|
|
1841
|
+
.filter((behavior) =>
|
|
1842
|
+
!representativeVerifiedBehaviorIds.has(behavior.id));
|
|
1843
|
+
if (pendingRepresentative.length > 0) {
|
|
1844
|
+
throw toolError(
|
|
1845
|
+
'FRAMER_BEHAVIOR_REPRESENTATIVE_VERIFICATION_REQUIRED',
|
|
1846
|
+
`Verify one representative implementation for ${pendingRepresentative.map((behavior) => behavior.id).join(', ')} before batching further mutations.`,
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
840
1851
|
|
|
841
1852
|
async function invokeUnsafe(name, rawArguments = {}) {
|
|
842
1853
|
if (closed) {
|
|
@@ -857,6 +1868,25 @@ export function createFramerAgentToolRuntime({
|
|
|
857
1868
|
'Framer Agent tool arguments must be an object.',
|
|
858
1869
|
);
|
|
859
1870
|
}
|
|
1871
|
+
const mutation =
|
|
1872
|
+
name === 'framer_apply_changes' || name === 'framer_write';
|
|
1873
|
+
const category =
|
|
1874
|
+
name === 'framer_plan_task'
|
|
1875
|
+
? 'plan'
|
|
1876
|
+
: mutation
|
|
1877
|
+
? 'mutation'
|
|
1878
|
+
: 'read';
|
|
1879
|
+
const fingerprint = callFingerprint(name, rawArguments);
|
|
1880
|
+
if (
|
|
1881
|
+
repeatedFailureFingerprint === fingerprint
|
|
1882
|
+
&& repeatedFailureCount >= 5
|
|
1883
|
+
) {
|
|
1884
|
+
repeatedFailureStops += 1;
|
|
1885
|
+
throw toolError(
|
|
1886
|
+
'FRAMER_AGENT_REPEATED_CALL_LOOP',
|
|
1887
|
+
'The same Framer tool call has failed five times in a row. Change the approach or report the concrete blocker.',
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
860
1890
|
if (name === 'progress_update') {
|
|
861
1891
|
const message = String(rawArguments.message || '').trim().slice(0, 500);
|
|
862
1892
|
if (!message) {
|
|
@@ -865,15 +1895,20 @@ export function createFramerAgentToolRuntime({
|
|
|
865
1895
|
'A progress message is required.',
|
|
866
1896
|
);
|
|
867
1897
|
}
|
|
1898
|
+
const toolCall = reserveToolCall(name, 'progress');
|
|
868
1899
|
reportActivity({
|
|
869
1900
|
kind: 'status_update',
|
|
870
1901
|
source: 'harness',
|
|
871
1902
|
message,
|
|
872
1903
|
});
|
|
1904
|
+
repeatedFailureFingerprint = null;
|
|
1905
|
+
repeatedFailureCount = 0;
|
|
1906
|
+
toolCall.status = 'succeeded';
|
|
873
1907
|
return { acknowledged: true };
|
|
874
1908
|
}
|
|
875
1909
|
|
|
876
1910
|
const startedAt = Date.now();
|
|
1911
|
+
const toolCall = reserveToolCall(name, category);
|
|
877
1912
|
reportActivity({
|
|
878
1913
|
kind: 'tool_active',
|
|
879
1914
|
source: 'framer-agent',
|
|
@@ -882,17 +1917,48 @@ export function createFramerAgentToolRuntime({
|
|
|
882
1917
|
message:
|
|
883
1918
|
name === 'framer_apply_changes' || name === 'framer_write'
|
|
884
1919
|
? 'Updating the Framer project'
|
|
1920
|
+
: name === 'framer_plan_task'
|
|
1921
|
+
? 'Planning the Framer work and verification'
|
|
885
1922
|
: 'Inspecting the Framer project',
|
|
886
1923
|
});
|
|
887
1924
|
try {
|
|
888
1925
|
let result;
|
|
889
|
-
if (name === '
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
'
|
|
1926
|
+
if (name === 'framer_write' && lowLevelWriteDisabled) {
|
|
1927
|
+
lowLevelWriteBlockedCalls += 1;
|
|
1928
|
+
throw toolError(
|
|
1929
|
+
'FRAMER_AGENT_WRITE_FALLBACK_REQUIRED',
|
|
1930
|
+
'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
1931
|
);
|
|
1932
|
+
} else if (name === 'framer_instructions') {
|
|
1933
|
+
const queries = Array.isArray(rawArguments.queries)
|
|
1934
|
+
? uniqueStrings(rawArguments.queries, 8)
|
|
1935
|
+
: [];
|
|
1936
|
+
if (queries.length > 0 && !instructionLookupUnavailable) {
|
|
1937
|
+
try {
|
|
1938
|
+
result = await executeCli(
|
|
1939
|
+
['docs', ...queries],
|
|
1940
|
+
{ timeoutMs: 30_000 },
|
|
1941
|
+
);
|
|
1942
|
+
} catch (error) {
|
|
1943
|
+
instructionFallbacks += 1;
|
|
1944
|
+
instructionLookupUnavailable = true;
|
|
1945
|
+
trace?.warn?.('framer_agent_docs_fallback', {
|
|
1946
|
+
code: error?.code,
|
|
1947
|
+
message: error?.message,
|
|
1948
|
+
});
|
|
1949
|
+
reportActivity({
|
|
1950
|
+
kind: 'status_update',
|
|
1951
|
+
source: 'framer-agent',
|
|
1952
|
+
message:
|
|
1953
|
+
'Targeted Framer documentation is unavailable, so I am continuing with the built-in harness guidance.',
|
|
1954
|
+
});
|
|
1955
|
+
result = builtInInstructions(true);
|
|
1956
|
+
}
|
|
1957
|
+
} else {
|
|
1958
|
+
if (queries.length > 0) instructionFallbacks += 1;
|
|
1959
|
+
result = builtInInstructions(queries.length > 0);
|
|
1960
|
+
}
|
|
894
1961
|
} else if (name === 'framer_context') {
|
|
895
|
-
readCalls += 1;
|
|
896
1962
|
result = await execCode(
|
|
897
1963
|
[
|
|
898
1964
|
'const [context, branch] = await Promise.all([',
|
|
@@ -903,8 +1969,210 @@ export function createFramerAgentToolRuntime({
|
|
|
903
1969
|
].join('\n'),
|
|
904
1970
|
);
|
|
905
1971
|
markInspection();
|
|
1972
|
+
} else if (name === 'framer_plan_task') {
|
|
1973
|
+
if (lastInspectionSequence === 0) {
|
|
1974
|
+
throw toolError(
|
|
1975
|
+
'FRAMER_HARNESS_INSPECTION_REQUIRED',
|
|
1976
|
+
'Inspect the live Framer target before declaring the task and verification plan.',
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
const nextPlan = normalizeTaskPlan(
|
|
1980
|
+
rawArguments,
|
|
1981
|
+
inheritedRejectedMechanisms,
|
|
1982
|
+
);
|
|
1983
|
+
if (
|
|
1984
|
+
mutationCalls > 0
|
|
1985
|
+
&& nextPlan.verification.includes('visual')
|
|
1986
|
+
&& !screenshotBeforeMutation
|
|
1987
|
+
) {
|
|
1988
|
+
throw toolError(
|
|
1989
|
+
'FRAMER_HARNESS_BASELINE_SCREENSHOT_REQUIRED',
|
|
1990
|
+
'Visual verification cannot be added after mutations when no baseline screenshot was captured.',
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
const previousMechanisms = new Map(
|
|
1994
|
+
(taskPlan?.mechanisms || []).map((decision) => [
|
|
1995
|
+
decision.behaviorId,
|
|
1996
|
+
decision.mechanism,
|
|
1997
|
+
]),
|
|
1998
|
+
);
|
|
1999
|
+
const behaviorPlan = mergeBehaviorPlans(taskPlan, nextPlan);
|
|
2000
|
+
taskPlan = {
|
|
2001
|
+
summary: nextPlan.summary,
|
|
2002
|
+
domains: uniqueStrings([
|
|
2003
|
+
...(taskPlan?.domains || []),
|
|
2004
|
+
...nextPlan.domains,
|
|
2005
|
+
]),
|
|
2006
|
+
verification: uniqueStrings([
|
|
2007
|
+
...(taskPlan?.verification || []),
|
|
2008
|
+
...nextPlan.verification,
|
|
2009
|
+
]),
|
|
2010
|
+
...behaviorPlan,
|
|
2011
|
+
};
|
|
2012
|
+
for (const decision of taskPlan.mechanisms) {
|
|
2013
|
+
const prior = previousMechanisms.get(decision.behaviorId);
|
|
2014
|
+
if (prior && prior !== decision.mechanism) {
|
|
2015
|
+
representativeVerifiedBehaviorIds.delete(decision.behaviorId);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
result = {
|
|
2019
|
+
stdout: JSON.stringify({
|
|
2020
|
+
plan: {
|
|
2021
|
+
summary: taskPlan.summary,
|
|
2022
|
+
domains: [...taskPlan.domains],
|
|
2023
|
+
verification: [...taskPlan.verification],
|
|
2024
|
+
behaviors: taskPlan.behaviors.map((behavior) => ({
|
|
2025
|
+
...behavior,
|
|
2026
|
+
expectedContent: [...behavior.expectedContent],
|
|
2027
|
+
})),
|
|
2028
|
+
mechanisms: taskPlan.mechanisms.map((decision) => ({
|
|
2029
|
+
...decision,
|
|
2030
|
+
})),
|
|
2031
|
+
compatibility: taskPlan.compatibility.map((entry) => ({
|
|
2032
|
+
...entry,
|
|
2033
|
+
requiredCapabilities: [...entry.requiredCapabilities],
|
|
2034
|
+
})),
|
|
2035
|
+
rejectedMechanisms: taskPlan.rejectedMechanisms.map((entry) => ({
|
|
2036
|
+
...entry,
|
|
2037
|
+
})),
|
|
2038
|
+
},
|
|
2039
|
+
instruction:
|
|
2040
|
+
'Implement one representative first when required, verify it, then complete coverage and gather every derived verification result after the final mutation.',
|
|
2041
|
+
}),
|
|
2042
|
+
stderr: '',
|
|
2043
|
+
};
|
|
2044
|
+
} else if (name === 'framer_verify_interactions') {
|
|
2045
|
+
if (mutationCalls === 0) {
|
|
2046
|
+
throw toolError(
|
|
2047
|
+
'FRAMER_HARNESS_INTERACTION_MUTATION_REQUIRED',
|
|
2048
|
+
'Verify interactions only after the relevant Framer mutation has succeeded.',
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
if (!taskPlan?.behaviors?.length) {
|
|
2052
|
+
throw toolError(
|
|
2053
|
+
'FRAMER_BEHAVIOR_CONTRACT_REQUIRED',
|
|
2054
|
+
'Declare observable behaviors and compatible mechanisms with framer_plan_task before interaction verification.',
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
const checks = normalizeBehaviorEvidence(
|
|
2058
|
+
rawArguments.checks,
|
|
2059
|
+
taskPlan,
|
|
2060
|
+
);
|
|
2061
|
+
const pagePath = normalizePagePath(rawArguments.pagePath);
|
|
2062
|
+
const requestedIds = evidenceNodeIds(checks);
|
|
2063
|
+
const liveResult = await execCode(
|
|
2064
|
+
[
|
|
2065
|
+
`const requestedIds = ${JSON.stringify(requestedIds)};`,
|
|
2066
|
+
'const nodes = await Promise.all(requestedIds.map(async (id) => ({',
|
|
2067
|
+
' id,',
|
|
2068
|
+
' node: await framer.agent.serialize({ id, depth: 8 },',
|
|
2069
|
+
` { pagePath: ${JSON.stringify(pagePath)} }).catch(() => null),`,
|
|
2070
|
+
' parentId: await framer.agent.getParentNode({ id },',
|
|
2071
|
+
` { pagePath: ${JSON.stringify(pagePath)} }).then((node) => node?.id ?? null).catch(() => null),`,
|
|
2072
|
+
' ancestorIds: await framer.agent.getAncestors({ id },',
|
|
2073
|
+
` { pagePath: ${JSON.stringify(pagePath)} }).then((nodes) => Array.isArray(nodes) ? nodes.map((node) => node?.id).filter(Boolean) : []).catch(() => []),`,
|
|
2074
|
+
'})));',
|
|
2075
|
+
'console.log(JSON.stringify({ nodes }, null, 2));',
|
|
2076
|
+
].join('\n'),
|
|
2077
|
+
);
|
|
2078
|
+
const parsed = parseStructuredOutput(liveResult.stdout);
|
|
2079
|
+
const nodesById = new Map(
|
|
2080
|
+
(Array.isArray(parsed?.nodes) ? parsed.nodes : [])
|
|
2081
|
+
.filter((entry) => isRecord(entry) && typeof entry.id === 'string')
|
|
2082
|
+
.map((entry) => [entry.id, {
|
|
2083
|
+
node: entry.node || null,
|
|
2084
|
+
parentId:
|
|
2085
|
+
typeof entry.parentId === 'string'
|
|
2086
|
+
? entry.parentId
|
|
2087
|
+
: null,
|
|
2088
|
+
ancestorIds: Array.isArray(entry.ancestorIds)
|
|
2089
|
+
? entry.ancestorIds
|
|
2090
|
+
: [],
|
|
2091
|
+
}]),
|
|
2092
|
+
);
|
|
2093
|
+
const verified = verifyBehaviorEvidence({
|
|
2094
|
+
plan: taskPlan,
|
|
2095
|
+
checks,
|
|
2096
|
+
snapshots: nodesById,
|
|
2097
|
+
context: {
|
|
2098
|
+
lowLevelWriteCalls,
|
|
2099
|
+
authorizedInstanceContainerIds,
|
|
2100
|
+
},
|
|
2101
|
+
});
|
|
2102
|
+
markInspection();
|
|
2103
|
+
interactionVerificationCalls += 1;
|
|
2104
|
+
interactionVerification = {
|
|
2105
|
+
...verified,
|
|
2106
|
+
verifiedAt: new Date().toISOString(),
|
|
2107
|
+
};
|
|
2108
|
+
for (const behaviorResult of verified.behaviors) {
|
|
2109
|
+
const failedChecks = verified.checks.filter((check) =>
|
|
2110
|
+
check.behaviorId === behaviorResult.behaviorId && !check.passed);
|
|
2111
|
+
if (failedChecks.length === 0) {
|
|
2112
|
+
repeatedBehaviorVerificationFailures.delete(
|
|
2113
|
+
behaviorResult.behaviorId,
|
|
2114
|
+
);
|
|
2115
|
+
continue;
|
|
2116
|
+
}
|
|
2117
|
+
const failureFingerprint = crypto
|
|
2118
|
+
.createHash('sha256')
|
|
2119
|
+
.update(JSON.stringify({
|
|
2120
|
+
mechanism: behaviorResult.mechanism,
|
|
2121
|
+
failures: failedChecks
|
|
2122
|
+
.flatMap((check) => check.failures)
|
|
2123
|
+
.sort(),
|
|
2124
|
+
}))
|
|
2125
|
+
.digest('hex');
|
|
2126
|
+
const prior = repeatedBehaviorVerificationFailures.get(
|
|
2127
|
+
behaviorResult.behaviorId,
|
|
2128
|
+
);
|
|
2129
|
+
const count =
|
|
2130
|
+
prior?.fingerprint === failureFingerprint
|
|
2131
|
+
? prior.count + 1
|
|
2132
|
+
: 1;
|
|
2133
|
+
repeatedBehaviorVerificationFailures.set(
|
|
2134
|
+
behaviorResult.behaviorId,
|
|
2135
|
+
{ fingerprint: failureFingerprint, count },
|
|
2136
|
+
);
|
|
2137
|
+
if (count < 2 || !behaviorResult.mechanism) continue;
|
|
2138
|
+
const alreadyRejected = taskPlan.rejectedMechanisms.some((rejected) =>
|
|
2139
|
+
rejected.mechanism === behaviorResult.mechanism
|
|
2140
|
+
&& (
|
|
2141
|
+
rejected.behaviorId === behaviorResult.behaviorId
|
|
2142
|
+
|| rejected.behaviorSignature
|
|
2143
|
+
=== behaviorResult.behaviorSignature
|
|
2144
|
+
));
|
|
2145
|
+
if (alreadyRejected) continue;
|
|
2146
|
+
taskPlan = {
|
|
2147
|
+
...taskPlan,
|
|
2148
|
+
rejectedMechanisms: [
|
|
2149
|
+
...taskPlan.rejectedMechanisms,
|
|
2150
|
+
{
|
|
2151
|
+
behaviorId: behaviorResult.behaviorId,
|
|
2152
|
+
behaviorSignature: behaviorResult.behaviorSignature,
|
|
2153
|
+
mechanism: behaviorResult.mechanism,
|
|
2154
|
+
reason:
|
|
2155
|
+
`Repeated structural verification failed: ${failedChecks
|
|
2156
|
+
.flatMap((check) => check.failures)
|
|
2157
|
+
.join('; ')
|
|
2158
|
+
.slice(0, 700)}`,
|
|
2159
|
+
source: 'verification_failure',
|
|
2160
|
+
},
|
|
2161
|
+
],
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
verified.representativeBehaviorIds.forEach((behaviorId) =>
|
|
2165
|
+
representativeVerifiedBehaviorIds.add(behaviorId));
|
|
2166
|
+
if (interactionVerification.completed) {
|
|
2167
|
+
verificationEvidence.set('interactions', operationSequence);
|
|
2168
|
+
} else {
|
|
2169
|
+
verificationEvidence.delete('interactions');
|
|
2170
|
+
}
|
|
2171
|
+
result = {
|
|
2172
|
+
stdout: JSON.stringify(interactionVerification),
|
|
2173
|
+
stderr: liveResult.stderr,
|
|
2174
|
+
};
|
|
906
2175
|
} else if (name === 'framer_read_project') {
|
|
907
|
-
readCalls += 1;
|
|
908
2176
|
const queries = Array.isArray(rawArguments.queries)
|
|
909
2177
|
? rawArguments.queries.slice(0, 50)
|
|
910
2178
|
: [];
|
|
@@ -921,7 +2189,41 @@ export function createFramerAgentToolRuntime({
|
|
|
921
2189
|
'console.log(JSON.stringify(result, null, 2));',
|
|
922
2190
|
].join('\n'),
|
|
923
2191
|
);
|
|
924
|
-
markInspection(
|
|
2192
|
+
markInspection({
|
|
2193
|
+
visual: queries.some((query) => query.type === 'screenshot'),
|
|
2194
|
+
verifies: rawArguments.verifies,
|
|
2195
|
+
});
|
|
2196
|
+
} else if (name === 'framer_query_images') {
|
|
2197
|
+
const query = String(rawArguments.query || '').trim();
|
|
2198
|
+
if (query.length < 2) {
|
|
2199
|
+
throw toolError(
|
|
2200
|
+
'FRAMER_AGENT_IMAGE_QUERY_INVALID',
|
|
2201
|
+
'A specific image-search query is required.',
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
const count = Math.max(
|
|
2205
|
+
1,
|
|
2206
|
+
Math.min(8, Math.floor(Number(rawArguments.count) || 4)),
|
|
2207
|
+
);
|
|
2208
|
+
const orientation = ['landscape', 'portrait', 'squarish']
|
|
2209
|
+
.includes(rawArguments.orientation)
|
|
2210
|
+
? rawArguments.orientation
|
|
2211
|
+
: undefined;
|
|
2212
|
+
result = await execCode(
|
|
2213
|
+
[
|
|
2214
|
+
'const result = await framer.agent.queryImages({',
|
|
2215
|
+
' source: "unsplash",',
|
|
2216
|
+
` query: ${JSON.stringify(query)},`,
|
|
2217
|
+
` count: ${count},`,
|
|
2218
|
+
...(orientation
|
|
2219
|
+
? [` orientation: ${JSON.stringify(orientation)},`]
|
|
2220
|
+
: []),
|
|
2221
|
+
'});',
|
|
2222
|
+
'console.log(JSON.stringify(result, null, 2));',
|
|
2223
|
+
].join('\n'),
|
|
2224
|
+
{ timeoutMs: 30_000 },
|
|
2225
|
+
);
|
|
2226
|
+
imageSearchCalls += 1;
|
|
925
2227
|
} else if (name === 'framer_apply_changes') {
|
|
926
2228
|
const changes = String(rawArguments.changes || '').trim();
|
|
927
2229
|
if (!changes) {
|
|
@@ -930,6 +2232,7 @@ export function createFramerAgentToolRuntime({
|
|
|
930
2232
|
'Framer Agent changes are required.',
|
|
931
2233
|
);
|
|
932
2234
|
}
|
|
2235
|
+
requireTaskPlanForMutation({ canvas: true });
|
|
933
2236
|
const pagePath = normalizePagePath(rawArguments.pagePath);
|
|
934
2237
|
result = await execCode(
|
|
935
2238
|
[
|
|
@@ -938,31 +2241,58 @@ export function createFramerAgentToolRuntime({
|
|
|
938
2241
|
].join('\n'),
|
|
939
2242
|
);
|
|
940
2243
|
mutationCalls += 1;
|
|
2244
|
+
applyChangesCalls += 1;
|
|
941
2245
|
markMutation();
|
|
2246
|
+
const parsedResult = parseStructuredOutput(result.stdout);
|
|
2247
|
+
mutationReceipts.push({
|
|
2248
|
+
sequence: operationSequence,
|
|
2249
|
+
kind: 'framer_apply_changes',
|
|
2250
|
+
pagePath,
|
|
2251
|
+
...summarizeFramerChanges(changes),
|
|
2252
|
+
renamedIds: canonicalIdMappings(parsedResult),
|
|
2253
|
+
});
|
|
942
2254
|
} else if (name === 'framer_read') {
|
|
943
|
-
readCalls += 1;
|
|
944
2255
|
result = await execCode(
|
|
945
2256
|
validateCode(rawArguments.code, {
|
|
946
2257
|
write: false,
|
|
947
2258
|
allowPublishing,
|
|
948
2259
|
}),
|
|
949
2260
|
);
|
|
950
|
-
markInspection();
|
|
2261
|
+
markInspection({ verifies: rawArguments.verifies });
|
|
951
2262
|
} else if (name === 'framer_write') {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
)
|
|
2263
|
+
const code = validateCode(rawArguments.code, {
|
|
2264
|
+
write: true,
|
|
2265
|
+
allowPublishing,
|
|
2266
|
+
});
|
|
2267
|
+
const reason = String(rawArguments.reason || '').trim();
|
|
2268
|
+
if (reason.length < 10) {
|
|
2269
|
+
throw toolError(
|
|
2270
|
+
'FRAMER_AGENT_WRITE_REASON_REQUIRED',
|
|
2271
|
+
'Explain why this operation cannot be completed with framer_apply_changes before using framer_write.',
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
2274
|
+
requireTaskPlanForMutation();
|
|
2275
|
+
result = await execCode(code);
|
|
958
2276
|
mutationCalls += 1;
|
|
2277
|
+
lowLevelWriteCalls += 1;
|
|
959
2278
|
markMutation();
|
|
2279
|
+
mutationReceipts.push({
|
|
2280
|
+
sequence: operationSequence,
|
|
2281
|
+
kind: 'framer_write',
|
|
2282
|
+
reason: reason.slice(0, 500),
|
|
2283
|
+
codeHash: crypto.createHash('sha256').update(code).digest('hex'),
|
|
2284
|
+
});
|
|
960
2285
|
} else {
|
|
961
2286
|
throw toolError(
|
|
962
2287
|
'FRAMER_AGENT_TOOL_UNKNOWN',
|
|
963
2288
|
`Unknown Framer Agent tool: ${name}`,
|
|
964
2289
|
);
|
|
965
2290
|
}
|
|
2291
|
+
const durationMs = Date.now() - startedAt;
|
|
2292
|
+
repeatedFailureFingerprint = null;
|
|
2293
|
+
repeatedFailureCount = 0;
|
|
2294
|
+
toolCall.status = 'succeeded';
|
|
2295
|
+
toolCall.durationMs = durationMs;
|
|
966
2296
|
reportActivity({
|
|
967
2297
|
kind: 'tool_active',
|
|
968
2298
|
source: 'framer-agent',
|
|
@@ -972,10 +2302,54 @@ export function createFramerAgentToolRuntime({
|
|
|
972
2302
|
name === 'framer_apply_changes' || name === 'framer_write'
|
|
973
2303
|
? 'Updated the Framer project'
|
|
974
2304
|
: 'Finished inspecting the Framer project',
|
|
975
|
-
durationMs
|
|
2305
|
+
durationMs,
|
|
976
2306
|
});
|
|
977
2307
|
return normalizedResult(result);
|
|
978
2308
|
} catch (error) {
|
|
2309
|
+
if (
|
|
2310
|
+
name === 'framer_write'
|
|
2311
|
+
&& error?.code === 'FRAMER_AGENT_CODE_UNSAFE'
|
|
2312
|
+
) {
|
|
2313
|
+
unsafeWriteFailures += 1;
|
|
2314
|
+
if (
|
|
2315
|
+
!lowLevelWriteDisabled
|
|
2316
|
+
&& unsafeWriteFailures >= MAX_UNSAFE_WRITE_FAILURES
|
|
2317
|
+
) {
|
|
2318
|
+
lowLevelWriteDisabled = true;
|
|
2319
|
+
reportActivity({
|
|
2320
|
+
kind: 'status_update',
|
|
2321
|
+
source: 'framer-agent',
|
|
2322
|
+
message:
|
|
2323
|
+
'Repeated low-level writes were rejected by the safety boundary, so I switched the run to the safer Framer change path.',
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
if (
|
|
2328
|
+
name === 'framer_write'
|
|
2329
|
+
&& error?.code === 'FRAMER_AGENT_COMMAND_FAILED'
|
|
2330
|
+
) {
|
|
2331
|
+
lowLevelWriteFailures += 1;
|
|
2332
|
+
if (
|
|
2333
|
+
!lowLevelWriteDisabled
|
|
2334
|
+
&& lowLevelWriteFailures >= MAX_LOW_LEVEL_WRITE_FAILURES
|
|
2335
|
+
) {
|
|
2336
|
+
lowLevelWriteDisabled = true;
|
|
2337
|
+
reportActivity({
|
|
2338
|
+
kind: 'status_update',
|
|
2339
|
+
source: 'framer-agent',
|
|
2340
|
+
message:
|
|
2341
|
+
'Repeated low-level Framer writes failed, so I switched the run back to semantic Framer changes and focused inspection.',
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
if (repeatedFailureFingerprint === fingerprint) {
|
|
2346
|
+
repeatedFailureCount += 1;
|
|
2347
|
+
} else {
|
|
2348
|
+
repeatedFailureFingerprint = fingerprint;
|
|
2349
|
+
repeatedFailureCount = 1;
|
|
2350
|
+
}
|
|
2351
|
+
toolCall.durationMs = Date.now() - startedAt;
|
|
2352
|
+
toolCall.errorCode = error?.code || 'FRAMER_AGENT_TOOL_FAILED';
|
|
979
2353
|
trace?.warn?.('framer_agent_tool_failed', {
|
|
980
2354
|
name,
|
|
981
2355
|
code: error?.code,
|
|
@@ -1061,31 +2435,96 @@ export function createFramerAgentToolRuntime({
|
|
|
1061
2435
|
definitions,
|
|
1062
2436
|
toolNames: definitions.map((definition) => definition.name),
|
|
1063
2437
|
preflight: validateAuthoritativeNodeIds,
|
|
2438
|
+
loadProjectGuidance,
|
|
2439
|
+
configurePromptContext,
|
|
1064
2440
|
invoke,
|
|
1065
2441
|
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
|
-
}),
|
|
2442
|
+
synchronize: inspectFinalState,
|
|
2443
|
+
inspect: inspectFinalState,
|
|
2444
|
+
finalizeCompletion,
|
|
2445
|
+
verificationRepairPrompt,
|
|
1080
2446
|
sourceHash: () => null,
|
|
1081
2447
|
hasPendingChanges: () => false,
|
|
1082
2448
|
directWorkspace: true,
|
|
1083
2449
|
summary: () => ({
|
|
1084
2450
|
projectId,
|
|
2451
|
+
projectUrl,
|
|
1085
2452
|
mutationCalls,
|
|
2453
|
+
applyChangesCalls,
|
|
2454
|
+
lowLevelWriteCalls,
|
|
2455
|
+
imageSearchCalls,
|
|
2456
|
+
interactionVerificationCalls,
|
|
2457
|
+
mutationReceipts: mutationReceipts.map((receipt) => ({
|
|
2458
|
+
...receipt,
|
|
2459
|
+
...(Array.isArray(receipt.commands)
|
|
2460
|
+
? {
|
|
2461
|
+
commands: receipt.commands.map((command) => ({
|
|
2462
|
+
...command,
|
|
2463
|
+
...(Array.isArray(command.eventActions)
|
|
2464
|
+
? {
|
|
2465
|
+
eventActions: command.eventActions.map((action) => ({
|
|
2466
|
+
...action,
|
|
2467
|
+
})),
|
|
2468
|
+
}
|
|
2469
|
+
: {}),
|
|
2470
|
+
})),
|
|
2471
|
+
}
|
|
2472
|
+
: {}),
|
|
2473
|
+
...(receipt.renamedIds
|
|
2474
|
+
? { renamedIds: { ...receipt.renamedIds } }
|
|
2475
|
+
: {}),
|
|
2476
|
+
})),
|
|
1086
2477
|
readCalls,
|
|
2478
|
+
progressCalls,
|
|
2479
|
+
toolCallCount,
|
|
2480
|
+
toolDurationMs: toolCalls.reduce(
|
|
2481
|
+
(total, call) => total + call.durationMs,
|
|
2482
|
+
0,
|
|
2483
|
+
),
|
|
2484
|
+
toolCalls: toolCalls.map((call) => ({ ...call })),
|
|
2485
|
+
workingPreferences: {
|
|
2486
|
+
focus: workingPreferences.mode,
|
|
2487
|
+
writeScope: workingPreferences.writeScope,
|
|
2488
|
+
preferBatchedMutations:
|
|
2489
|
+
workingPreferences.preferBatchedMutations,
|
|
2490
|
+
avoidRepeatedInspection:
|
|
2491
|
+
workingPreferences.avoidRepeatedInspection,
|
|
2492
|
+
expandScopeOnlyWhenNeeded:
|
|
2493
|
+
workingPreferences.expandScopeOnlyWhenNeeded,
|
|
2494
|
+
},
|
|
2495
|
+
loopProtection: {
|
|
2496
|
+
identicalFailureLimit: 5,
|
|
2497
|
+
blockedCalls: repeatedFailureStops,
|
|
2498
|
+
unsafeWriteFailureLimit: MAX_UNSAFE_WRITE_FAILURES,
|
|
2499
|
+
unsafeWriteFailures,
|
|
2500
|
+
lowLevelWriteFailureLimit: MAX_LOW_LEVEL_WRITE_FAILURES,
|
|
2501
|
+
lowLevelWriteFailures,
|
|
2502
|
+
lowLevelWriteDisabled,
|
|
2503
|
+
lowLevelWriteBlockedCalls,
|
|
2504
|
+
instructionFallbacks,
|
|
2505
|
+
},
|
|
1087
2506
|
branch: branchChange,
|
|
1088
2507
|
lastInspectionAt,
|
|
2508
|
+
lastMutationAt,
|
|
2509
|
+
taskPlan: taskPlanSnapshot(),
|
|
2510
|
+
verification: verification(),
|
|
2511
|
+
framerAgentVersion,
|
|
2512
|
+
framerGuidanceLoaded: guidanceMetadata.loaded,
|
|
2513
|
+
doctrineLoaded: promptMetadata.doctrineLoaded,
|
|
2514
|
+
guidanceHash: guidanceMetadata.contentHash,
|
|
2515
|
+
projectContextHash: guidanceMetadata.projectContextHash,
|
|
2516
|
+
visualVerificationRequired:
|
|
2517
|
+
verification().visualRequired,
|
|
2518
|
+
visualVerificationCompleted:
|
|
2519
|
+
verification().visualCompleted,
|
|
2520
|
+
guidance: {
|
|
2521
|
+
loaded: guidanceMetadata.loaded,
|
|
2522
|
+
contentHash: guidanceMetadata.contentHash,
|
|
2523
|
+
projectContextHash: guidanceMetadata.projectContextHash,
|
|
2524
|
+
files: [...guidanceMetadata.files],
|
|
2525
|
+
bytes: guidanceMetadata.bytes,
|
|
2526
|
+
},
|
|
2527
|
+
doctrine: { ...promptMetadata },
|
|
1089
2528
|
}),
|
|
1090
2529
|
fatalInfrastructureError: () => null,
|
|
1091
2530
|
waitForFatalInfrastructure: () => new Promise(() => undefined),
|