@gakim-digital/dexter-bridge 0.11.14 → 0.11.16

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.
@@ -3,24 +3,32 @@ import fs from 'node:fs';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { createRequire } from 'node:module';
5
5
  import path from 'node:path';
6
+ import jsTokens from 'js-tokens';
6
7
  import {
7
- BEHAVIOR_ACTIVATIONS,
8
- BEHAVIOR_LAYOUT_EFFECTS,
9
- BEHAVIOR_MECHANISMS,
10
- BEHAVIOR_PRESENTATIONS,
11
8
  canonicalIdMappings,
12
9
  evidenceNodeIds,
13
10
  mergeBehaviorPlans,
14
11
  normalizeBehaviorEvidence,
15
12
  normalizeBehaviorPlan,
13
+ summarizeBehaviorVerification,
16
14
  summarizeFramerChanges,
17
- verifyBehaviorEvidence,
15
+ verifyBehaviorChecks,
18
16
  } from './framerBehaviorContract.js';
19
17
 
20
18
  const require = createRequire(import.meta.url);
21
19
  const MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
22
20
  const MAX_MODEL_RESULT_BYTES = 120 * 1024;
23
21
  const MAX_CODE_FILE_CONTENT_CHARS = 200_000;
22
+ const MAX_SVG_MARKUP_CHARS = 200_000;
23
+ const MAX_SVG_ATTRIBUTES = 24;
24
+ const UNSAFE_SVG_PATTERN =
25
+ /<\s*(?:script|foreignObject|iframe)\b|\son[a-z]+\s*=|javascript:|(?:xlink:)?href\s*=\s*["']\s*(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
26
+ const MUTATION_TOOL_NAMES = new Set([
27
+ 'framer_apply_changes',
28
+ 'framer_add_svg',
29
+ 'framer_update_code_file',
30
+ 'framer_write',
31
+ ]);
24
32
  const MAX_SCREENSHOT_BYTES = 4 * 1024 * 1024;
25
33
  const MAX_SCREENSHOTS_PER_CALL = 4;
26
34
  const SCREENSHOT_FETCH_TIMEOUT_MS = 20_000;
@@ -31,8 +39,6 @@ const DEFAULT_COMMAND_TIMEOUT_MS = 5 * 60_000;
31
39
  const SESSION_TIMEOUT_MS = 10 * 60_000;
32
40
  const MAX_UNSAFE_WRITE_FAILURES = 2;
33
41
  const MAX_LOW_LEVEL_WRITE_FAILURES = 3;
34
- const MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR = 3;
35
- const MAX_INTERACTION_REPAIRS_PER_BEHAVIOR = 1;
36
42
  const MAX_GUIDANCE_BYTES = 180 * 1024;
37
43
  const MAX_GUIDANCE_FILE_BYTES = 48 * 1024;
38
44
  const PROJECT_ID_PATTERN = /^[A-Za-z0-9]{20}$/;
@@ -91,10 +97,15 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
91
97
  {
92
98
  name: 'framer_instructions',
93
99
  description:
94
- '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.',
100
+ 'Read on-demand project guides by id from the "On-demand guides" catalog, or request targeted official API documentation when an exact method or type is unclear.',
95
101
  inputSchema: {
96
102
  type: 'object',
97
103
  properties: {
104
+ guides: {
105
+ type: 'array',
106
+ maxItems: 6,
107
+ items: { type: 'string', minLength: 1, maxLength: 120 },
108
+ },
98
109
  queries: {
99
110
  type: 'array',
100
111
  maxItems: 8,
@@ -140,7 +151,7 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
140
151
  {
141
152
  name: 'framer_plan_task',
142
153
  description:
143
- '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.',
154
+ 'After inspecting the live Framer target and before the first mutation, declare your understanding of the task and the evidence you will produce. Dexter does not classify the request or choose how to build it; use any capability the Framer guidance documents.',
144
155
  inputSchema: {
145
156
  type: 'object',
146
157
  properties: {
@@ -167,147 +178,27 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
167
178
  type: 'array',
168
179
  maxItems: 20,
169
180
  description:
170
- 'Required for interaction work. Describe observable behavior independently from its implementation.',
181
+ 'Interactive or animated behaviors the result must have, such as links, hover or tap effects, overlays, variants, appear, loop, or ticker motion. Declaring behaviors adds interaction verification.',
171
182
  items: {
172
183
  type: 'object',
173
184
  properties: {
174
185
  id: { type: 'string', minLength: 1, maxLength: 120 },
175
- description: { type: 'string', minLength: 5, maxLength: 500 },
176
- triggerDescription: {
177
- type: 'string',
178
- minLength: 3,
179
- maxLength: 300,
180
- },
181
- activation: {
182
- type: 'string',
183
- enum: BEHAVIOR_ACTIVATIONS,
184
- },
185
- presentation: {
186
- type: 'string',
187
- enum: BEHAVIOR_PRESENTATIONS,
188
- },
189
- stateChange: { type: 'boolean' },
190
- layoutEffect: {
191
- type: 'string',
192
- enum: BEHAVIOR_LAYOUT_EFFECTS,
193
- },
194
- reversible: { type: 'boolean' },
195
- repeatable: { type: 'boolean' },
196
- expectedTargets: {
197
- type: 'integer',
198
- minimum: 1,
199
- maximum: 50,
200
- },
201
- expectedContent: {
202
- type: 'array',
203
- maxItems: 20,
204
- items: { type: 'string', minLength: 1, maxLength: 500 },
205
- description:
206
- 'Optional exact literal text that must exist in the interaction target. Do not put behavioral descriptions or prose assertions here.',
207
- },
208
- expectedDestinations: {
209
- type: 'array',
210
- maxItems: 20,
211
- items: { type: 'string', minLength: 1, maxLength: 500 },
212
- description:
213
- 'Required for navigation behavior. Use exact routes or URLs such as /contact.',
214
- },
215
- initialState: {
216
- type: 'string',
217
- minLength: 1,
218
- maxLength: 300,
219
- },
220
- activatedState: {
221
- type: 'string',
222
- minLength: 1,
223
- maxLength: 300,
224
- },
225
- },
226
- required: [
227
- 'id',
228
- 'description',
229
- 'triggerDescription',
230
- 'activation',
231
- 'presentation',
232
- 'stateChange',
233
- 'layoutEffect',
234
- 'reversible',
235
- 'repeatable',
236
- 'expectedTargets',
237
- 'initialState',
238
- 'activatedState',
239
- ],
240
- additionalProperties: false,
241
- },
242
- },
243
- mechanisms: {
244
- type: 'array',
245
- maxItems: 20,
246
- description:
247
- 'Required for every declared behavior. Choose the mechanism from live project capabilities and explain why it satisfies the behavior.',
248
- items: {
249
- type: 'object',
250
- properties: {
251
- behaviorId: { type: 'string', minLength: 1, maxLength: 120 },
252
- mechanism: {
253
- type: 'string',
254
- enum: BEHAVIOR_MECHANISMS,
255
- },
256
- rationale: {
186
+ description: {
257
187
  type: 'string',
258
- minLength: 10,
259
- maxLength: 800,
188
+ minLength: 5,
189
+ maxLength: 500,
190
+ description: 'What the visitor should experience.',
260
191
  },
261
- representativeFirst: { type: 'boolean', default: true },
262
192
  targetNodeIds: {
263
193
  type: 'array',
264
194
  maxItems: 50,
265
195
  uniqueItems: true,
266
- items: {
267
- type: 'string',
268
- minLength: 1,
269
- maxLength: 200,
270
- },
196
+ items: { type: 'string', minLength: 1, maxLength: 200 },
271
197
  description:
272
- 'All intended target node IDs. Required when expectedTargets is greater than one.',
273
- },
274
- representativeTargetNodeId: {
275
- type: 'string',
276
- minLength: 1,
277
- maxLength: 200,
278
- description:
279
- 'The single target to implement and verify before fan-out.',
280
- },
281
- },
282
- required: ['behaviorId', 'mechanism', 'rationale'],
283
- additionalProperties: false,
284
- },
285
- },
286
- rejectedMechanisms: {
287
- type: 'array',
288
- maxItems: 30,
289
- description:
290
- 'Mechanisms already disproven by user-observed failure or prior verification. Do not silently reuse them.',
291
- items: {
292
- type: 'object',
293
- properties: {
294
- behaviorId: { type: 'string', minLength: 1, maxLength: 120 },
295
- behaviorSignature: {
296
- type: 'string',
297
- minLength: 1,
298
- maxLength: 128,
299
- },
300
- mechanism: {
301
- type: 'string',
302
- enum: BEHAVIOR_MECHANISMS,
303
- },
304
- reason: { type: 'string', minLength: 5, maxLength: 800 },
305
- source: {
306
- type: 'string',
307
- enum: ['user_reported_failure', 'verification_failure'],
198
+ 'Every node that should carry this behavior once the work is done. Update the plan if they change, for example after replacing nodes.',
308
199
  },
309
200
  },
310
- required: ['mechanism', 'reason'],
201
+ required: ['id', 'description'],
311
202
  additionalProperties: false,
312
203
  },
313
204
  },
@@ -319,7 +210,7 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
319
210
  {
320
211
  name: 'framer_verify_interactions',
321
212
  description:
322
- 'Verify the observable behavior contract from live Framer evidence. Results are verified, contradicted, or unknown. Unknown evidence never proves failure and must not trigger mutation. Native links and component-control navigation are verified from destinations, not onTap. You may add a grounded semanticAssessment; it cannot override a deterministic contradiction or authorize writes.',
213
+ 'Confirm planned behaviors against the live project. For each target node, name the Framer property that carries the behavior (for example loopEffect, hoverEffect, appearEffect, link, or onTap) and optionally a literal the value must contain (such as a route or SHOW_OVERLAY). Results: verified (the property is set and matches), contradicted (the property holds a different value), or unknown (the property was not found on the node). Unknown never blocks your work; it is reported as unverified. Checks accumulate across calls, so you can verify targets on several pages.',
323
214
  inputSchema: {
324
215
  type: 'object',
325
216
  properties: {
@@ -327,103 +218,16 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
327
218
  checks: {
328
219
  type: 'array',
329
220
  minItems: 1,
330
- maxItems: 20,
221
+ maxItems: 50,
331
222
  items: {
332
223
  type: 'object',
333
224
  properties: {
334
- description: {
335
- type: 'string',
336
- minLength: 1,
337
- maxLength: 300,
338
- },
339
- behaviorId: {
340
- type: 'string',
341
- minLength: 1,
342
- maxLength: 120,
343
- },
344
- triggerNodeId: {
345
- type: 'string',
346
- minLength: 1,
347
- maxLength: 200,
348
- },
349
- event: {
350
- type: 'string',
351
- minLength: 1,
352
- maxLength: 80,
353
- },
354
- expectedDestination: {
355
- type: 'string',
356
- minLength: 1,
357
- maxLength: 500,
358
- },
359
- targetNodeId: {
360
- type: 'string',
361
- minLength: 1,
362
- maxLength: 200,
363
- },
364
- componentNodeId: {
365
- type: 'string',
366
- minLength: 1,
367
- maxLength: 200,
368
- },
369
- instanceNodeIds: {
370
- type: 'array',
371
- maxItems: 50,
372
- uniqueItems: true,
373
- items: {
374
- type: 'string',
375
- minLength: 1,
376
- maxLength: 200,
377
- },
378
- },
379
- openVariantNodeId: {
380
- type: 'string',
381
- minLength: 1,
382
- maxLength: 200,
383
- },
384
- closedVariantNodeId: {
385
- type: 'string',
386
- minLength: 1,
387
- maxLength: 200,
388
- },
389
- openAnswerNodeId: {
390
- type: 'string',
391
- minLength: 1,
392
- maxLength: 200,
393
- },
394
- closedAnswerNodeId: {
395
- type: 'string',
396
- minLength: 1,
397
- maxLength: 200,
398
- },
399
- semanticAssessment: {
400
- type: 'object',
401
- properties: {
402
- verdict: {
403
- type: 'string',
404
- enum: ['verified', 'contradicted', 'unknown'],
405
- },
406
- rationale: {
407
- type: 'string',
408
- minLength: 10,
409
- maxLength: 1200,
410
- },
411
- evidenceNodeIds: {
412
- type: 'array',
413
- maxItems: 50,
414
- uniqueItems: true,
415
- items: {
416
- type: 'string',
417
- minLength: 1,
418
- maxLength: 200,
419
- },
420
- },
421
- },
422
- required: ['verdict', 'rationale', 'evidenceNodeIds'],
423
- additionalProperties: false,
424
- },
225
+ behaviorId: { type: 'string', minLength: 1, maxLength: 120 },
226
+ nodeId: { type: 'string', minLength: 1, maxLength: 200 },
227
+ property: { type: 'string', minLength: 1, maxLength: 120 },
228
+ contains: { type: 'string', minLength: 1, maxLength: 500 },
425
229
  },
426
- required: ['behaviorId'],
230
+ required: ['behaviorId', 'nodeId', 'property'],
427
231
  additionalProperties: false,
428
232
  },
429
233
  },
@@ -477,6 +281,33 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
477
281
  additionalProperties: false,
478
282
  },
479
283
  },
284
+ {
285
+ name: 'framer_add_svg',
286
+ description:
287
+ 'Create an SVG layer from markup you write and place it inside a parent node. Use it for geometric and decorative artwork such as line work, starbursts, rings, sparkles, wireframe shapes, dividers, and simple marks. Use icon sets for icons, real text for text, and images for photos. Returns the new SVG node id; check it in a screenshot. To revise its markup later, use framer_write with setAttributes({ svg }) on that node.',
288
+ inputSchema: {
289
+ type: 'object',
290
+ properties: {
291
+ svg: {
292
+ type: 'string',
293
+ minLength: 1,
294
+ maxLength: MAX_SVG_MARKUP_CHARS,
295
+ description:
296
+ 'One self-contained <svg> element with a viewBox. No scripts, event handlers, foreignObject, or external references.',
297
+ },
298
+ parentId: { type: 'string', minLength: 1, maxLength: 200 },
299
+ index: { type: 'integer', minimum: 0 },
300
+ name: { type: 'string', minLength: 1, maxLength: 120 },
301
+ attributes: {
302
+ type: 'object',
303
+ description:
304
+ 'Optional Framer layer attributes for the SVG node, for example {"position":"absolute","top":"40px","left":"0px","width":"320px","height":"320px","rotation":12,"opacity":0.6}. Defaults to position relative at the SVG\'s own size.',
305
+ },
306
+ },
307
+ required: ['svg', 'parentId'],
308
+ additionalProperties: false,
309
+ },
310
+ },
480
311
  {
481
312
  name: 'framer_read_code_file',
482
313
  description:
@@ -646,6 +477,50 @@ function readGuidanceFile(projectRoot, relativePath, remainingBytes, section) {
646
477
  };
647
478
  }
648
479
 
480
+ const CORE_GUIDE_SECTIONS = [
481
+ { file: 'how-projects-work', section: 'Scope Types' },
482
+ { file: 'how-projects-work', section: 'Replicas' },
483
+ { file: 'how-projects-work', section: 'Transforms' },
484
+ { file: 'implementation-strategy', section: 'Determining Strategy' },
485
+ { file: 'implementation-strategy', section: 'Edit Strategy' },
486
+ { file: 'implementation-strategy', section: 'Update Loop' },
487
+ ];
488
+ const ON_DEMAND_GUIDE_FILES = [
489
+ { file: 'how-projects-work', sections: true },
490
+ { file: 'implementation-strategy', sections: true },
491
+ { file: 'core-examples', sections: false },
492
+ ];
493
+
494
+ // Lists the topic guides that are not part of every prompt, keyed by
495
+ // "file#Section" (or "file" for a whole file), from the files on disk.
496
+ function onDemandGuideCatalog(projectRoot) {
497
+ const catalog = new Map();
498
+ for (const { file, sections } of ON_DEMAND_GUIDE_FILES) {
499
+ const relativePath = `prompt/${file}.md`;
500
+ const absolutePath = path.join(projectRoot, relativePath);
501
+ if (!fs.existsSync(absolutePath)) continue;
502
+ if (!sections) {
503
+ catalog.set(file, { root: projectRoot, path: relativePath });
504
+ continue;
505
+ }
506
+ const headings = fs.readFileSync(absolutePath, 'utf8')
507
+ .split('\n')
508
+ .filter((line) => line.startsWith('## '))
509
+ .map((line) => line.slice(3).trim())
510
+ .filter((heading) => !CORE_GUIDE_SECTIONS.some(
511
+ (core) => core.file === file && core.section === heading,
512
+ ));
513
+ for (const heading of headings) {
514
+ catalog.set(`${file}#${heading}`, {
515
+ root: projectRoot,
516
+ path: relativePath,
517
+ section: heading,
518
+ });
519
+ }
520
+ }
521
+ return catalog;
522
+ }
523
+
649
524
  function markdownSection(markdown, heading) {
650
525
  const lines = markdown.split('\n');
651
526
  const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
@@ -742,7 +617,7 @@ function normalizeCodeFilePath(value) {
742
617
  return filePath;
743
618
  }
744
619
 
745
- function codeFileResultError(result, fallbackCode, fallbackMessage) {
620
+ function structuredResultError(result, fallbackCode, fallbackMessage) {
746
621
  const error = isRecord(result?.error) ? result.error : {};
747
622
  const code =
748
623
  typeof error.code === 'string' && error.code.trim()
@@ -753,7 +628,7 @@ function codeFileResultError(result, fallbackCode, fallbackMessage) {
753
628
  ? error.message.trim().slice(0, 2_000)
754
629
  : fallbackMessage;
755
630
  const statusCode =
756
- code === 'FRAMER_CODE_FILE_NOT_FOUND'
631
+ code === 'FRAMER_CODE_FILE_NOT_FOUND' || code === 'FRAMER_SVG_PARENT_NOT_FOUND'
757
632
  ? 404
758
633
  : code === 'FRAMER_CODE_FILE_VERSION_CONFLICT'
759
634
  ? 409
@@ -779,7 +654,7 @@ function normalizeVerificationValues(values, allowed) {
779
654
  .filter((value) => allowed.includes(value));
780
655
  }
781
656
 
782
- function normalizeTaskPlan(rawArguments, inheritedRejectedMechanisms = []) {
657
+ function normalizeTaskPlan(rawArguments) {
783
658
  const summary = String(rawArguments.summary || '').trim().slice(0, 1000);
784
659
  const domains = normalizeVerificationValues(
785
660
  rawArguments.domains,
@@ -798,19 +673,7 @@ function normalizeTaskPlan(rawArguments, inheritedRejectedMechanisms = []) {
798
673
  // The model's declared verification is authoritative; domains describe the
799
674
  // work and never add checks on their own.
800
675
  const verification = new Set(['structural', ...requestedVerification]);
801
- const behaviorPlan = normalizeBehaviorPlan(
802
- rawArguments,
803
- inheritedRejectedMechanisms,
804
- );
805
- if (
806
- domains.includes('interactions')
807
- && behaviorPlan.behaviors.length === 0
808
- ) {
809
- throw toolError(
810
- 'FRAMER_BEHAVIOR_CONTRACT_REQUIRED',
811
- 'Interaction work requires observable behaviors and one compatible mechanism decision per behavior.',
812
- );
813
- }
676
+ const behaviorPlan = normalizeBehaviorPlan(rawArguments);
814
677
  if (behaviorPlan.behaviors.length > 0) {
815
678
  if (!domains.includes('interactions')) domains.push('interactions');
816
679
  verification.add('interactions');
@@ -936,6 +799,28 @@ function authoritativeNodeIdsFromAssignment(assignment) {
936
799
  ]);
937
800
  }
938
801
 
802
+ const LITERAL_TOKEN_TYPES = new Set([
803
+ 'StringLiteral',
804
+ 'NoSubstitutionTemplate',
805
+ 'TemplateHead',
806
+ 'TemplateMiddle',
807
+ 'TemplateTail',
808
+ 'SingleLineComment',
809
+ 'MultiLineComment',
810
+ 'RegularExpressionLiteral',
811
+ ]);
812
+
813
+ // Blanks string, template, comment, and regex text so data such as image URLs
814
+ // or product copy is not mistaken for a forbidden global. Template
815
+ // substitutions stay visible because they are code.
816
+ function codeOutsideLiterals(code) {
817
+ let visible = '';
818
+ for (const token of jsTokens(code)) {
819
+ visible += LITERAL_TOKEN_TYPES.has(token.type) ? ' ' : token.value;
820
+ }
821
+ return visible;
822
+ }
823
+
939
824
  function validateCode(value, { write = false, allowPublishing = false } = {}) {
940
825
  const code = String(value || '').trim();
941
826
  if (!code) {
@@ -944,7 +829,7 @@ function validateCode(value, { write = false, allowPublishing = false } = {}) {
944
829
  'Framer Agent code is required.',
945
830
  );
946
831
  }
947
- if (UNSAFE_EXEC_PATTERN.test(code)) {
832
+ if (UNSAFE_EXEC_PATTERN.test(codeOutsideLiterals(code))) {
948
833
  throw toolError(
949
834
  'FRAMER_AGENT_CODE_UNSAFE',
950
835
  'Framer Agent code may use only the provided framer, state, and console globals.',
@@ -968,6 +853,35 @@ function validateCode(value, { write = false, allowPublishing = false } = {}) {
968
853
  return code;
969
854
  }
970
855
 
856
+ function normalizeSvgMarkup(value) {
857
+ const svg = String(value || '').trim();
858
+ if (!/^<svg\b[\s\S]*<\/svg>$/i.test(svg) || svg.length > MAX_SVG_MARKUP_CHARS) {
859
+ throw toolError(
860
+ 'FRAMER_SVG_INVALID',
861
+ `Provide one <svg>…</svg> element of at most ${MAX_SVG_MARKUP_CHARS} characters.`,
862
+ );
863
+ }
864
+ if (UNSAFE_SVG_PATTERN.test(svg)) {
865
+ throw toolError(
866
+ 'FRAMER_SVG_UNSAFE',
867
+ 'SVG markup must be self-contained: no scripts, event handlers, foreignObject, or external references.',
868
+ );
869
+ }
870
+ return svg;
871
+ }
872
+
873
+ function normalizeSvgAttributes(value) {
874
+ if (value === undefined) return {};
875
+ if (!isRecord(value) || Object.keys(value).length > MAX_SVG_ATTRIBUTES) {
876
+ throw toolError(
877
+ 'FRAMER_SVG_ATTRIBUTES_INVALID',
878
+ `attributes must be an object with at most ${MAX_SVG_ATTRIBUTES} Framer layer attributes.`,
879
+ );
880
+ }
881
+ const { svg: _ignored, ...attributes } = value;
882
+ return attributes;
883
+ }
884
+
971
885
  function extractJsonArray(value) {
972
886
  const text = String(value || '').trim();
973
887
  const start = text.indexOf('[');
@@ -1246,32 +1160,9 @@ export function createFramerAgentToolRuntime({
1246
1160
  );
1247
1161
  }
1248
1162
  const allowPublishing = assignment?.context?.permissions?.publish === true;
1249
- const inheritedRejectedMechanisms =
1250
- assignment?.context?.repairContext?.rejectedMechanisms
1251
- || assignment?.repairContext?.rejectedMechanisms
1252
- || [];
1253
- const authorizedInstanceContainerIds = uniqueStrings(
1254
- assignment?.context?.permissions?.scopedComponentArtifacts
1255
- ?.instanceContainerNodeIds
1256
- || assignment?.context?.contextGrant?.writableNodeIds
1257
- || [],
1258
- );
1259
1163
  const activePagePath = normalizePagePath(
1260
1164
  assignment?.context?.activePagePath,
1261
1165
  );
1262
- const semanticVerifierMode =
1263
- ['off', 'shadow', 'enforce'].includes(
1264
- String(
1265
- assignment?.context?.harnessAuthority?.semanticVerifierMode
1266
- || env.DEXTER_FRAMER_SEMANTIC_VERIFIER_MODE
1267
- || '',
1268
- ).trim(),
1269
- )
1270
- ? String(
1271
- assignment?.context?.harnessAuthority?.semanticVerifierMode
1272
- || env.DEXTER_FRAMER_SEMANTIC_VERIFIER_MODE,
1273
- ).trim()
1274
- : 'enforce';
1275
1166
  const authoritativeNodeIds =
1276
1167
  authoritativeNodeIdsFromAssignment(assignment);
1277
1168
  let sessionId = null;
@@ -1303,20 +1194,10 @@ export function createFramerAgentToolRuntime({
1303
1194
  let lastMutationAt = null;
1304
1195
  let screenshotCalls = 0;
1305
1196
  let interactionVerificationCalls = 0;
1306
- let interactionVerification = {
1307
- completed: false,
1308
- verificationLevel: 'unknown',
1309
- checks: [],
1310
- behaviors: [],
1311
- budgetExhaustedBehaviorIds: [],
1312
- verifiedAt: null,
1313
- };
1314
- const representativeVerifiedBehaviorIds = new Set();
1315
- const representativeMutatedBehaviorIds = new Set();
1316
- const representativeRepairNodeIds = new Set();
1317
- const representativeRepairBehaviorByNode = new Map();
1318
- const representativeEvidenceBehaviorByNode = new Map();
1319
- const behaviorVerificationAttempts = new Map();
1197
+ // Latest result per behavior/node/property, with the operation sequence it
1198
+ // was observed at, so checks from several calls (or pages) accumulate.
1199
+ const interactionCheckResults = new Map();
1200
+ let interactionVerifiedAt = null;
1320
1201
  const mutationReceipts = [];
1321
1202
  let lastScreenshotSequence = 0;
1322
1203
  let lastScreenshotAttemptSequence = 0;
@@ -1326,6 +1207,8 @@ export function createFramerAgentToolRuntime({
1326
1207
  const screenshotFailures = [];
1327
1208
  let taskPlan = null;
1328
1209
  const verificationEvidence = new Map();
1210
+ let guideCatalog = new Map();
1211
+ const guidesRead = [];
1329
1212
  let guidanceMetadata = {
1330
1213
  loaded: false,
1331
1214
  contentHash: null,
@@ -1334,6 +1217,7 @@ export function createFramerAgentToolRuntime({
1334
1217
  bytes: 0,
1335
1218
  };
1336
1219
  let promptMetadata = {
1220
+ promptBytes: 0,
1337
1221
  doctrineLoaded: false,
1338
1222
  doctrineVersion: null,
1339
1223
  };
@@ -1343,6 +1227,7 @@ export function createFramerAgentToolRuntime({
1343
1227
  const framerAgentVersion = installedFramerAgentVersion();
1344
1228
  let operationSequence = 0;
1345
1229
  let lastMutationSequence = 0;
1230
+ let lastCodeMutationSequence = 0;
1346
1231
  let lastInspectionSequence = 0;
1347
1232
  const abortController = new AbortController();
1348
1233
 
@@ -1649,6 +1534,10 @@ export function createFramerAgentToolRuntime({
1649
1534
  if (!fs.existsSync(projectRoot)) {
1650
1535
  return { content: '', ...guidanceMetadata };
1651
1536
  }
1537
+ // Every run gets the guidance an edit always needs: principles, command
1538
+ // syntax, tool APIs, scope and replica rules, and the edit loop. Topic
1539
+ // guides (CMS, components, creation strategy, examples, ...) are listed
1540
+ // in a catalog and read on demand through framer_instructions.
1652
1541
  const requestedFiles = [
1653
1542
  { root: skillRoot, path: 'SKILL.md', label: 'SKILL.md' },
1654
1543
  { root: projectRoot, path: 'index.md', label: 'project/index.md' },
@@ -1657,24 +1546,19 @@ export function createFramerAgentToolRuntime({
1657
1546
  path: 'project-inventory.md',
1658
1547
  label: 'project/project-inventory.md',
1659
1548
  },
1660
- { root: projectRoot, path: 'prompt/core-principles.md' },
1661
- { root: projectRoot, path: 'prompt/core-examples.md' },
1662
- { root: projectRoot, path: 'prompt/implementation-strategy.md' },
1663
- { root: projectRoot, path: 'prompt/updating-the-project.md' },
1664
- { root: projectRoot, path: 'prompt/tools.md' },
1665
1549
  { root: projectRoot, path: 'prompt/overview.md' },
1550
+ { root: projectRoot, path: 'prompt/core-principles.md' },
1666
1551
  { root: projectRoot, path: 'prompt/guardrails.md' },
1667
1552
  { root: projectRoot, path: 'prompt/critical-reminders.md' },
1668
- // The byte cap clips how-projects-work.md before its 3D transform
1669
- // guidance, so that section loads on its own first.
1670
- {
1671
- root: projectRoot,
1672
- path: 'prompt/how-projects-work.md',
1673
- section: 'Transforms',
1674
- label: 'prompt/how-projects-work.md#transforms',
1675
- },
1676
1553
  { root: projectRoot, path: 'prompt/design-rules.md' },
1677
- { root: projectRoot, path: 'prompt/how-projects-work.md' },
1554
+ { root: projectRoot, path: 'prompt/updating-the-project.md' },
1555
+ { root: projectRoot, path: 'prompt/tools.md' },
1556
+ ...CORE_GUIDE_SECTIONS.map(({ file, section }) => ({
1557
+ root: projectRoot,
1558
+ path: `prompt/${file}.md`,
1559
+ section,
1560
+ label: `prompt/${file}.md#${section}`,
1561
+ })),
1678
1562
  ];
1679
1563
  const files = [];
1680
1564
  let remainingBytes = MAX_GUIDANCE_BYTES;
@@ -1693,9 +1577,19 @@ export function createFramerAgentToolRuntime({
1693
1577
  remainingBytes -= Buffer.byteLength(loaded.content, 'utf8');
1694
1578
  if (remainingBytes <= 0) break;
1695
1579
  }
1696
- const content = files
1697
- .map((file) => `## ${file.path}\n\n${file.content}`)
1698
- .join('\n\n');
1580
+ guideCatalog = onDemandGuideCatalog(projectRoot);
1581
+ const catalogText = guideCatalog.size > 0
1582
+ ? [
1583
+ '## On-demand guides',
1584
+ '',
1585
+ 'Read a guide with framer_instructions({ guides: [id] }) before work that depends on it.',
1586
+ ...[...guideCatalog.keys()].map((id) => `- ${id}`),
1587
+ ].join('\n')
1588
+ : '';
1589
+ const content = [
1590
+ ...files.map((file) => `## ${file.path}\n\n${file.content}`),
1591
+ ...(catalogText ? [catalogText] : []),
1592
+ ].join('\n\n');
1699
1593
  const inventory = files.find(
1700
1594
  (file) => file.path.endsWith('project-inventory.md'),
1701
1595
  )?.content || '';
@@ -1716,8 +1610,10 @@ export function createFramerAgentToolRuntime({
1716
1610
  const configurePromptContext = ({
1717
1611
  doctrineLoaded = false,
1718
1612
  doctrineVersion = null,
1613
+ promptBytes = 0,
1719
1614
  } = {}) => {
1720
1615
  promptMetadata = {
1616
+ promptBytes: Math.max(0, Number(promptBytes) || 0),
1721
1617
  doctrineLoaded: doctrineLoaded === true,
1722
1618
  doctrineVersion:
1723
1619
  typeof doctrineVersion === 'string' && doctrineVersion.trim()
@@ -1858,9 +1754,14 @@ export function createFramerAgentToolRuntime({
1858
1754
  return result;
1859
1755
  };
1860
1756
 
1757
+ // Selected layers that do not resolve to native nodes (stale selections,
1758
+ // aliases, layers inside component instances) no longer stop the run. The
1759
+ // model is told which targets to locate by name, and the reason is kept for
1760
+ // debugging.
1761
+ let unresolvedTargets = [];
1861
1762
  const validateAuthoritativeNodeIds = async () => {
1862
1763
  await ensureSession();
1863
- if (authoritativeNodeIds.length === 0) return;
1764
+ if (authoritativeNodeIds.length === 0) return { unresolvedTargets };
1864
1765
  if (nodeValidationPromise) return nodeValidationPromise;
1865
1766
  nodeValidationPromise = (async () => {
1866
1767
  reportActivity({
@@ -1872,9 +1773,13 @@ export function createFramerAgentToolRuntime({
1872
1773
  [
1873
1774
  `const requestedIds = ${JSON.stringify(authoritativeNodeIds)};`,
1874
1775
  'const resolved = await Promise.all(requestedIds.map(async (requestedId) => {',
1875
- ' const node = await framer.agent.getNode({ id: requestedId },',
1876
- ` { pagePath: ${JSON.stringify(activePagePath)} }).catch(() => null);`,
1877
- ' return { requestedId, resolvedId: node?.id ?? null };',
1776
+ ' try {',
1777
+ ' const node = await framer.agent.getNode({ id: requestedId },',
1778
+ ` { pagePath: ${JSON.stringify(activePagePath)} });`,
1779
+ ' return { requestedId, resolvedId: node?.id ?? null, error: node ? null : "not found on the active page" };',
1780
+ ' } catch (error) {',
1781
+ ' return { requestedId, resolvedId: null, error: String(error?.message || error).slice(0, 200) };',
1782
+ ' }',
1878
1783
  '}));',
1879
1784
  'console.log(JSON.stringify({ resolved }));',
1880
1785
  ].join('\n'),
@@ -1884,26 +1789,30 @@ export function createFramerAgentToolRuntime({
1884
1789
  const resolved = Array.isArray(parsed?.resolved)
1885
1790
  ? parsed.resolved
1886
1791
  : [];
1887
- const invalid = authoritativeNodeIds.filter((requestedId) => {
1792
+ unresolvedTargets = authoritativeNodeIds.flatMap((requestedId) => {
1888
1793
  const match = resolved.find(
1889
1794
  (entry) =>
1890
1795
  isRecord(entry)
1891
1796
  && entry.requestedId === requestedId,
1892
1797
  );
1893
- return !match || match.resolvedId !== requestedId;
1798
+ if (match?.resolvedId === requestedId) return [];
1799
+ return [{
1800
+ id: requestedId,
1801
+ reason: !match
1802
+ ? 'not checked'
1803
+ : match.resolvedId
1804
+ ? `resolves to ${match.resolvedId}`
1805
+ : String(match.error || 'not found on the active page'),
1806
+ }];
1894
1807
  });
1895
- if (invalid.length > 0) {
1896
- throw toolError(
1897
- 'FRAMER_AGENT_NODE_ID_INVALID',
1898
- `Dexter received Framer targets that do not resolve to exact native node IDs: ${invalid.join(', ')}. Re-select the affected layers in Framer and try again.`,
1899
- 409,
1900
- );
1901
- }
1902
1808
  reportActivity({
1903
1809
  kind: 'status_update',
1904
1810
  source: 'framer-agent',
1905
- message: 'The selected Framer layers are ready.',
1811
+ message: unresolvedTargets.length > 0
1812
+ ? 'Some selected layers could not be matched exactly, so Dexter will locate them by name.'
1813
+ : 'The selected Framer layers are ready.',
1906
1814
  });
1815
+ return { unresolvedTargets };
1907
1816
  })().catch((error) => {
1908
1817
  nodeValidationPromise = null;
1909
1818
  throw error;
@@ -1954,9 +1863,10 @@ export function createFramerAgentToolRuntime({
1954
1863
  }
1955
1864
  };
1956
1865
 
1957
- const markMutation = () => {
1866
+ const markMutation = ({ code = false } = {}) => {
1958
1867
  operationSequence += 1;
1959
1868
  lastMutationSequence = operationSequence;
1869
+ if (code) lastCodeMutationSequence = operationSequence;
1960
1870
  lastMutationAt = new Date().toISOString();
1961
1871
  };
1962
1872
 
@@ -1972,30 +1882,35 @@ export function createFramerAgentToolRuntime({
1972
1882
  ? {
1973
1883
  behaviors: taskPlan.behaviors.map((behavior) => ({
1974
1884
  ...behavior,
1975
- expectedContent: [...behavior.expectedContent],
1976
- expectedDestinations: [...behavior.expectedDestinations],
1977
- })),
1978
- mechanisms: taskPlan.mechanisms.map((decision) => ({
1979
- ...decision,
1980
- targetNodeIds: [...decision.targetNodeIds],
1981
- })),
1982
- rejectedMechanisms: taskPlan.rejectedMechanisms.map((entry) => ({
1983
- ...entry,
1984
- })),
1985
- compatibility: taskPlan.compatibility.map((entry) => ({
1986
- ...entry,
1987
- requiredCapabilities: [...entry.requiredCapabilities],
1885
+ targetNodeIds: [...behavior.targetNodeIds],
1988
1886
  })),
1989
1887
  }
1990
1888
  : {}),
1991
1889
  };
1992
1890
  };
1891
+ // Only checks observed after the latest mutation describe the current project.
1892
+ const freshInteractionChecks = () =>
1893
+ [...interactionCheckResults.values()]
1894
+ .filter((entry) => entry.sequence > lastMutationSequence)
1895
+ .map((entry) => entry.result);
1896
+ const interactionSummary = () =>
1897
+ summarizeBehaviorVerification(taskPlan, freshInteractionChecks());
1898
+ const interactionStatus = () => {
1899
+ if (!taskPlan?.behaviors?.length || mutationCalls === 0) return 'not_required';
1900
+ return interactionSummary().status;
1901
+ };
1993
1902
  const synchronized = () =>
1994
1903
  mutationCalls === 0 || lastInspectionSequence > lastMutationSequence;
1995
1904
  const verificationCheckCompleted = (check) => {
1996
1905
  if (mutationCalls === 0) return true;
1997
1906
  if (check === 'structural') return synchronized();
1998
- return Number(verificationEvidence.get(check) || 0) > lastMutationSequence;
1907
+ if (check === 'interactions') {
1908
+ return ['verified', 'not_required'].includes(interactionStatus());
1909
+ }
1910
+ // Canvas edits cannot change a code file, so only code edits stale it.
1911
+ const staleAfter =
1912
+ check === 'code' ? lastCodeMutationSequence : lastMutationSequence;
1913
+ return Number(verificationEvidence.get(check) || 0) > staleAfter;
1999
1914
  };
2000
1915
  const visualVerificationRequired = () =>
2001
1916
  requiredVerificationChecks().includes('visual');
@@ -2013,12 +1928,19 @@ export function createFramerAgentToolRuntime({
2013
1928
  const requiredChecks = () => [
2014
1929
  ...new Set(['structural', ...requiredVerificationChecks()]),
2015
1930
  ];
1931
+ // Checks the model attempted but that live evidence could not confirm. They
1932
+ // are reported as unverified and do not fail the run.
1933
+ const unavailableChecks = () => [
1934
+ ...(visualVerificationStatus() === 'unavailable' ? ['visual'] : []),
1935
+ ...(interactionStatus() === 'unverified' ? ['interactions'] : []),
1936
+ ];
2016
1937
  const missingVerificationChecks = () => {
2017
1938
  if (mutationCalls === 0) return [];
2018
1939
  const missing = [];
2019
1940
  if (!taskPlan) missing.push('task-plan');
1941
+ const unavailable = unavailableChecks();
2020
1942
  for (const check of requiredChecks()) {
2021
- if (check === 'visual' && visualVerificationUnavailable()) continue;
1943
+ if (unavailable.includes(check)) continue;
2022
1944
  if (!verificationCheckCompleted(check)) missing.push(check);
2023
1945
  }
2024
1946
  return missing;
@@ -2046,48 +1968,17 @@ export function createFramerAgentToolRuntime({
2046
1968
  ? {
2047
1969
  interactionVerificationCalls,
2048
1970
  interactionVerification: {
2049
- completed: verificationCheckCompleted('interactions'),
2050
- verificationLevel:
2051
- verificationCheckCompleted('interactions')
2052
- ? interactionVerification.verificationLevel
2053
- : 'unknown',
2054
- checks: interactionVerification.checks.map((check) => ({
2055
- ...check,
2056
- contradictions: [...check.contradictions],
2057
- unknowns: [...check.unknowns],
2058
- observations: check.observations.map((observation) => ({
2059
- ...observation,
2060
- })),
2061
- nextSafeActions: [...check.nextSafeActions],
2062
- ...(check.semanticAssessment
2063
- ? {
2064
- semanticAssessment: {
2065
- ...check.semanticAssessment,
2066
- evidenceNodeIds: [
2067
- ...check.semanticAssessment.evidenceNodeIds,
2068
- ],
2069
- },
2070
- }
2071
- : {}),
2072
- })),
2073
- behaviors: interactionVerification.behaviors.map((behavior) => ({
2074
- ...behavior,
2075
- contradictions: [...behavior.contradictions],
2076
- unknowns: [...behavior.unknowns],
2077
- })),
2078
- semanticVerifierMode,
2079
- budgetExhaustedBehaviorIds: [
2080
- ...interactionVerification.budgetExhaustedBehaviorIds,
2081
- ],
2082
- verifiedAt: interactionVerification.verifiedAt,
1971
+ status: interactionStatus(),
1972
+ behaviors: taskPlan?.behaviors?.length ? interactionSummary().behaviors : [],
1973
+ checks: freshInteractionChecks().map((check) => ({ ...check })),
1974
+ verifiedAt: interactionVerifiedAt,
2083
1975
  },
2084
1976
  }
2085
1977
  : {}),
2086
1978
  taskPlan: taskPlanSnapshot(),
2087
1979
  requiredChecks: requiredChecks(),
2088
1980
  completedChecks: requiredChecks().filter(verificationCheckCompleted),
2089
- unavailableChecks:
2090
- visualVerificationStatus() === 'unavailable' ? ['visual'] : [],
1981
+ unavailableChecks: unavailableChecks(),
2091
1982
  missingChecks: missingVerificationChecks(),
2092
1983
  lastMutationAt,
2093
1984
  lastInspectionAt,
@@ -2109,49 +2000,58 @@ export function createFramerAgentToolRuntime({
2109
2000
  };
2110
2001
  };
2111
2002
 
2112
- const withUnavailableVisualCheck = (completion) =>
2113
- visualVerificationStatus() === 'unavailable'
2114
- ? {
2115
- ...completion,
2116
- checks: [
2117
- ...(Array.isArray(completion?.checks) ? completion.checks : []),
2118
- {
2003
+ const withUnavailableChecks = (completion) => {
2004
+ const unavailable = unavailableChecks();
2005
+ if (unavailable.length === 0) return completion;
2006
+ const unverifiedBehaviors = interactionSummary().behaviors
2007
+ .filter((behavior) => behavior.verdict === 'unknown')
2008
+ .map((behavior) => `${behavior.behaviorId} (${behavior.unknowns[0] || 'not confirmed'})`);
2009
+ return {
2010
+ ...completion,
2011
+ checks: [
2012
+ ...(Array.isArray(completion?.checks) ? completion.checks : []),
2013
+ ...(unavailable.includes('visual')
2014
+ ? [{
2119
2015
  command: 'Visual verification',
2120
2016
  status: 'skipped',
2121
2017
  output:
2122
2018
  'Screenshots of the final canvas could not be delivered to the model, so the visual result is unverified.',
2123
- },
2124
- ],
2125
- }
2126
- : completion;
2019
+ }]
2020
+ : []),
2021
+ ...(unavailable.includes('interactions')
2022
+ ? [{
2023
+ command: 'Interaction verification',
2024
+ status: 'skipped',
2025
+ output:
2026
+ `The live project did not expose evidence for: ${unverifiedBehaviors.join('; ')}. These behaviors are unverified.`,
2027
+ }]
2028
+ : []),
2029
+ ],
2030
+ };
2031
+ };
2127
2032
 
2128
2033
  const finalizeCompletion = (completion) => {
2129
2034
  if (completion?.status !== 'completed' || mutationCalls === 0) {
2130
2035
  return completion;
2131
2036
  }
2132
- if (verification().completed) return withUnavailableVisualCheck(completion);
2037
+ if (verification().completed) return withUnavailableChecks(completion);
2133
2038
  const missing = missingVerificationChecks().map((check) => {
2134
2039
  if (check === 'task-plan') return 'a harness task and verification plan';
2135
2040
  if (check === 'structural') return 'a final structural read';
2136
2041
  if (check === 'visual') return 'post-mutation screenshot verification';
2137
2042
  return `post-mutation ${check} verification`;
2138
2043
  });
2139
- const interactionDetails = interactionVerification.behaviors
2140
- .filter((behavior) => behavior.verdict !== 'verified')
2141
- .map((behavior) => {
2142
- const detail =
2143
- behavior.contradictions[0]
2144
- || behavior.unknowns[0]
2145
- || 'no conclusive live evidence';
2146
- return `${behavior.behaviorId}=${behavior.verdict} (${detail})`;
2147
- });
2044
+ const interactionDetails = (taskPlan?.behaviors?.length ? interactionSummary().behaviors : [])
2045
+ .filter((behavior) => behavior.verdict === 'contradicted' || !behavior.checked)
2046
+ .map((behavior) =>
2047
+ `${behavior.behaviorId}=${behavior.verdict === 'contradicted' ? behavior.contradictions[0] : 'not checked after the latest change'}`);
2148
2048
  const reason = [
2149
2049
  `The harness ended before completing ${missing.join(' and ')}.`,
2150
2050
  ...(interactionDetails.length > 0
2151
2051
  ? [`Interaction evidence: ${interactionDetails.join('; ')}.`]
2152
2052
  : []),
2153
2053
  ].join(' ');
2154
- const failed = withUnavailableVisualCheck(completion);
2054
+ const failed = withUnavailableChecks(completion);
2155
2055
  return {
2156
2056
  ...failed,
2157
2057
  status: 'failed',
@@ -2170,6 +2070,22 @@ export function createFramerAgentToolRuntime({
2170
2070
  };
2171
2071
  };
2172
2072
 
2073
+ // A model that reports failure without changing anything and without a
2074
+ // refused plan or edit gave up on its own. It gets one prompt to continue.
2075
+ const unblockedFailurePrompt = (completion) => {
2076
+ if (completion?.status !== 'failed' || mutationCalls > 0) return null;
2077
+ const lastGate = [...toolCalls]
2078
+ .reverse()
2079
+ .find((call) => call.category === 'plan' || call.category === 'mutation');
2080
+ if (lastGate?.status === 'failed') return null;
2081
+ return [
2082
+ 'You returned failed without changing the Framer project, and no tool refused the work.',
2083
+ 'Nothing in this run is blocking you and there is no time limit.',
2084
+ 'Continue the same task from the current project state: plan it, apply the change, verify it, then return the required JSON outcome.',
2085
+ 'Return failed or blocked only for a concrete blocker you can name.',
2086
+ ].join(' ');
2087
+ };
2088
+
2173
2089
  const verificationRepairPrompt = () => {
2174
2090
  const current = verification();
2175
2091
  if (current.completed || mutationCalls === 0) return null;
@@ -2185,27 +2101,33 @@ export function createFramerAgentToolRuntime({
2185
2101
  missing.push(
2186
2102
  'capture and inspect a screenshot of the affected canvas after the latest mutation',
2187
2103
  );
2104
+ } else if (check === 'interactions') {
2105
+ const summary = interactionSummary();
2106
+ const unchecked = summary.behaviors
2107
+ .filter((behavior) => !behavior.checked)
2108
+ .map((behavior) => behavior.uncheckedTargets.length
2109
+ ? `${behavior.behaviorId} on ${behavior.uncheckedTargets.join(', ')}`
2110
+ : behavior.behaviorId);
2111
+ const contradicted = summary.behaviors
2112
+ .filter((behavior) => behavior.verdict === 'contradicted')
2113
+ .map((behavior) => behavior.contradictions[0]);
2114
+ missing.push([
2115
+ unchecked.length
2116
+ ? `call framer_verify_interactions for ${unchecked.join('; ')}, naming the Framer property that carries each behavior`
2117
+ : '',
2118
+ contradicted.length
2119
+ ? `fix or explain these mismatches: ${contradicted.join('; ')}`
2120
+ : '',
2121
+ ].filter(Boolean).join(' and '));
2188
2122
  } else {
2189
- const interactionState = interactionVerification.behaviors
2190
- .map((behavior) =>
2191
- `${behavior.behaviorId}:${behavior.verdict}`)
2192
- .join(', ');
2193
- const exhausted =
2194
- interactionVerification.budgetExhaustedBehaviorIds.length > 0;
2195
- missing.push(
2196
- check === 'interactions'
2197
- ? exhausted
2198
- ? `report interaction verification as incomplete without further mutation or repeated verification (${interactionState || 'unknown'})`
2199
- : `call framer_verify_interactions with canonical evidence node IDs for every declared behavior and chosen mechanism; repair only contradicted evidence, while unknown evidence requires a different read or grounded semantic assessment (${interactionState || 'not yet checked'})`
2200
- : `perform a focused read tagged with verifies:["${check}"] after the latest mutation`,
2201
- );
2123
+ missing.push(`perform a focused read tagged with verifies:["${check}"] after the latest mutation`);
2202
2124
  }
2203
2125
  }
2204
2126
  return [
2205
2127
  'Continue the same Framer task from its current project state.',
2206
2128
  'Do not redo successful work.',
2207
2129
  `Before returning, ${missing.join(' and ')}.`,
2208
- 'Never mutate merely because evidence is unknown. Repair one observed contradiction at most once, then return the required JSON outcome.',
2130
+ 'If the live project does not expose a property, report that behavior as unverified rather than changing working parts. Then return the required JSON outcome.',
2209
2131
  ].join(' ');
2210
2132
  };
2211
2133
 
@@ -2219,6 +2141,7 @@ export function createFramerAgentToolRuntime({
2219
2141
  'Use framer_read_project for supported project queries such as screenshots.',
2220
2142
  'To place a user attachment in the project, call framer_upload_attachment with its id from context.referenceAssetFiles and use the returned url as the image fill. Never upload user files to external services.',
2221
2143
  'Use framer_query_images for approved stock imagery only after checking user attachments and suitable existing project images.',
2144
+ 'Use framer_add_svg for geometric or decorative artwork you draw as SVG markup; use icon sets for icons and real text for text.',
2222
2145
  'Use framer_apply_changes for layout and styling. Read its diagnostics, verify the result, and repair concrete issues.',
2223
2146
  'Use framer_read_code_file and framer_update_code_file for existing code components and overrides instead of generic JavaScript.',
2224
2147
  'If framer_write is rejected as unsafe, do not retry JavaScript variants. Switch to framer_apply_changes or report the concrete blocker.',
@@ -2250,109 +2173,13 @@ export function createFramerAgentToolRuntime({
2250
2173
  .update(`${name}\n${JSON.stringify(rawArguments)}`)
2251
2174
  .digest('hex');
2252
2175
 
2253
- const representativeRepairTargets = (changes) => {
2254
- const commands = summarizeFramerChanges(changes).commands;
2255
- if (
2256
- commands.length === 0
2257
- || commands.some((command) =>
2258
- command.operation !== 'set'
2259
- || !command.nodeId
2260
- || !representativeRepairNodeIds.has(command.nodeId))
2261
- ) {
2262
- return [];
2263
- }
2264
- return uniqueStrings(commands.map((command) => command.nodeId));
2265
- };
2266
-
2267
- const requireTaskPlanForMutation = ({ changes = '' } = {}) => {
2176
+ const requireTaskPlanForMutation = () => {
2268
2177
  if (!taskPlan) {
2269
2178
  throw toolError(
2270
2179
  'FRAMER_HARNESS_PLAN_REQUIRED',
2271
2180
  'Inspect the live Framer target, then call framer_plan_task before making a mutation.',
2272
2181
  );
2273
2182
  }
2274
- const repairTargets = representativeRepairTargets(changes);
2275
- const isRepresentativeRepair = repairTargets.length > 0;
2276
- const rejectedSelectedMechanisms = taskPlan.behaviors.filter((behavior) => {
2277
- const selected = taskPlan.mechanisms.find((decision) =>
2278
- decision.behaviorId === behavior.id);
2279
- return selected && taskPlan.rejectedMechanisms.some((rejected) =>
2280
- rejected.mechanism === selected.mechanism
2281
- && (
2282
- rejected.behaviorId === behavior.id
2283
- || rejected.behaviorSignature === behavior.signature
2284
- ));
2285
- });
2286
- if (rejectedSelectedMechanisms.length > 0) {
2287
- throw toolError(
2288
- 'FRAMER_BEHAVIOR_MECHANISM_REPLAN_REQUIRED',
2289
- `Choose a different mechanism for ${rejectedSelectedMechanisms.map((behavior) => behavior.id).join(', ')} before making another mutation.`,
2290
- );
2291
- }
2292
- const changedNodeIds = new Set(
2293
- summarizeFramerChanges(changes).commands
2294
- .flatMap((command) => [
2295
- command.nodeId,
2296
- command.sourceNodeId,
2297
- ])
2298
- .filter(Boolean),
2299
- );
2300
- const rawChanges = String(changes || '');
2301
- const pendingRepresentative = taskPlan.behaviors
2302
- .filter((behavior) => behavior.expectedTargets > 1)
2303
- .map((behavior) => ({
2304
- behavior,
2305
- decision: taskPlan.mechanisms.find((decision) =>
2306
- decision.behaviorId === behavior.id),
2307
- }))
2308
- .filter(({ decision }) => decision?.representativeFirst)
2309
- .filter(({ behavior }) =>
2310
- !representativeVerifiedBehaviorIds.has(behavior.id));
2311
- for (const { behavior, decision } of pendingRepresentative) {
2312
- const touchedTargets = decision.targetNodeIds.filter((nodeId) =>
2313
- rawChanges.includes(nodeId));
2314
- const touchedEvidence = [...changedNodeIds].filter((nodeId) =>
2315
- representativeEvidenceBehaviorByNode.get(nodeId) === behavior.id);
2316
- if (
2317
- touchedTargets.length > 1
2318
- || (
2319
- touchedTargets.length === 1
2320
- && touchedTargets[0] !== decision.representativeTargetNodeId
2321
- )
2322
- ) {
2323
- throw toolError(
2324
- 'FRAMER_BEHAVIOR_REPRESENTATIVE_MUTATION_REQUIRED',
2325
- `Implement only representative target ${decision.representativeTargetNodeId} for ${behavior.id} before changing the remaining targets.`,
2326
- );
2327
- }
2328
- if (touchedTargets.length === 0 && touchedEvidence.length === 0) {
2329
- continue;
2330
- }
2331
- if (!representativeMutatedBehaviorIds.has(behavior.id)) continue;
2332
- const attempt = behaviorVerificationAttempts.get(behavior.id);
2333
- const repairForBehavior =
2334
- isRepresentativeRepair
2335
- && repairTargets.every((nodeId) =>
2336
- representativeRepairBehaviorByNode.get(nodeId) === behavior.id);
2337
- if (
2338
- !repairForBehavior
2339
- || Number(attempt?.repairs || 0)
2340
- >= MAX_INTERACTION_REPAIRS_PER_BEHAVIOR
2341
- ) {
2342
- throw toolError(
2343
- 'FRAMER_BEHAVIOR_REPRESENTATIVE_VERIFICATION_REQUIRED',
2344
- `Verify representative target ${decision.representativeTargetNodeId} for ${behavior.id} before changing it again or batching the remaining targets.`,
2345
- );
2346
- }
2347
- behaviorVerificationAttempts.set(behavior.id, {
2348
- ...attempt,
2349
- repairs: Number(attempt?.repairs || 0) + 1,
2350
- });
2351
- }
2352
- return pendingRepresentative
2353
- .filter(({ decision }) =>
2354
- rawChanges.includes(decision.representativeTargetNodeId))
2355
- .map(({ behavior }) => behavior.id);
2356
2183
  };
2357
2184
 
2358
2185
  async function invokeUnsafe(name, rawArguments = {}) {
@@ -2374,10 +2201,7 @@ export function createFramerAgentToolRuntime({
2374
2201
  'Framer Agent tool arguments must be an object.',
2375
2202
  );
2376
2203
  }
2377
- const mutation =
2378
- name === 'framer_apply_changes'
2379
- || name === 'framer_update_code_file'
2380
- || name === 'framer_write';
2204
+ const mutation = MUTATION_TOOL_NAMES.has(name);
2381
2205
  const category =
2382
2206
  name === 'framer_plan_task'
2383
2207
  ? 'plan'
@@ -2423,9 +2247,7 @@ export function createFramerAgentToolRuntime({
2423
2247
  name,
2424
2248
  phase: 'started',
2425
2249
  message:
2426
- name === 'framer_apply_changes'
2427
- || name === 'framer_update_code_file'
2428
- || name === 'framer_write'
2250
+ mutation
2429
2251
  ? 'Updating the Framer project'
2430
2252
  : name === 'framer_plan_task'
2431
2253
  ? 'Planning the Framer work and verification'
@@ -2444,7 +2266,40 @@ export function createFramerAgentToolRuntime({
2444
2266
  const queries = Array.isArray(rawArguments.queries)
2445
2267
  ? uniqueStrings(rawArguments.queries, 8)
2446
2268
  : [];
2447
- if (queries.length > 0 && !instructionLookupUnavailable) {
2269
+ const guideIds = Array.isArray(rawArguments.guides)
2270
+ ? uniqueStrings(rawArguments.guides, 6)
2271
+ : [];
2272
+ if (guideIds.length > 0) {
2273
+ const guides = guideIds.flatMap((id) => {
2274
+ const entry = guideCatalog.get(id);
2275
+ const loaded = entry
2276
+ ? readGuidanceFile(
2277
+ entry.root,
2278
+ entry.path,
2279
+ MAX_GUIDANCE_FILE_BYTES,
2280
+ entry.section,
2281
+ )
2282
+ : null;
2283
+ if (!loaded) return [];
2284
+ if (!guidesRead.includes(id)) guidesRead.push(id);
2285
+ return [{ id, content: loaded.content }];
2286
+ });
2287
+ const unknown = guideIds.filter(
2288
+ (id) => !guides.some((guide) => guide.id === id),
2289
+ );
2290
+ result = {
2291
+ stdout: JSON.stringify({
2292
+ guides,
2293
+ ...(unknown.length > 0
2294
+ ? {
2295
+ unknownGuides: unknown,
2296
+ availableGuides: [...guideCatalog.keys()],
2297
+ }
2298
+ : {}),
2299
+ }),
2300
+ stderr: '',
2301
+ };
2302
+ } else if (queries.length > 0 && !instructionLookupUnavailable) {
2448
2303
  try {
2449
2304
  result = await executeCli(
2450
2305
  ['docs', ...queries],
@@ -2487,22 +2342,7 @@ export function createFramerAgentToolRuntime({
2487
2342
  'Inspect the live Framer target before declaring the task and verification plan.',
2488
2343
  );
2489
2344
  }
2490
- const nextPlan = normalizeTaskPlan(
2491
- rawArguments,
2492
- inheritedRejectedMechanisms,
2493
- );
2494
- const previousMechanisms = new Map(
2495
- (taskPlan?.mechanisms || []).map((decision) => [
2496
- decision.behaviorId,
2497
- JSON.stringify({
2498
- mechanism: decision.mechanism,
2499
- targetNodeIds: decision.targetNodeIds,
2500
- representativeTargetNodeId:
2501
- decision.representativeTargetNodeId,
2502
- }),
2503
- ]),
2504
- );
2505
- const behaviorPlan = mergeBehaviorPlans(taskPlan, nextPlan);
2345
+ const nextPlan = normalizeTaskPlan(rawArguments);
2506
2346
  taskPlan = {
2507
2347
  summary: nextPlan.summary,
2508
2348
  domains: uniqueStrings([
@@ -2513,280 +2353,55 @@ export function createFramerAgentToolRuntime({
2513
2353
  ...(taskPlan?.verification || []),
2514
2354
  ...nextPlan.verification,
2515
2355
  ]),
2516
- ...behaviorPlan,
2356
+ ...mergeBehaviorPlans(taskPlan, nextPlan),
2517
2357
  };
2518
- for (const decision of taskPlan.mechanisms) {
2519
- const prior = previousMechanisms.get(decision.behaviorId);
2520
- const current = JSON.stringify({
2521
- mechanism: decision.mechanism,
2522
- targetNodeIds: decision.targetNodeIds,
2523
- representativeTargetNodeId:
2524
- decision.representativeTargetNodeId,
2525
- });
2526
- if (prior && prior !== current) {
2527
- representativeVerifiedBehaviorIds.delete(decision.behaviorId);
2528
- representativeMutatedBehaviorIds.delete(decision.behaviorId);
2529
- behaviorVerificationAttempts.delete(decision.behaviorId);
2530
- }
2531
- }
2532
2358
  result = {
2533
- stdout: JSON.stringify({
2534
- plan: {
2535
- summary: taskPlan.summary,
2536
- domains: [...taskPlan.domains],
2537
- verification: [...taskPlan.verification],
2538
- behaviors: taskPlan.behaviors.map((behavior) => ({
2539
- ...behavior,
2540
- expectedContent: [...behavior.expectedContent],
2541
- })),
2542
- mechanisms: taskPlan.mechanisms.map((decision) => ({
2543
- ...decision,
2544
- })),
2545
- compatibility: taskPlan.compatibility.map((entry) => ({
2546
- ...entry,
2547
- requiredCapabilities: [...entry.requiredCapabilities],
2548
- })),
2549
- rejectedMechanisms: taskPlan.rejectedMechanisms.map((entry) => ({
2550
- ...entry,
2551
- })),
2552
- },
2553
- instruction:
2554
- 'Implement one representative first when required, verify it, then complete coverage and gather every derived verification result after the final mutation.',
2555
- }),
2359
+ stdout: JSON.stringify({ plan: taskPlanSnapshot() }),
2556
2360
  stderr: '',
2557
2361
  };
2558
2362
  } else if (name === 'framer_verify_interactions') {
2559
- if (mutationCalls === 0) {
2560
- throw toolError(
2561
- 'FRAMER_HARNESS_INTERACTION_MUTATION_REQUIRED',
2562
- 'Verify interactions only after the relevant Framer mutation has succeeded.',
2563
- );
2564
- }
2565
2363
  if (!taskPlan?.behaviors?.length) {
2566
2364
  throw toolError(
2567
2365
  'FRAMER_BEHAVIOR_CONTRACT_REQUIRED',
2568
- 'Declare observable behaviors and compatible mechanisms with framer_plan_task before interaction verification.',
2569
- );
2570
- }
2571
- const checks = normalizeBehaviorEvidence(
2572
- rawArguments.checks,
2573
- taskPlan,
2574
- );
2575
- const exhaustedBehaviorIds = uniqueStrings(
2576
- checks
2577
- .map((check) => check.behaviorId)
2578
- .filter((behaviorId) =>
2579
- behaviorVerificationAttempts.get(behaviorId)?.exhausted),
2580
- );
2581
- if (exhaustedBehaviorIds.length > 0) {
2582
- throw toolError(
2583
- 'FRAMER_INTERACTION_VERIFICATION_BUDGET_EXHAUSTED',
2584
- `Interaction verification is exhausted for ${exhaustedBehaviorIds.join(', ')}. Stop retrying unchanged evidence and report the work as partially verified.`,
2366
+ 'Declare the behaviors to confirm in framer_plan_task before verifying them.',
2585
2367
  );
2586
2368
  }
2369
+ const checks = normalizeBehaviorEvidence(rawArguments.checks, taskPlan);
2587
2370
  const pagePath = normalizePagePath(rawArguments.pagePath);
2588
- const requestedIds = evidenceNodeIds(checks);
2589
- const componentIds = uniqueStrings(
2590
- checks.map((check) => check.componentNodeId),
2591
- );
2592
2371
  const liveResult = await execCode(
2593
2372
  [
2594
- `const requestedIds = ${JSON.stringify(requestedIds)};`,
2595
- `const componentIds = ${JSON.stringify(componentIds)};`,
2596
- 'const [nodes, componentControls] = await Promise.all([',
2597
- ' Promise.all(requestedIds.map(async (id) => ({',
2598
- ' id,',
2599
- ' node: await framer.agent.serialize({ id, depth: 8 },',
2600
- ` { pagePath: ${JSON.stringify(pagePath)} }).catch(() => null),`,
2601
- ' parentId: await framer.agent.getParentNode({ id },',
2602
- ` { pagePath: ${JSON.stringify(pagePath)} }).then((node) => node?.id ?? null).catch(() => null),`,
2603
- ' ancestorIds: await framer.agent.getAncestors({ id },',
2604
- ` { pagePath: ${JSON.stringify(pagePath)} }).then((nodes) => Array.isArray(nodes) ? nodes.map((node) => node?.id).filter(Boolean) : []).catch(() => []),`,
2605
- ' }))),',
2606
- ' componentIds.length > 0',
2607
- ' ? framer.agent.readComponentControls({ componentIds },',
2608
- ` { pagePath: ${JSON.stringify(pagePath)} }).catch(() => null)`,
2609
- ' : null,',
2610
- ']);',
2611
- 'console.log(JSON.stringify({ nodes, componentControls }, null, 2));',
2373
+ `const requestedIds = ${JSON.stringify(evidenceNodeIds(checks))};`,
2374
+ 'const nodes = await Promise.all(requestedIds.map(async (id) => ({',
2375
+ ' id,',
2376
+ ' node: await framer.agent.serialize({ id, depth: 1 },',
2377
+ ` { pagePath: ${JSON.stringify(pagePath)} }).catch(() => null),`,
2378
+ '})));',
2379
+ 'console.log(JSON.stringify({ nodes }, null, 2));',
2612
2380
  ].join('\n'),
2613
2381
  );
2614
2382
  const parsed = parseStructuredOutput(liveResult.stdout);
2615
- const nodesById = new Map(
2383
+ const nodes = new Map(
2616
2384
  (Array.isArray(parsed?.nodes) ? parsed.nodes : [])
2617
- .filter((entry) => isRecord(entry) && typeof entry.id === 'string')
2618
- .map((entry) => [entry.id, {
2619
- node: entry.node || null,
2620
- parentId:
2621
- typeof entry.parentId === 'string'
2622
- ? entry.parentId
2623
- : null,
2624
- ancestorIds: Array.isArray(entry.ancestorIds)
2625
- ? entry.ancestorIds
2626
- : [],
2627
- }]),
2385
+ .filter((entry) => isRecord(entry) && typeof entry.id === 'string' && entry.node)
2386
+ .map((entry) => [entry.id, entry.node]),
2628
2387
  );
2629
- const verified = verifyBehaviorEvidence({
2630
- plan: taskPlan,
2631
- checks,
2632
- snapshots: nodesById,
2633
- context: {
2634
- lowLevelWriteCalls,
2635
- authorizedInstanceContainerIds,
2636
- componentControls: parsed?.componentControls || null,
2637
- semanticVerifierMode,
2638
- },
2639
- });
2388
+ const results = verifyBehaviorChecks({ checks, nodes });
2640
2389
  markInspection();
2641
2390
  interactionVerificationCalls += 1;
2642
- representativeRepairNodeIds.clear();
2643
- representativeRepairBehaviorByNode.clear();
2644
- representativeEvidenceBehaviorByNode.clear();
2645
- const checkedBehaviorIds = new Set(
2646
- checks.map((check) => check.behaviorId),
2647
- );
2648
- for (const behaviorResult of verified.behaviors) {
2649
- if (!checkedBehaviorIds.has(behaviorResult.behaviorId)) continue;
2650
- const behaviorChecks = verified.checks.filter((check) =>
2651
- check.behaviorId === behaviorResult.behaviorId);
2652
- for (const check of behaviorChecks) {
2653
- evidenceNodeIds([check]).forEach((nodeId) =>
2654
- representativeEvidenceBehaviorByNode.set(
2655
- nodeId,
2656
- behaviorResult.behaviorId,
2657
- ));
2658
- }
2659
- const deterministicallyContradicted = behaviorChecks.some((check) =>
2660
- check.deterministicVerdict === 'contradicted');
2661
- const prior = behaviorVerificationAttempts.get(
2662
- behaviorResult.behaviorId,
2663
- ) || {
2664
- attempts: 0,
2665
- unchangedEvidenceCount: 0,
2666
- repairs: 0,
2667
- fingerprint: null,
2668
- exhausted: false,
2669
- };
2670
- if (behaviorResult.verdict === 'verified') {
2671
- behaviorVerificationAttempts.delete(behaviorResult.behaviorId);
2672
- Object.assign(behaviorResult, {
2673
- verificationAttempts: prior.attempts + 1,
2674
- verificationAttemptsRemaining: Math.max(
2675
- 0,
2676
- MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR
2677
- - prior.attempts
2678
- - 1,
2679
- ),
2680
- repairAttempts: prior.repairs,
2681
- budgetExhausted: false,
2682
- });
2683
- continue;
2684
- }
2685
- const evidenceFingerprint = crypto
2686
- .createHash('sha256')
2687
- .update(JSON.stringify({
2688
- mechanism: behaviorResult.mechanism,
2689
- verdict: behaviorResult.verdict,
2690
- contradictions: [...behaviorResult.contradictions].sort(),
2691
- unknowns: [...behaviorResult.unknowns].sort(),
2692
- checks: behaviorChecks.map((check) => ({
2693
- verdict: check.verdict,
2694
- nodeIds: evidenceNodeIds([check]).sort(),
2695
- observations: check.observations,
2696
- })),
2697
- }))
2698
- .digest('hex');
2699
- const attempts = prior.attempts + 1;
2700
- const unchangedEvidenceCount =
2701
- prior.fingerprint === evidenceFingerprint
2702
- ? prior.unchangedEvidenceCount + 1
2703
- : 1;
2704
- const exhausted =
2705
- attempts >= MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR
2706
- || unchangedEvidenceCount >= 2
2707
- || (
2708
- behaviorResult.verdict === 'contradicted'
2709
- && prior.repairs >= MAX_INTERACTION_REPAIRS_PER_BEHAVIOR
2710
- );
2711
- behaviorVerificationAttempts.set(behaviorResult.behaviorId, {
2712
- attempts,
2713
- unchangedEvidenceCount,
2714
- repairs: prior.repairs,
2715
- fingerprint: evidenceFingerprint,
2716
- exhausted,
2717
- });
2718
- Object.assign(behaviorResult, {
2719
- verificationAttempts: attempts,
2720
- verificationAttemptsRemaining: Math.max(
2721
- 0,
2722
- MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR - attempts,
2723
- ),
2724
- repairAttempts: prior.repairs,
2725
- budgetExhausted: exhausted,
2726
- });
2727
- for (const check of behaviorChecks) {
2728
- check.repairAllowed =
2729
- check.deterministicVerdict === 'contradicted'
2730
- && prior.repairs < MAX_INTERACTION_REPAIRS_PER_BEHAVIOR
2731
- && !exhausted;
2732
- if (!check.repairAllowed) continue;
2733
- evidenceNodeIds([check]).forEach((nodeId) => {
2734
- representativeRepairNodeIds.add(nodeId);
2735
- representativeRepairBehaviorByNode.set(
2736
- nodeId,
2737
- behaviorResult.behaviorId,
2738
- );
2739
- });
2740
- }
2741
- if (
2742
- !deterministicallyContradicted
2743
- || !exhausted
2744
- || !behaviorResult.mechanism
2745
- ) continue;
2746
- const alreadyRejected = taskPlan.rejectedMechanisms.some((rejected) =>
2747
- rejected.mechanism === behaviorResult.mechanism
2748
- && (
2749
- rejected.behaviorId === behaviorResult.behaviorId
2750
- || rejected.behaviorSignature
2751
- === behaviorResult.behaviorSignature
2752
- ));
2753
- if (alreadyRejected) continue;
2754
- taskPlan = {
2755
- ...taskPlan,
2756
- rejectedMechanisms: [
2757
- ...taskPlan.rejectedMechanisms,
2758
- {
2759
- behaviorId: behaviorResult.behaviorId,
2760
- behaviorSignature: behaviorResult.behaviorSignature,
2761
- mechanism: behaviorResult.mechanism,
2762
- reason:
2763
- `Repeated live evidence contradicted this mechanism: ${behaviorChecks
2764
- .flatMap((check) => check.contradictions)
2765
- .join('; ')
2766
- .slice(0, 700)}`,
2767
- source: 'verification_failure',
2768
- },
2769
- ],
2770
- };
2771
- }
2772
- const budgetExhaustedBehaviorIds = [...behaviorVerificationAttempts]
2773
- .filter(([, attempt]) => attempt.exhausted)
2774
- .map(([behaviorId]) => behaviorId);
2775
- interactionVerification = {
2776
- ...verified,
2777
- semanticVerifierMode,
2778
- budgetExhaustedBehaviorIds,
2779
- verifiedAt: new Date().toISOString(),
2780
- };
2781
- verified.representativeBehaviorIds.forEach((behaviorId) =>
2782
- representativeVerifiedBehaviorIds.add(behaviorId));
2783
- if (interactionVerification.completed) {
2784
- verificationEvidence.set('interactions', operationSequence);
2785
- } else {
2786
- verificationEvidence.delete('interactions');
2391
+ interactionVerifiedAt = new Date().toISOString();
2392
+ for (const checkResult of results) {
2393
+ interactionCheckResults.set(
2394
+ `${checkResult.behaviorId}\n${checkResult.nodeId}\n${checkResult.property}`,
2395
+ { sequence: operationSequence, result: checkResult },
2396
+ );
2787
2397
  }
2398
+ const summary = interactionSummary();
2788
2399
  result = {
2789
- stdout: JSON.stringify(interactionVerification),
2400
+ stdout: JSON.stringify({
2401
+ status: summary.status,
2402
+ checks: results,
2403
+ behaviors: summary.behaviors,
2404
+ }),
2790
2405
  stderr: liveResult.stderr,
2791
2406
  };
2792
2407
  } else if (name === 'framer_read_project') {
@@ -2860,6 +2475,88 @@ export function createFramerAgentToolRuntime({
2860
2475
  imageSearchCalls += 1;
2861
2476
  } else if (name === 'framer_upload_attachment') {
2862
2477
  result = await uploadAttachment(String(rawArguments.attachmentId || ''));
2478
+ } else if (name === 'framer_add_svg') {
2479
+ const svg = normalizeSvgMarkup(rawArguments.svg);
2480
+ const parentId = String(rawArguments.parentId || '').trim();
2481
+ if (!parentId) {
2482
+ throw toolError(
2483
+ 'FRAMER_SVG_PARENT_REQUIRED',
2484
+ 'framer_add_svg needs the id of the parent node.',
2485
+ );
2486
+ }
2487
+ const index = Number.isInteger(rawArguments.index) && rawArguments.index >= 0
2488
+ ? rawArguments.index
2489
+ : null;
2490
+ const svgName = String(rawArguments.name || '').trim().slice(0, 120) || 'SVG';
2491
+ const attributes = normalizeSvgAttributes(rawArguments.attributes);
2492
+ requireTaskPlanForMutation();
2493
+ // addSVG has no parent option and replaces a selected SVG, so clear
2494
+ // the selection, find the one new SVG node, then move it into place.
2495
+ result = await execCode(
2496
+ [
2497
+ `const svg = ${JSON.stringify(svg)};`,
2498
+ `const parentId = ${JSON.stringify(parentId)};`,
2499
+ `const index = ${JSON.stringify(index)};`,
2500
+ `const attributes = ${JSON.stringify({ name: svgName, position: 'relative', ...attributes })};`,
2501
+ 'const fail = (code, message) => console.log(JSON.stringify({ ok: false, error: { code, message } }));',
2502
+ 'const parent = await framer.getNode(parentId);',
2503
+ 'if (!parent) {',
2504
+ ' fail("FRAMER_SVG_PARENT_NOT_FOUND", `No node exists with id "${parentId}".`);',
2505
+ '} else {',
2506
+ ' const previousIds = new Set((await framer.getNodesWithType("SVGNode")).map((node) => node.id));',
2507
+ ' try { await framer.setSelection([]); } catch {}',
2508
+ ' await framer.addSVG({ svg, name: attributes.name });',
2509
+ ' let node = null;',
2510
+ ' for (let attempt = 0; attempt < 6 && !node; attempt += 1) {',
2511
+ ' if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, 150));',
2512
+ ' const inserted = (await framer.getNodesWithType("SVGNode")).filter((candidate) => !previousIds.has(candidate.id));',
2513
+ ' if (inserted.length === 1) node = inserted[0];',
2514
+ ' }',
2515
+ ' if (!node) {',
2516
+ ' fail("FRAMER_SVG_NOT_INSERTED", "Framer did not expose exactly one new SVG node after inserting the markup.");',
2517
+ ' } else {',
2518
+ ' try {',
2519
+ ' await framer.setParent(node.id, parentId, index ?? undefined);',
2520
+ ' await node.setAttributes(attributes);',
2521
+ ' const placedParent = await node.getParent();',
2522
+ ' if (placedParent?.id !== parentId) throw new Error("The SVG did not land inside the requested parent.");',
2523
+ ' const placed = await framer.getNode(node.id);',
2524
+ ' console.log(JSON.stringify({',
2525
+ ' ok: true,',
2526
+ ' node: {',
2527
+ ' id: node.id,',
2528
+ ' name: placed?.name ?? attributes.name,',
2529
+ ' parentId,',
2530
+ ' position: placed?.position ?? null,',
2531
+ ' width: placed?.width ?? null,',
2532
+ ' height: placed?.height ?? null,',
2533
+ ' },',
2534
+ ' }));',
2535
+ ' } catch (error) {',
2536
+ ' await framer.removeNodes([node.id]).catch(() => undefined);',
2537
+ ' fail("FRAMER_SVG_PLACEMENT_FAILED", String(error?.message || error));',
2538
+ ' }',
2539
+ ' }',
2540
+ '}',
2541
+ ].join('\n'),
2542
+ );
2543
+ const parsed = parseStructuredOutput(result.stdout);
2544
+ if (!isRecord(parsed) || parsed.ok !== true) {
2545
+ throw structuredResultError(
2546
+ parsed,
2547
+ 'FRAMER_SVG_PLACEMENT_FAILED',
2548
+ 'Dexter could not place the SVG in the Framer project.',
2549
+ );
2550
+ }
2551
+ mutationCalls += 1;
2552
+ markMutation();
2553
+ mutationReceipts.push({
2554
+ sequence: operationSequence,
2555
+ kind: 'framer_add_svg',
2556
+ nodeId: parsed.node?.id ?? null,
2557
+ parentId,
2558
+ svgHash: crypto.createHash('sha256').update(svg).digest('hex'),
2559
+ });
2863
2560
  } else if (name === 'framer_read_code_file') {
2864
2561
  const filePath = normalizeCodeFilePath(rawArguments.path);
2865
2562
  result = await execCode(
@@ -2913,7 +2610,7 @@ export function createFramerAgentToolRuntime({
2913
2610
  );
2914
2611
  const parsed = parseStructuredOutput(result.stdout);
2915
2612
  if (!isRecord(parsed) || parsed.ok !== true) {
2916
- throw codeFileResultError(
2613
+ throw structuredResultError(
2917
2614
  parsed,
2918
2615
  'FRAMER_CODE_FILE_READ_FAILED',
2919
2616
  `Dexter could not read ${filePath}.`,
@@ -3072,14 +2769,14 @@ export function createFramerAgentToolRuntime({
3072
2769
  );
3073
2770
  const parsed = parseStructuredOutput(result.stdout);
3074
2771
  if (!isRecord(parsed) || parsed.ok !== true) {
3075
- throw codeFileResultError(
2772
+ throw structuredResultError(
3076
2773
  parsed,
3077
2774
  'FRAMER_CODE_FILE_UPDATE_FAILED',
3078
2775
  `Dexter could not update ${filePath}.`,
3079
2776
  );
3080
2777
  }
3081
2778
  mutationCalls += 1;
3082
- markMutation();
2779
+ markMutation({ code: true });
3083
2780
  const mutationSequence = operationSequence;
3084
2781
  markInspection({
3085
2782
  verifies: parsed.typecheck?.passed === true ? ['code'] : [],
@@ -3116,8 +2813,7 @@ export function createFramerAgentToolRuntime({
3116
2813
  'Framer Agent changes are required.',
3117
2814
  );
3118
2815
  }
3119
- const mutatedRepresentativeBehaviorIds =
3120
- requireTaskPlanForMutation({ changes });
2816
+ requireTaskPlanForMutation();
3121
2817
  const pagePath = normalizePagePath(rawArguments.pagePath);
3122
2818
  result = await execCode(
3123
2819
  [
@@ -3128,8 +2824,6 @@ export function createFramerAgentToolRuntime({
3128
2824
  mutationCalls += 1;
3129
2825
  applyChangesCalls += 1;
3130
2826
  markMutation();
3131
- mutatedRepresentativeBehaviorIds.forEach((behaviorId) =>
3132
- representativeMutatedBehaviorIds.add(behaviorId));
3133
2827
  const parsedResult = parseStructuredOutput(result.stdout);
3134
2828
  mutationReceipts.push({
3135
2829
  sequence: operationSequence,
@@ -3192,9 +2886,7 @@ export function createFramerAgentToolRuntime({
3192
2886
  name,
3193
2887
  phase: 'completed',
3194
2888
  message:
3195
- name === 'framer_apply_changes'
3196
- || name === 'framer_update_code_file'
3197
- || name === 'framer_write'
2889
+ mutation
3198
2890
  ? 'Updated the Framer project'
3199
2891
  : 'Finished inspecting the Framer project',
3200
2892
  durationMs,
@@ -3203,6 +2895,9 @@ export function createFramerAgentToolRuntime({
3203
2895
  name === 'framer_read_code_file'
3204
2896
  ? normalizedCodeFileReadResult(result)
3205
2897
  : normalizedResult(result);
2898
+ // Result size is what this call adds to every later model turn.
2899
+ toolCall.resultBytes = Buffer.byteLength(JSON.stringify(normalized), 'utf8');
2900
+ if (modelImages.length > 0) toolCall.imageCount = modelImages.length;
3206
2901
  if (name !== 'framer_read_project') return normalized;
3207
2902
  const screenshotCount = screenshotQueries(rawArguments).length;
3208
2903
  return {
@@ -3271,7 +2966,7 @@ export function createFramerAgentToolRuntime({
3271
2966
  name,
3272
2967
  phase: 'failed',
3273
2968
  message:
3274
- name === 'framer_apply_changes' || name === 'framer_write'
2969
+ mutation
3275
2970
  ? 'The Framer project update failed'
3276
2971
  : 'The Framer project inspection failed',
3277
2972
  durationMs: Date.now() - startedAt,
@@ -3353,12 +3048,14 @@ export function createFramerAgentToolRuntime({
3353
3048
  inspect: inspectFinalState,
3354
3049
  finalizeCompletion,
3355
3050
  verificationRepairPrompt,
3051
+ unblockedFailurePrompt,
3356
3052
  sourceHash: () => null,
3357
3053
  hasPendingChanges: () => false,
3358
3054
  directWorkspace: true,
3359
3055
  summary: () => ({
3360
3056
  projectId,
3361
3057
  projectUrl,
3058
+ unresolvedTargets: unresolvedTargets.map((target) => ({ ...target })),
3362
3059
  mutationCalls,
3363
3060
  applyChangesCalls,
3364
3061
  lowLevelWriteCalls,
@@ -3412,10 +3109,6 @@ export function createFramerAgentToolRuntime({
3412
3109
  lowLevelWriteDisabled,
3413
3110
  lowLevelWriteBlockedCalls,
3414
3111
  instructionFallbacks,
3415
- interactionVerificationAttemptLimit:
3416
- MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR,
3417
- interactionRepairLimit:
3418
- MAX_INTERACTION_REPAIRS_PER_BEHAVIOR,
3419
3112
  },
3420
3113
  branch: branchChange,
3421
3114
  lastInspectionAt,
@@ -3442,6 +3135,7 @@ export function createFramerAgentToolRuntime({
3442
3135
  projectContextHash: guidanceMetadata.projectContextHash,
3443
3136
  files: [...guidanceMetadata.files],
3444
3137
  bytes: guidanceMetadata.bytes,
3138
+ guidesRead: [...guidesRead],
3445
3139
  },
3446
3140
  doctrine: { ...promptMetadata },
3447
3141
  }),