@gakim-digital/dexter-bridge 0.11.14 → 0.11.15

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,18 +3,16 @@ 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);
@@ -31,8 +29,6 @@ const DEFAULT_COMMAND_TIMEOUT_MS = 5 * 60_000;
31
29
  const SESSION_TIMEOUT_MS = 10 * 60_000;
32
30
  const MAX_UNSAFE_WRITE_FAILURES = 2;
33
31
  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
32
  const MAX_GUIDANCE_BYTES = 180 * 1024;
37
33
  const MAX_GUIDANCE_FILE_BYTES = 48 * 1024;
38
34
  const PROJECT_ID_PATTERN = /^[A-Za-z0-9]{20}$/;
@@ -91,10 +87,15 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
91
87
  {
92
88
  name: 'framer_instructions',
93
89
  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.',
90
+ '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
91
  inputSchema: {
96
92
  type: 'object',
97
93
  properties: {
94
+ guides: {
95
+ type: 'array',
96
+ maxItems: 6,
97
+ items: { type: 'string', minLength: 1, maxLength: 120 },
98
+ },
98
99
  queries: {
99
100
  type: 'array',
100
101
  maxItems: 8,
@@ -140,7 +141,7 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
140
141
  {
141
142
  name: 'framer_plan_task',
142
143
  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.',
144
+ '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
145
  inputSchema: {
145
146
  type: 'object',
146
147
  properties: {
@@ -167,147 +168,27 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
167
168
  type: 'array',
168
169
  maxItems: 20,
169
170
  description:
170
- 'Required for interaction work. Describe observable behavior independently from its implementation.',
171
+ '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
172
  items: {
172
173
  type: 'object',
173
174
  properties: {
174
175
  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: {
176
+ description: {
257
177
  type: 'string',
258
- minLength: 10,
259
- maxLength: 800,
178
+ minLength: 5,
179
+ maxLength: 500,
180
+ description: 'What the visitor should experience.',
260
181
  },
261
- representativeFirst: { type: 'boolean', default: true },
262
182
  targetNodeIds: {
263
183
  type: 'array',
264
184
  maxItems: 50,
265
185
  uniqueItems: true,
266
- items: {
267
- type: 'string',
268
- minLength: 1,
269
- maxLength: 200,
270
- },
186
+ items: { type: 'string', minLength: 1, maxLength: 200 },
271
187
  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'],
188
+ 'Every node that should carry this behavior once the work is done. Update the plan if they change, for example after replacing nodes.',
308
189
  },
309
190
  },
310
- required: ['mechanism', 'reason'],
191
+ required: ['id', 'description'],
311
192
  additionalProperties: false,
312
193
  },
313
194
  },
@@ -319,7 +200,7 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
319
200
  {
320
201
  name: 'framer_verify_interactions',
321
202
  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.',
203
+ '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
204
  inputSchema: {
324
205
  type: 'object',
325
206
  properties: {
@@ -327,103 +208,16 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
327
208
  checks: {
328
209
  type: 'array',
329
210
  minItems: 1,
330
- maxItems: 20,
211
+ maxItems: 50,
331
212
  items: {
332
213
  type: 'object',
333
214
  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
- },
215
+ behaviorId: { type: 'string', minLength: 1, maxLength: 120 },
216
+ nodeId: { type: 'string', minLength: 1, maxLength: 200 },
217
+ property: { type: 'string', minLength: 1, maxLength: 120 },
218
+ contains: { type: 'string', minLength: 1, maxLength: 500 },
425
219
  },
426
- required: ['behaviorId'],
220
+ required: ['behaviorId', 'nodeId', 'property'],
427
221
  additionalProperties: false,
428
222
  },
429
223
  },
@@ -646,6 +440,50 @@ function readGuidanceFile(projectRoot, relativePath, remainingBytes, section) {
646
440
  };
647
441
  }
648
442
 
443
+ const CORE_GUIDE_SECTIONS = [
444
+ { file: 'how-projects-work', section: 'Scope Types' },
445
+ { file: 'how-projects-work', section: 'Replicas' },
446
+ { file: 'how-projects-work', section: 'Transforms' },
447
+ { file: 'implementation-strategy', section: 'Determining Strategy' },
448
+ { file: 'implementation-strategy', section: 'Edit Strategy' },
449
+ { file: 'implementation-strategy', section: 'Update Loop' },
450
+ ];
451
+ const ON_DEMAND_GUIDE_FILES = [
452
+ { file: 'how-projects-work', sections: true },
453
+ { file: 'implementation-strategy', sections: true },
454
+ { file: 'core-examples', sections: false },
455
+ ];
456
+
457
+ // Lists the topic guides that are not part of every prompt, keyed by
458
+ // "file#Section" (or "file" for a whole file), from the files on disk.
459
+ function onDemandGuideCatalog(projectRoot) {
460
+ const catalog = new Map();
461
+ for (const { file, sections } of ON_DEMAND_GUIDE_FILES) {
462
+ const relativePath = `prompt/${file}.md`;
463
+ const absolutePath = path.join(projectRoot, relativePath);
464
+ if (!fs.existsSync(absolutePath)) continue;
465
+ if (!sections) {
466
+ catalog.set(file, { root: projectRoot, path: relativePath });
467
+ continue;
468
+ }
469
+ const headings = fs.readFileSync(absolutePath, 'utf8')
470
+ .split('\n')
471
+ .filter((line) => line.startsWith('## '))
472
+ .map((line) => line.slice(3).trim())
473
+ .filter((heading) => !CORE_GUIDE_SECTIONS.some(
474
+ (core) => core.file === file && core.section === heading,
475
+ ));
476
+ for (const heading of headings) {
477
+ catalog.set(`${file}#${heading}`, {
478
+ root: projectRoot,
479
+ path: relativePath,
480
+ section: heading,
481
+ });
482
+ }
483
+ }
484
+ return catalog;
485
+ }
486
+
649
487
  function markdownSection(markdown, heading) {
650
488
  const lines = markdown.split('\n');
651
489
  const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
@@ -779,7 +617,7 @@ function normalizeVerificationValues(values, allowed) {
779
617
  .filter((value) => allowed.includes(value));
780
618
  }
781
619
 
782
- function normalizeTaskPlan(rawArguments, inheritedRejectedMechanisms = []) {
620
+ function normalizeTaskPlan(rawArguments) {
783
621
  const summary = String(rawArguments.summary || '').trim().slice(0, 1000);
784
622
  const domains = normalizeVerificationValues(
785
623
  rawArguments.domains,
@@ -798,19 +636,7 @@ function normalizeTaskPlan(rawArguments, inheritedRejectedMechanisms = []) {
798
636
  // The model's declared verification is authoritative; domains describe the
799
637
  // work and never add checks on their own.
800
638
  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
- }
639
+ const behaviorPlan = normalizeBehaviorPlan(rawArguments);
814
640
  if (behaviorPlan.behaviors.length > 0) {
815
641
  if (!domains.includes('interactions')) domains.push('interactions');
816
642
  verification.add('interactions');
@@ -936,6 +762,28 @@ function authoritativeNodeIdsFromAssignment(assignment) {
936
762
  ]);
937
763
  }
938
764
 
765
+ const LITERAL_TOKEN_TYPES = new Set([
766
+ 'StringLiteral',
767
+ 'NoSubstitutionTemplate',
768
+ 'TemplateHead',
769
+ 'TemplateMiddle',
770
+ 'TemplateTail',
771
+ 'SingleLineComment',
772
+ 'MultiLineComment',
773
+ 'RegularExpressionLiteral',
774
+ ]);
775
+
776
+ // Blanks string, template, comment, and regex text so data such as image URLs
777
+ // or product copy is not mistaken for a forbidden global. Template
778
+ // substitutions stay visible because they are code.
779
+ function codeOutsideLiterals(code) {
780
+ let visible = '';
781
+ for (const token of jsTokens(code)) {
782
+ visible += LITERAL_TOKEN_TYPES.has(token.type) ? ' ' : token.value;
783
+ }
784
+ return visible;
785
+ }
786
+
939
787
  function validateCode(value, { write = false, allowPublishing = false } = {}) {
940
788
  const code = String(value || '').trim();
941
789
  if (!code) {
@@ -944,7 +792,7 @@ function validateCode(value, { write = false, allowPublishing = false } = {}) {
944
792
  'Framer Agent code is required.',
945
793
  );
946
794
  }
947
- if (UNSAFE_EXEC_PATTERN.test(code)) {
795
+ if (UNSAFE_EXEC_PATTERN.test(codeOutsideLiterals(code))) {
948
796
  throw toolError(
949
797
  'FRAMER_AGENT_CODE_UNSAFE',
950
798
  'Framer Agent code may use only the provided framer, state, and console globals.',
@@ -1246,32 +1094,9 @@ export function createFramerAgentToolRuntime({
1246
1094
  );
1247
1095
  }
1248
1096
  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
1097
  const activePagePath = normalizePagePath(
1260
1098
  assignment?.context?.activePagePath,
1261
1099
  );
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
1100
  const authoritativeNodeIds =
1276
1101
  authoritativeNodeIdsFromAssignment(assignment);
1277
1102
  let sessionId = null;
@@ -1303,20 +1128,10 @@ export function createFramerAgentToolRuntime({
1303
1128
  let lastMutationAt = null;
1304
1129
  let screenshotCalls = 0;
1305
1130
  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();
1131
+ // Latest result per behavior/node/property, with the operation sequence it
1132
+ // was observed at, so checks from several calls (or pages) accumulate.
1133
+ const interactionCheckResults = new Map();
1134
+ let interactionVerifiedAt = null;
1320
1135
  const mutationReceipts = [];
1321
1136
  let lastScreenshotSequence = 0;
1322
1137
  let lastScreenshotAttemptSequence = 0;
@@ -1326,6 +1141,8 @@ export function createFramerAgentToolRuntime({
1326
1141
  const screenshotFailures = [];
1327
1142
  let taskPlan = null;
1328
1143
  const verificationEvidence = new Map();
1144
+ let guideCatalog = new Map();
1145
+ const guidesRead = [];
1329
1146
  let guidanceMetadata = {
1330
1147
  loaded: false,
1331
1148
  contentHash: null,
@@ -1334,6 +1151,7 @@ export function createFramerAgentToolRuntime({
1334
1151
  bytes: 0,
1335
1152
  };
1336
1153
  let promptMetadata = {
1154
+ promptBytes: 0,
1337
1155
  doctrineLoaded: false,
1338
1156
  doctrineVersion: null,
1339
1157
  };
@@ -1343,6 +1161,7 @@ export function createFramerAgentToolRuntime({
1343
1161
  const framerAgentVersion = installedFramerAgentVersion();
1344
1162
  let operationSequence = 0;
1345
1163
  let lastMutationSequence = 0;
1164
+ let lastCodeMutationSequence = 0;
1346
1165
  let lastInspectionSequence = 0;
1347
1166
  const abortController = new AbortController();
1348
1167
 
@@ -1649,6 +1468,10 @@ export function createFramerAgentToolRuntime({
1649
1468
  if (!fs.existsSync(projectRoot)) {
1650
1469
  return { content: '', ...guidanceMetadata };
1651
1470
  }
1471
+ // Every run gets the guidance an edit always needs: principles, command
1472
+ // syntax, tool APIs, scope and replica rules, and the edit loop. Topic
1473
+ // guides (CMS, components, creation strategy, examples, ...) are listed
1474
+ // in a catalog and read on demand through framer_instructions.
1652
1475
  const requestedFiles = [
1653
1476
  { root: skillRoot, path: 'SKILL.md', label: 'SKILL.md' },
1654
1477
  { root: projectRoot, path: 'index.md', label: 'project/index.md' },
@@ -1657,24 +1480,19 @@ export function createFramerAgentToolRuntime({
1657
1480
  path: 'project-inventory.md',
1658
1481
  label: 'project/project-inventory.md',
1659
1482
  },
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
1483
  { root: projectRoot, path: 'prompt/overview.md' },
1484
+ { root: projectRoot, path: 'prompt/core-principles.md' },
1666
1485
  { root: projectRoot, path: 'prompt/guardrails.md' },
1667
1486
  { 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
1487
  { root: projectRoot, path: 'prompt/design-rules.md' },
1677
- { root: projectRoot, path: 'prompt/how-projects-work.md' },
1488
+ { root: projectRoot, path: 'prompt/updating-the-project.md' },
1489
+ { root: projectRoot, path: 'prompt/tools.md' },
1490
+ ...CORE_GUIDE_SECTIONS.map(({ file, section }) => ({
1491
+ root: projectRoot,
1492
+ path: `prompt/${file}.md`,
1493
+ section,
1494
+ label: `prompt/${file}.md#${section}`,
1495
+ })),
1678
1496
  ];
1679
1497
  const files = [];
1680
1498
  let remainingBytes = MAX_GUIDANCE_BYTES;
@@ -1693,9 +1511,19 @@ export function createFramerAgentToolRuntime({
1693
1511
  remainingBytes -= Buffer.byteLength(loaded.content, 'utf8');
1694
1512
  if (remainingBytes <= 0) break;
1695
1513
  }
1696
- const content = files
1697
- .map((file) => `## ${file.path}\n\n${file.content}`)
1698
- .join('\n\n');
1514
+ guideCatalog = onDemandGuideCatalog(projectRoot);
1515
+ const catalogText = guideCatalog.size > 0
1516
+ ? [
1517
+ '## On-demand guides',
1518
+ '',
1519
+ 'Read a guide with framer_instructions({ guides: [id] }) before work that depends on it.',
1520
+ ...[...guideCatalog.keys()].map((id) => `- ${id}`),
1521
+ ].join('\n')
1522
+ : '';
1523
+ const content = [
1524
+ ...files.map((file) => `## ${file.path}\n\n${file.content}`),
1525
+ ...(catalogText ? [catalogText] : []),
1526
+ ].join('\n\n');
1699
1527
  const inventory = files.find(
1700
1528
  (file) => file.path.endsWith('project-inventory.md'),
1701
1529
  )?.content || '';
@@ -1716,8 +1544,10 @@ export function createFramerAgentToolRuntime({
1716
1544
  const configurePromptContext = ({
1717
1545
  doctrineLoaded = false,
1718
1546
  doctrineVersion = null,
1547
+ promptBytes = 0,
1719
1548
  } = {}) => {
1720
1549
  promptMetadata = {
1550
+ promptBytes: Math.max(0, Number(promptBytes) || 0),
1721
1551
  doctrineLoaded: doctrineLoaded === true,
1722
1552
  doctrineVersion:
1723
1553
  typeof doctrineVersion === 'string' && doctrineVersion.trim()
@@ -1858,9 +1688,14 @@ export function createFramerAgentToolRuntime({
1858
1688
  return result;
1859
1689
  };
1860
1690
 
1691
+ // Selected layers that do not resolve to native nodes (stale selections,
1692
+ // aliases, layers inside component instances) no longer stop the run. The
1693
+ // model is told which targets to locate by name, and the reason is kept for
1694
+ // debugging.
1695
+ let unresolvedTargets = [];
1861
1696
  const validateAuthoritativeNodeIds = async () => {
1862
1697
  await ensureSession();
1863
- if (authoritativeNodeIds.length === 0) return;
1698
+ if (authoritativeNodeIds.length === 0) return { unresolvedTargets };
1864
1699
  if (nodeValidationPromise) return nodeValidationPromise;
1865
1700
  nodeValidationPromise = (async () => {
1866
1701
  reportActivity({
@@ -1872,9 +1707,13 @@ export function createFramerAgentToolRuntime({
1872
1707
  [
1873
1708
  `const requestedIds = ${JSON.stringify(authoritativeNodeIds)};`,
1874
1709
  '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 };',
1710
+ ' try {',
1711
+ ' const node = await framer.agent.getNode({ id: requestedId },',
1712
+ ` { pagePath: ${JSON.stringify(activePagePath)} });`,
1713
+ ' return { requestedId, resolvedId: node?.id ?? null, error: node ? null : "not found on the active page" };',
1714
+ ' } catch (error) {',
1715
+ ' return { requestedId, resolvedId: null, error: String(error?.message || error).slice(0, 200) };',
1716
+ ' }',
1878
1717
  '}));',
1879
1718
  'console.log(JSON.stringify({ resolved }));',
1880
1719
  ].join('\n'),
@@ -1884,26 +1723,30 @@ export function createFramerAgentToolRuntime({
1884
1723
  const resolved = Array.isArray(parsed?.resolved)
1885
1724
  ? parsed.resolved
1886
1725
  : [];
1887
- const invalid = authoritativeNodeIds.filter((requestedId) => {
1726
+ unresolvedTargets = authoritativeNodeIds.flatMap((requestedId) => {
1888
1727
  const match = resolved.find(
1889
1728
  (entry) =>
1890
1729
  isRecord(entry)
1891
1730
  && entry.requestedId === requestedId,
1892
1731
  );
1893
- return !match || match.resolvedId !== requestedId;
1732
+ if (match?.resolvedId === requestedId) return [];
1733
+ return [{
1734
+ id: requestedId,
1735
+ reason: !match
1736
+ ? 'not checked'
1737
+ : match.resolvedId
1738
+ ? `resolves to ${match.resolvedId}`
1739
+ : String(match.error || 'not found on the active page'),
1740
+ }];
1894
1741
  });
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
1742
  reportActivity({
1903
1743
  kind: 'status_update',
1904
1744
  source: 'framer-agent',
1905
- message: 'The selected Framer layers are ready.',
1745
+ message: unresolvedTargets.length > 0
1746
+ ? 'Some selected layers could not be matched exactly, so Dexter will locate them by name.'
1747
+ : 'The selected Framer layers are ready.',
1906
1748
  });
1749
+ return { unresolvedTargets };
1907
1750
  })().catch((error) => {
1908
1751
  nodeValidationPromise = null;
1909
1752
  throw error;
@@ -1954,9 +1797,10 @@ export function createFramerAgentToolRuntime({
1954
1797
  }
1955
1798
  };
1956
1799
 
1957
- const markMutation = () => {
1800
+ const markMutation = ({ code = false } = {}) => {
1958
1801
  operationSequence += 1;
1959
1802
  lastMutationSequence = operationSequence;
1803
+ if (code) lastCodeMutationSequence = operationSequence;
1960
1804
  lastMutationAt = new Date().toISOString();
1961
1805
  };
1962
1806
 
@@ -1972,30 +1816,35 @@ export function createFramerAgentToolRuntime({
1972
1816
  ? {
1973
1817
  behaviors: taskPlan.behaviors.map((behavior) => ({
1974
1818
  ...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],
1819
+ targetNodeIds: [...behavior.targetNodeIds],
1988
1820
  })),
1989
1821
  }
1990
1822
  : {}),
1991
1823
  };
1992
1824
  };
1825
+ // Only checks observed after the latest mutation describe the current project.
1826
+ const freshInteractionChecks = () =>
1827
+ [...interactionCheckResults.values()]
1828
+ .filter((entry) => entry.sequence > lastMutationSequence)
1829
+ .map((entry) => entry.result);
1830
+ const interactionSummary = () =>
1831
+ summarizeBehaviorVerification(taskPlan, freshInteractionChecks());
1832
+ const interactionStatus = () => {
1833
+ if (!taskPlan?.behaviors?.length || mutationCalls === 0) return 'not_required';
1834
+ return interactionSummary().status;
1835
+ };
1993
1836
  const synchronized = () =>
1994
1837
  mutationCalls === 0 || lastInspectionSequence > lastMutationSequence;
1995
1838
  const verificationCheckCompleted = (check) => {
1996
1839
  if (mutationCalls === 0) return true;
1997
1840
  if (check === 'structural') return synchronized();
1998
- return Number(verificationEvidence.get(check) || 0) > lastMutationSequence;
1841
+ if (check === 'interactions') {
1842
+ return ['verified', 'not_required'].includes(interactionStatus());
1843
+ }
1844
+ // Canvas edits cannot change a code file, so only code edits stale it.
1845
+ const staleAfter =
1846
+ check === 'code' ? lastCodeMutationSequence : lastMutationSequence;
1847
+ return Number(verificationEvidence.get(check) || 0) > staleAfter;
1999
1848
  };
2000
1849
  const visualVerificationRequired = () =>
2001
1850
  requiredVerificationChecks().includes('visual');
@@ -2013,12 +1862,19 @@ export function createFramerAgentToolRuntime({
2013
1862
  const requiredChecks = () => [
2014
1863
  ...new Set(['structural', ...requiredVerificationChecks()]),
2015
1864
  ];
1865
+ // Checks the model attempted but that live evidence could not confirm. They
1866
+ // are reported as unverified and do not fail the run.
1867
+ const unavailableChecks = () => [
1868
+ ...(visualVerificationStatus() === 'unavailable' ? ['visual'] : []),
1869
+ ...(interactionStatus() === 'unverified' ? ['interactions'] : []),
1870
+ ];
2016
1871
  const missingVerificationChecks = () => {
2017
1872
  if (mutationCalls === 0) return [];
2018
1873
  const missing = [];
2019
1874
  if (!taskPlan) missing.push('task-plan');
1875
+ const unavailable = unavailableChecks();
2020
1876
  for (const check of requiredChecks()) {
2021
- if (check === 'visual' && visualVerificationUnavailable()) continue;
1877
+ if (unavailable.includes(check)) continue;
2022
1878
  if (!verificationCheckCompleted(check)) missing.push(check);
2023
1879
  }
2024
1880
  return missing;
@@ -2046,48 +1902,17 @@ export function createFramerAgentToolRuntime({
2046
1902
  ? {
2047
1903
  interactionVerificationCalls,
2048
1904
  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,
1905
+ status: interactionStatus(),
1906
+ behaviors: taskPlan?.behaviors?.length ? interactionSummary().behaviors : [],
1907
+ checks: freshInteractionChecks().map((check) => ({ ...check })),
1908
+ verifiedAt: interactionVerifiedAt,
2083
1909
  },
2084
1910
  }
2085
1911
  : {}),
2086
1912
  taskPlan: taskPlanSnapshot(),
2087
1913
  requiredChecks: requiredChecks(),
2088
1914
  completedChecks: requiredChecks().filter(verificationCheckCompleted),
2089
- unavailableChecks:
2090
- visualVerificationStatus() === 'unavailable' ? ['visual'] : [],
1915
+ unavailableChecks: unavailableChecks(),
2091
1916
  missingChecks: missingVerificationChecks(),
2092
1917
  lastMutationAt,
2093
1918
  lastInspectionAt,
@@ -2109,49 +1934,58 @@ export function createFramerAgentToolRuntime({
2109
1934
  };
2110
1935
  };
2111
1936
 
2112
- const withUnavailableVisualCheck = (completion) =>
2113
- visualVerificationStatus() === 'unavailable'
2114
- ? {
2115
- ...completion,
2116
- checks: [
2117
- ...(Array.isArray(completion?.checks) ? completion.checks : []),
2118
- {
1937
+ const withUnavailableChecks = (completion) => {
1938
+ const unavailable = unavailableChecks();
1939
+ if (unavailable.length === 0) return completion;
1940
+ const unverifiedBehaviors = interactionSummary().behaviors
1941
+ .filter((behavior) => behavior.verdict === 'unknown')
1942
+ .map((behavior) => `${behavior.behaviorId} (${behavior.unknowns[0] || 'not confirmed'})`);
1943
+ return {
1944
+ ...completion,
1945
+ checks: [
1946
+ ...(Array.isArray(completion?.checks) ? completion.checks : []),
1947
+ ...(unavailable.includes('visual')
1948
+ ? [{
2119
1949
  command: 'Visual verification',
2120
1950
  status: 'skipped',
2121
1951
  output:
2122
1952
  'Screenshots of the final canvas could not be delivered to the model, so the visual result is unverified.',
2123
- },
2124
- ],
2125
- }
2126
- : completion;
1953
+ }]
1954
+ : []),
1955
+ ...(unavailable.includes('interactions')
1956
+ ? [{
1957
+ command: 'Interaction verification',
1958
+ status: 'skipped',
1959
+ output:
1960
+ `The live project did not expose evidence for: ${unverifiedBehaviors.join('; ')}. These behaviors are unverified.`,
1961
+ }]
1962
+ : []),
1963
+ ],
1964
+ };
1965
+ };
2127
1966
 
2128
1967
  const finalizeCompletion = (completion) => {
2129
1968
  if (completion?.status !== 'completed' || mutationCalls === 0) {
2130
1969
  return completion;
2131
1970
  }
2132
- if (verification().completed) return withUnavailableVisualCheck(completion);
1971
+ if (verification().completed) return withUnavailableChecks(completion);
2133
1972
  const missing = missingVerificationChecks().map((check) => {
2134
1973
  if (check === 'task-plan') return 'a harness task and verification plan';
2135
1974
  if (check === 'structural') return 'a final structural read';
2136
1975
  if (check === 'visual') return 'post-mutation screenshot verification';
2137
1976
  return `post-mutation ${check} verification`;
2138
1977
  });
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
- });
1978
+ const interactionDetails = (taskPlan?.behaviors?.length ? interactionSummary().behaviors : [])
1979
+ .filter((behavior) => behavior.verdict === 'contradicted' || !behavior.checked)
1980
+ .map((behavior) =>
1981
+ `${behavior.behaviorId}=${behavior.verdict === 'contradicted' ? behavior.contradictions[0] : 'not checked after the latest change'}`);
2148
1982
  const reason = [
2149
1983
  `The harness ended before completing ${missing.join(' and ')}.`,
2150
1984
  ...(interactionDetails.length > 0
2151
1985
  ? [`Interaction evidence: ${interactionDetails.join('; ')}.`]
2152
1986
  : []),
2153
1987
  ].join(' ');
2154
- const failed = withUnavailableVisualCheck(completion);
1988
+ const failed = withUnavailableChecks(completion);
2155
1989
  return {
2156
1990
  ...failed,
2157
1991
  status: 'failed',
@@ -2170,6 +2004,22 @@ export function createFramerAgentToolRuntime({
2170
2004
  };
2171
2005
  };
2172
2006
 
2007
+ // A model that reports failure without changing anything and without a
2008
+ // refused plan or edit gave up on its own. It gets one prompt to continue.
2009
+ const unblockedFailurePrompt = (completion) => {
2010
+ if (completion?.status !== 'failed' || mutationCalls > 0) return null;
2011
+ const lastGate = [...toolCalls]
2012
+ .reverse()
2013
+ .find((call) => call.category === 'plan' || call.category === 'mutation');
2014
+ if (lastGate?.status === 'failed') return null;
2015
+ return [
2016
+ 'You returned failed without changing the Framer project, and no tool refused the work.',
2017
+ 'Nothing in this run is blocking you and there is no time limit.',
2018
+ 'Continue the same task from the current project state: plan it, apply the change, verify it, then return the required JSON outcome.',
2019
+ 'Return failed or blocked only for a concrete blocker you can name.',
2020
+ ].join(' ');
2021
+ };
2022
+
2173
2023
  const verificationRepairPrompt = () => {
2174
2024
  const current = verification();
2175
2025
  if (current.completed || mutationCalls === 0) return null;
@@ -2185,27 +2035,33 @@ export function createFramerAgentToolRuntime({
2185
2035
  missing.push(
2186
2036
  'capture and inspect a screenshot of the affected canvas after the latest mutation',
2187
2037
  );
2038
+ } else if (check === 'interactions') {
2039
+ const summary = interactionSummary();
2040
+ const unchecked = summary.behaviors
2041
+ .filter((behavior) => !behavior.checked)
2042
+ .map((behavior) => behavior.uncheckedTargets.length
2043
+ ? `${behavior.behaviorId} on ${behavior.uncheckedTargets.join(', ')}`
2044
+ : behavior.behaviorId);
2045
+ const contradicted = summary.behaviors
2046
+ .filter((behavior) => behavior.verdict === 'contradicted')
2047
+ .map((behavior) => behavior.contradictions[0]);
2048
+ missing.push([
2049
+ unchecked.length
2050
+ ? `call framer_verify_interactions for ${unchecked.join('; ')}, naming the Framer property that carries each behavior`
2051
+ : '',
2052
+ contradicted.length
2053
+ ? `fix or explain these mismatches: ${contradicted.join('; ')}`
2054
+ : '',
2055
+ ].filter(Boolean).join(' and '));
2188
2056
  } 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
- );
2057
+ missing.push(`perform a focused read tagged with verifies:["${check}"] after the latest mutation`);
2202
2058
  }
2203
2059
  }
2204
2060
  return [
2205
2061
  'Continue the same Framer task from its current project state.',
2206
2062
  'Do not redo successful work.',
2207
2063
  `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.',
2064
+ '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
2065
  ].join(' ');
2210
2066
  };
2211
2067
 
@@ -2250,109 +2106,13 @@ export function createFramerAgentToolRuntime({
2250
2106
  .update(`${name}\n${JSON.stringify(rawArguments)}`)
2251
2107
  .digest('hex');
2252
2108
 
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 = '' } = {}) => {
2109
+ const requireTaskPlanForMutation = () => {
2268
2110
  if (!taskPlan) {
2269
2111
  throw toolError(
2270
2112
  'FRAMER_HARNESS_PLAN_REQUIRED',
2271
2113
  'Inspect the live Framer target, then call framer_plan_task before making a mutation.',
2272
2114
  );
2273
2115
  }
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
2116
  };
2357
2117
 
2358
2118
  async function invokeUnsafe(name, rawArguments = {}) {
@@ -2444,7 +2204,40 @@ export function createFramerAgentToolRuntime({
2444
2204
  const queries = Array.isArray(rawArguments.queries)
2445
2205
  ? uniqueStrings(rawArguments.queries, 8)
2446
2206
  : [];
2447
- if (queries.length > 0 && !instructionLookupUnavailable) {
2207
+ const guideIds = Array.isArray(rawArguments.guides)
2208
+ ? uniqueStrings(rawArguments.guides, 6)
2209
+ : [];
2210
+ if (guideIds.length > 0) {
2211
+ const guides = guideIds.flatMap((id) => {
2212
+ const entry = guideCatalog.get(id);
2213
+ const loaded = entry
2214
+ ? readGuidanceFile(
2215
+ entry.root,
2216
+ entry.path,
2217
+ MAX_GUIDANCE_FILE_BYTES,
2218
+ entry.section,
2219
+ )
2220
+ : null;
2221
+ if (!loaded) return [];
2222
+ if (!guidesRead.includes(id)) guidesRead.push(id);
2223
+ return [{ id, content: loaded.content }];
2224
+ });
2225
+ const unknown = guideIds.filter(
2226
+ (id) => !guides.some((guide) => guide.id === id),
2227
+ );
2228
+ result = {
2229
+ stdout: JSON.stringify({
2230
+ guides,
2231
+ ...(unknown.length > 0
2232
+ ? {
2233
+ unknownGuides: unknown,
2234
+ availableGuides: [...guideCatalog.keys()],
2235
+ }
2236
+ : {}),
2237
+ }),
2238
+ stderr: '',
2239
+ };
2240
+ } else if (queries.length > 0 && !instructionLookupUnavailable) {
2448
2241
  try {
2449
2242
  result = await executeCli(
2450
2243
  ['docs', ...queries],
@@ -2487,22 +2280,7 @@ export function createFramerAgentToolRuntime({
2487
2280
  'Inspect the live Framer target before declaring the task and verification plan.',
2488
2281
  );
2489
2282
  }
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);
2283
+ const nextPlan = normalizeTaskPlan(rawArguments);
2506
2284
  taskPlan = {
2507
2285
  summary: nextPlan.summary,
2508
2286
  domains: uniqueStrings([
@@ -2513,280 +2291,55 @@ export function createFramerAgentToolRuntime({
2513
2291
  ...(taskPlan?.verification || []),
2514
2292
  ...nextPlan.verification,
2515
2293
  ]),
2516
- ...behaviorPlan,
2294
+ ...mergeBehaviorPlans(taskPlan, nextPlan),
2517
2295
  };
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
2296
  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
- }),
2297
+ stdout: JSON.stringify({ plan: taskPlanSnapshot() }),
2556
2298
  stderr: '',
2557
2299
  };
2558
2300
  } 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
2301
  if (!taskPlan?.behaviors?.length) {
2566
2302
  throw toolError(
2567
2303
  '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.`,
2304
+ 'Declare the behaviors to confirm in framer_plan_task before verifying them.',
2585
2305
  );
2586
2306
  }
2307
+ const checks = normalizeBehaviorEvidence(rawArguments.checks, taskPlan);
2587
2308
  const pagePath = normalizePagePath(rawArguments.pagePath);
2588
- const requestedIds = evidenceNodeIds(checks);
2589
- const componentIds = uniqueStrings(
2590
- checks.map((check) => check.componentNodeId),
2591
- );
2592
2309
  const liveResult = await execCode(
2593
2310
  [
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));',
2311
+ `const requestedIds = ${JSON.stringify(evidenceNodeIds(checks))};`,
2312
+ 'const nodes = await Promise.all(requestedIds.map(async (id) => ({',
2313
+ ' id,',
2314
+ ' node: await framer.agent.serialize({ id, depth: 1 },',
2315
+ ` { pagePath: ${JSON.stringify(pagePath)} }).catch(() => null),`,
2316
+ '})));',
2317
+ 'console.log(JSON.stringify({ nodes }, null, 2));',
2612
2318
  ].join('\n'),
2613
2319
  );
2614
2320
  const parsed = parseStructuredOutput(liveResult.stdout);
2615
- const nodesById = new Map(
2321
+ const nodes = new Map(
2616
2322
  (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
- }]),
2323
+ .filter((entry) => isRecord(entry) && typeof entry.id === 'string' && entry.node)
2324
+ .map((entry) => [entry.id, entry.node]),
2628
2325
  );
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
- });
2326
+ const results = verifyBehaviorChecks({ checks, nodes });
2640
2327
  markInspection();
2641
2328
  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');
2329
+ interactionVerifiedAt = new Date().toISOString();
2330
+ for (const checkResult of results) {
2331
+ interactionCheckResults.set(
2332
+ `${checkResult.behaviorId}\n${checkResult.nodeId}\n${checkResult.property}`,
2333
+ { sequence: operationSequence, result: checkResult },
2334
+ );
2787
2335
  }
2336
+ const summary = interactionSummary();
2788
2337
  result = {
2789
- stdout: JSON.stringify(interactionVerification),
2338
+ stdout: JSON.stringify({
2339
+ status: summary.status,
2340
+ checks: results,
2341
+ behaviors: summary.behaviors,
2342
+ }),
2790
2343
  stderr: liveResult.stderr,
2791
2344
  };
2792
2345
  } else if (name === 'framer_read_project') {
@@ -3079,7 +2632,7 @@ export function createFramerAgentToolRuntime({
3079
2632
  );
3080
2633
  }
3081
2634
  mutationCalls += 1;
3082
- markMutation();
2635
+ markMutation({ code: true });
3083
2636
  const mutationSequence = operationSequence;
3084
2637
  markInspection({
3085
2638
  verifies: parsed.typecheck?.passed === true ? ['code'] : [],
@@ -3116,8 +2669,7 @@ export function createFramerAgentToolRuntime({
3116
2669
  'Framer Agent changes are required.',
3117
2670
  );
3118
2671
  }
3119
- const mutatedRepresentativeBehaviorIds =
3120
- requireTaskPlanForMutation({ changes });
2672
+ requireTaskPlanForMutation();
3121
2673
  const pagePath = normalizePagePath(rawArguments.pagePath);
3122
2674
  result = await execCode(
3123
2675
  [
@@ -3128,8 +2680,6 @@ export function createFramerAgentToolRuntime({
3128
2680
  mutationCalls += 1;
3129
2681
  applyChangesCalls += 1;
3130
2682
  markMutation();
3131
- mutatedRepresentativeBehaviorIds.forEach((behaviorId) =>
3132
- representativeMutatedBehaviorIds.add(behaviorId));
3133
2683
  const parsedResult = parseStructuredOutput(result.stdout);
3134
2684
  mutationReceipts.push({
3135
2685
  sequence: operationSequence,
@@ -3203,6 +2753,9 @@ export function createFramerAgentToolRuntime({
3203
2753
  name === 'framer_read_code_file'
3204
2754
  ? normalizedCodeFileReadResult(result)
3205
2755
  : normalizedResult(result);
2756
+ // Result size is what this call adds to every later model turn.
2757
+ toolCall.resultBytes = Buffer.byteLength(JSON.stringify(normalized), 'utf8');
2758
+ if (modelImages.length > 0) toolCall.imageCount = modelImages.length;
3206
2759
  if (name !== 'framer_read_project') return normalized;
3207
2760
  const screenshotCount = screenshotQueries(rawArguments).length;
3208
2761
  return {
@@ -3353,12 +2906,14 @@ export function createFramerAgentToolRuntime({
3353
2906
  inspect: inspectFinalState,
3354
2907
  finalizeCompletion,
3355
2908
  verificationRepairPrompt,
2909
+ unblockedFailurePrompt,
3356
2910
  sourceHash: () => null,
3357
2911
  hasPendingChanges: () => false,
3358
2912
  directWorkspace: true,
3359
2913
  summary: () => ({
3360
2914
  projectId,
3361
2915
  projectUrl,
2916
+ unresolvedTargets: unresolvedTargets.map((target) => ({ ...target })),
3362
2917
  mutationCalls,
3363
2918
  applyChangesCalls,
3364
2919
  lowLevelWriteCalls,
@@ -3412,10 +2967,6 @@ export function createFramerAgentToolRuntime({
3412
2967
  lowLevelWriteDisabled,
3413
2968
  lowLevelWriteBlockedCalls,
3414
2969
  instructionFallbacks,
3415
- interactionVerificationAttemptLimit:
3416
- MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR,
3417
- interactionRepairLimit:
3418
- MAX_INTERACTION_REPAIRS_PER_BEHAVIOR,
3419
2970
  },
3420
2971
  branch: branchChange,
3421
2972
  lastInspectionAt,
@@ -3442,6 +2993,7 @@ export function createFramerAgentToolRuntime({
3442
2993
  projectContextHash: guidanceMetadata.projectContextHash,
3443
2994
  files: [...guidanceMetadata.files],
3444
2995
  bytes: guidanceMetadata.bytes,
2996
+ guidesRead: [...guidesRead],
3445
2997
  },
3446
2998
  doctrine: { ...promptMetadata },
3447
2999
  }),