@gakim-digital/dexter-bridge 0.11.13 → 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
  },
@@ -619,7 +413,7 @@ function installedFramerAgentVersion() {
619
413
  return null;
620
414
  }
621
415
 
622
- function readGuidanceFile(projectRoot, relativePath, remainingBytes) {
416
+ function readGuidanceFile(projectRoot, relativePath, remainingBytes, section) {
623
417
  const normalized = relativePath.replaceAll('\\', '/');
624
418
  if (
625
419
  normalized.startsWith('/')
@@ -635,12 +429,71 @@ function readGuidanceFile(projectRoot, relativePath, remainingBytes) {
635
429
  }
636
430
  const maximum = Math.min(MAX_GUIDANCE_FILE_BYTES, remainingBytes);
637
431
  if (maximum <= 0) return null;
432
+ let content = fs.readFileSync(absolutePath, 'utf8');
433
+ if (section) {
434
+ content = markdownSection(content, section);
435
+ if (!content) return null;
436
+ }
638
437
  return {
639
438
  path: normalized,
640
- content: boundedText(fs.readFileSync(absolutePath, 'utf8'), maximum),
439
+ content: boundedText(content, maximum),
641
440
  };
642
441
  }
643
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
+
487
+ function markdownSection(markdown, heading) {
488
+ const lines = markdown.split('\n');
489
+ const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
490
+ if (start < 0) return '';
491
+ const end = lines.findIndex(
492
+ (line, index) => index > start && line.startsWith('## '),
493
+ );
494
+ return lines.slice(start, end < 0 ? undefined : end).join('\n').trim();
495
+ }
496
+
644
497
  function normalizeProjectId(value) {
645
498
  const projectId = String(value || '').trim();
646
499
  if (!PROJECT_ID_PATTERN.test(projectId)) {
@@ -764,7 +617,7 @@ function normalizeVerificationValues(values, allowed) {
764
617
  .filter((value) => allowed.includes(value));
765
618
  }
766
619
 
767
- function normalizeTaskPlan(rawArguments, inheritedRejectedMechanisms = []) {
620
+ function normalizeTaskPlan(rawArguments) {
768
621
  const summary = String(rawArguments.summary || '').trim().slice(0, 1000);
769
622
  const domains = normalizeVerificationValues(
770
623
  rawArguments.domains,
@@ -783,19 +636,7 @@ function normalizeTaskPlan(rawArguments, inheritedRejectedMechanisms = []) {
783
636
  // The model's declared verification is authoritative; domains describe the
784
637
  // work and never add checks on their own.
785
638
  const verification = new Set(['structural', ...requestedVerification]);
786
- const behaviorPlan = normalizeBehaviorPlan(
787
- rawArguments,
788
- inheritedRejectedMechanisms,
789
- );
790
- if (
791
- domains.includes('interactions')
792
- && behaviorPlan.behaviors.length === 0
793
- ) {
794
- throw toolError(
795
- 'FRAMER_BEHAVIOR_CONTRACT_REQUIRED',
796
- 'Interaction work requires observable behaviors and one compatible mechanism decision per behavior.',
797
- );
798
- }
639
+ const behaviorPlan = normalizeBehaviorPlan(rawArguments);
799
640
  if (behaviorPlan.behaviors.length > 0) {
800
641
  if (!domains.includes('interactions')) domains.push('interactions');
801
642
  verification.add('interactions');
@@ -921,6 +762,28 @@ function authoritativeNodeIdsFromAssignment(assignment) {
921
762
  ]);
922
763
  }
923
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
+
924
787
  function validateCode(value, { write = false, allowPublishing = false } = {}) {
925
788
  const code = String(value || '').trim();
926
789
  if (!code) {
@@ -929,7 +792,7 @@ function validateCode(value, { write = false, allowPublishing = false } = {}) {
929
792
  'Framer Agent code is required.',
930
793
  );
931
794
  }
932
- if (UNSAFE_EXEC_PATTERN.test(code)) {
795
+ if (UNSAFE_EXEC_PATTERN.test(codeOutsideLiterals(code))) {
933
796
  throw toolError(
934
797
  'FRAMER_AGENT_CODE_UNSAFE',
935
798
  'Framer Agent code may use only the provided framer, state, and console globals.',
@@ -1231,32 +1094,9 @@ export function createFramerAgentToolRuntime({
1231
1094
  );
1232
1095
  }
1233
1096
  const allowPublishing = assignment?.context?.permissions?.publish === true;
1234
- const inheritedRejectedMechanisms =
1235
- assignment?.context?.repairContext?.rejectedMechanisms
1236
- || assignment?.repairContext?.rejectedMechanisms
1237
- || [];
1238
- const authorizedInstanceContainerIds = uniqueStrings(
1239
- assignment?.context?.permissions?.scopedComponentArtifacts
1240
- ?.instanceContainerNodeIds
1241
- || assignment?.context?.contextGrant?.writableNodeIds
1242
- || [],
1243
- );
1244
1097
  const activePagePath = normalizePagePath(
1245
1098
  assignment?.context?.activePagePath,
1246
1099
  );
1247
- const semanticVerifierMode =
1248
- ['off', 'shadow', 'enforce'].includes(
1249
- String(
1250
- assignment?.context?.harnessAuthority?.semanticVerifierMode
1251
- || env.DEXTER_FRAMER_SEMANTIC_VERIFIER_MODE
1252
- || '',
1253
- ).trim(),
1254
- )
1255
- ? String(
1256
- assignment?.context?.harnessAuthority?.semanticVerifierMode
1257
- || env.DEXTER_FRAMER_SEMANTIC_VERIFIER_MODE,
1258
- ).trim()
1259
- : 'enforce';
1260
1100
  const authoritativeNodeIds =
1261
1101
  authoritativeNodeIdsFromAssignment(assignment);
1262
1102
  let sessionId = null;
@@ -1288,20 +1128,10 @@ export function createFramerAgentToolRuntime({
1288
1128
  let lastMutationAt = null;
1289
1129
  let screenshotCalls = 0;
1290
1130
  let interactionVerificationCalls = 0;
1291
- let interactionVerification = {
1292
- completed: false,
1293
- verificationLevel: 'unknown',
1294
- checks: [],
1295
- behaviors: [],
1296
- budgetExhaustedBehaviorIds: [],
1297
- verifiedAt: null,
1298
- };
1299
- const representativeVerifiedBehaviorIds = new Set();
1300
- const representativeMutatedBehaviorIds = new Set();
1301
- const representativeRepairNodeIds = new Set();
1302
- const representativeRepairBehaviorByNode = new Map();
1303
- const representativeEvidenceBehaviorByNode = new Map();
1304
- 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;
1305
1135
  const mutationReceipts = [];
1306
1136
  let lastScreenshotSequence = 0;
1307
1137
  let lastScreenshotAttemptSequence = 0;
@@ -1311,6 +1141,8 @@ export function createFramerAgentToolRuntime({
1311
1141
  const screenshotFailures = [];
1312
1142
  let taskPlan = null;
1313
1143
  const verificationEvidence = new Map();
1144
+ let guideCatalog = new Map();
1145
+ const guidesRead = [];
1314
1146
  let guidanceMetadata = {
1315
1147
  loaded: false,
1316
1148
  contentHash: null,
@@ -1319,6 +1151,7 @@ export function createFramerAgentToolRuntime({
1319
1151
  bytes: 0,
1320
1152
  };
1321
1153
  let promptMetadata = {
1154
+ promptBytes: 0,
1322
1155
  doctrineLoaded: false,
1323
1156
  doctrineVersion: null,
1324
1157
  };
@@ -1328,6 +1161,7 @@ export function createFramerAgentToolRuntime({
1328
1161
  const framerAgentVersion = installedFramerAgentVersion();
1329
1162
  let operationSequence = 0;
1330
1163
  let lastMutationSequence = 0;
1164
+ let lastCodeMutationSequence = 0;
1331
1165
  let lastInspectionSequence = 0;
1332
1166
  const abortController = new AbortController();
1333
1167
 
@@ -1634,6 +1468,10 @@ export function createFramerAgentToolRuntime({
1634
1468
  if (!fs.existsSync(projectRoot)) {
1635
1469
  return { content: '', ...guidanceMetadata };
1636
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.
1637
1475
  const requestedFiles = [
1638
1476
  { root: skillRoot, path: 'SKILL.md', label: 'SKILL.md' },
1639
1477
  { root: projectRoot, path: 'index.md', label: 'project/index.md' },
@@ -1642,20 +1480,19 @@ export function createFramerAgentToolRuntime({
1642
1480
  path: 'project-inventory.md',
1643
1481
  label: 'project/project-inventory.md',
1644
1482
  },
1645
- { root: projectRoot, path: 'prompt/core-principles.md' },
1646
- { root: projectRoot, path: 'prompt/core-examples.md' },
1647
- { root: projectRoot, path: 'prompt/implementation-strategy.md' },
1648
- { root: projectRoot, path: 'prompt/updating-the-project.md' },
1649
- { root: projectRoot, path: 'prompt/tools.md' },
1650
1483
  { root: projectRoot, path: 'prompt/overview.md' },
1484
+ { root: projectRoot, path: 'prompt/core-principles.md' },
1651
1485
  { root: projectRoot, path: 'prompt/guardrails.md' },
1652
1486
  { root: projectRoot, path: 'prompt/critical-reminders.md' },
1653
- ...(visualVerificationRequired
1654
- ? [
1655
- { root: projectRoot, path: 'prompt/design-rules.md' },
1656
- { root: projectRoot, path: 'prompt/how-projects-work.md' },
1657
- ]
1658
- : []),
1487
+ { root: projectRoot, path: 'prompt/design-rules.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
+ })),
1659
1496
  ];
1660
1497
  const files = [];
1661
1498
  let remainingBytes = MAX_GUIDANCE_BYTES;
@@ -1664,6 +1501,7 @@ export function createFramerAgentToolRuntime({
1664
1501
  requestedFile.root,
1665
1502
  requestedFile.path,
1666
1503
  remainingBytes,
1504
+ requestedFile.section,
1667
1505
  );
1668
1506
  if (!loaded) continue;
1669
1507
  files.push({
@@ -1673,9 +1511,19 @@ export function createFramerAgentToolRuntime({
1673
1511
  remainingBytes -= Buffer.byteLength(loaded.content, 'utf8');
1674
1512
  if (remainingBytes <= 0) break;
1675
1513
  }
1676
- const content = files
1677
- .map((file) => `## ${file.path}\n\n${file.content}`)
1678
- .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');
1679
1527
  const inventory = files.find(
1680
1528
  (file) => file.path.endsWith('project-inventory.md'),
1681
1529
  )?.content || '';
@@ -1696,8 +1544,10 @@ export function createFramerAgentToolRuntime({
1696
1544
  const configurePromptContext = ({
1697
1545
  doctrineLoaded = false,
1698
1546
  doctrineVersion = null,
1547
+ promptBytes = 0,
1699
1548
  } = {}) => {
1700
1549
  promptMetadata = {
1550
+ promptBytes: Math.max(0, Number(promptBytes) || 0),
1701
1551
  doctrineLoaded: doctrineLoaded === true,
1702
1552
  doctrineVersion:
1703
1553
  typeof doctrineVersion === 'string' && doctrineVersion.trim()
@@ -1838,9 +1688,14 @@ export function createFramerAgentToolRuntime({
1838
1688
  return result;
1839
1689
  };
1840
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 = [];
1841
1696
  const validateAuthoritativeNodeIds = async () => {
1842
1697
  await ensureSession();
1843
- if (authoritativeNodeIds.length === 0) return;
1698
+ if (authoritativeNodeIds.length === 0) return { unresolvedTargets };
1844
1699
  if (nodeValidationPromise) return nodeValidationPromise;
1845
1700
  nodeValidationPromise = (async () => {
1846
1701
  reportActivity({
@@ -1852,9 +1707,13 @@ export function createFramerAgentToolRuntime({
1852
1707
  [
1853
1708
  `const requestedIds = ${JSON.stringify(authoritativeNodeIds)};`,
1854
1709
  'const resolved = await Promise.all(requestedIds.map(async (requestedId) => {',
1855
- ' const node = await framer.agent.getNode({ id: requestedId },',
1856
- ` { pagePath: ${JSON.stringify(activePagePath)} }).catch(() => null);`,
1857
- ' 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
+ ' }',
1858
1717
  '}));',
1859
1718
  'console.log(JSON.stringify({ resolved }));',
1860
1719
  ].join('\n'),
@@ -1864,26 +1723,30 @@ export function createFramerAgentToolRuntime({
1864
1723
  const resolved = Array.isArray(parsed?.resolved)
1865
1724
  ? parsed.resolved
1866
1725
  : [];
1867
- const invalid = authoritativeNodeIds.filter((requestedId) => {
1726
+ unresolvedTargets = authoritativeNodeIds.flatMap((requestedId) => {
1868
1727
  const match = resolved.find(
1869
1728
  (entry) =>
1870
1729
  isRecord(entry)
1871
1730
  && entry.requestedId === requestedId,
1872
1731
  );
1873
- 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
+ }];
1874
1741
  });
1875
- if (invalid.length > 0) {
1876
- throw toolError(
1877
- 'FRAMER_AGENT_NODE_ID_INVALID',
1878
- `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.`,
1879
- 409,
1880
- );
1881
- }
1882
1742
  reportActivity({
1883
1743
  kind: 'status_update',
1884
1744
  source: 'framer-agent',
1885
- 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.',
1886
1748
  });
1749
+ return { unresolvedTargets };
1887
1750
  })().catch((error) => {
1888
1751
  nodeValidationPromise = null;
1889
1752
  throw error;
@@ -1934,9 +1797,10 @@ export function createFramerAgentToolRuntime({
1934
1797
  }
1935
1798
  };
1936
1799
 
1937
- const markMutation = () => {
1800
+ const markMutation = ({ code = false } = {}) => {
1938
1801
  operationSequence += 1;
1939
1802
  lastMutationSequence = operationSequence;
1803
+ if (code) lastCodeMutationSequence = operationSequence;
1940
1804
  lastMutationAt = new Date().toISOString();
1941
1805
  };
1942
1806
 
@@ -1952,30 +1816,35 @@ export function createFramerAgentToolRuntime({
1952
1816
  ? {
1953
1817
  behaviors: taskPlan.behaviors.map((behavior) => ({
1954
1818
  ...behavior,
1955
- expectedContent: [...behavior.expectedContent],
1956
- expectedDestinations: [...behavior.expectedDestinations],
1957
- })),
1958
- mechanisms: taskPlan.mechanisms.map((decision) => ({
1959
- ...decision,
1960
- targetNodeIds: [...decision.targetNodeIds],
1961
- })),
1962
- rejectedMechanisms: taskPlan.rejectedMechanisms.map((entry) => ({
1963
- ...entry,
1964
- })),
1965
- compatibility: taskPlan.compatibility.map((entry) => ({
1966
- ...entry,
1967
- requiredCapabilities: [...entry.requiredCapabilities],
1819
+ targetNodeIds: [...behavior.targetNodeIds],
1968
1820
  })),
1969
1821
  }
1970
1822
  : {}),
1971
1823
  };
1972
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
+ };
1973
1836
  const synchronized = () =>
1974
1837
  mutationCalls === 0 || lastInspectionSequence > lastMutationSequence;
1975
1838
  const verificationCheckCompleted = (check) => {
1976
1839
  if (mutationCalls === 0) return true;
1977
1840
  if (check === 'structural') return synchronized();
1978
- 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;
1979
1848
  };
1980
1849
  const visualVerificationRequired = () =>
1981
1850
  requiredVerificationChecks().includes('visual');
@@ -1993,12 +1862,19 @@ export function createFramerAgentToolRuntime({
1993
1862
  const requiredChecks = () => [
1994
1863
  ...new Set(['structural', ...requiredVerificationChecks()]),
1995
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
+ ];
1996
1871
  const missingVerificationChecks = () => {
1997
1872
  if (mutationCalls === 0) return [];
1998
1873
  const missing = [];
1999
1874
  if (!taskPlan) missing.push('task-plan');
1875
+ const unavailable = unavailableChecks();
2000
1876
  for (const check of requiredChecks()) {
2001
- if (check === 'visual' && visualVerificationUnavailable()) continue;
1877
+ if (unavailable.includes(check)) continue;
2002
1878
  if (!verificationCheckCompleted(check)) missing.push(check);
2003
1879
  }
2004
1880
  return missing;
@@ -2026,48 +1902,17 @@ export function createFramerAgentToolRuntime({
2026
1902
  ? {
2027
1903
  interactionVerificationCalls,
2028
1904
  interactionVerification: {
2029
- completed: verificationCheckCompleted('interactions'),
2030
- verificationLevel:
2031
- verificationCheckCompleted('interactions')
2032
- ? interactionVerification.verificationLevel
2033
- : 'unknown',
2034
- checks: interactionVerification.checks.map((check) => ({
2035
- ...check,
2036
- contradictions: [...check.contradictions],
2037
- unknowns: [...check.unknowns],
2038
- observations: check.observations.map((observation) => ({
2039
- ...observation,
2040
- })),
2041
- nextSafeActions: [...check.nextSafeActions],
2042
- ...(check.semanticAssessment
2043
- ? {
2044
- semanticAssessment: {
2045
- ...check.semanticAssessment,
2046
- evidenceNodeIds: [
2047
- ...check.semanticAssessment.evidenceNodeIds,
2048
- ],
2049
- },
2050
- }
2051
- : {}),
2052
- })),
2053
- behaviors: interactionVerification.behaviors.map((behavior) => ({
2054
- ...behavior,
2055
- contradictions: [...behavior.contradictions],
2056
- unknowns: [...behavior.unknowns],
2057
- })),
2058
- semanticVerifierMode,
2059
- budgetExhaustedBehaviorIds: [
2060
- ...interactionVerification.budgetExhaustedBehaviorIds,
2061
- ],
2062
- verifiedAt: interactionVerification.verifiedAt,
1905
+ status: interactionStatus(),
1906
+ behaviors: taskPlan?.behaviors?.length ? interactionSummary().behaviors : [],
1907
+ checks: freshInteractionChecks().map((check) => ({ ...check })),
1908
+ verifiedAt: interactionVerifiedAt,
2063
1909
  },
2064
1910
  }
2065
1911
  : {}),
2066
1912
  taskPlan: taskPlanSnapshot(),
2067
1913
  requiredChecks: requiredChecks(),
2068
1914
  completedChecks: requiredChecks().filter(verificationCheckCompleted),
2069
- unavailableChecks:
2070
- visualVerificationStatus() === 'unavailable' ? ['visual'] : [],
1915
+ unavailableChecks: unavailableChecks(),
2071
1916
  missingChecks: missingVerificationChecks(),
2072
1917
  lastMutationAt,
2073
1918
  lastInspectionAt,
@@ -2089,49 +1934,58 @@ export function createFramerAgentToolRuntime({
2089
1934
  };
2090
1935
  };
2091
1936
 
2092
- const withUnavailableVisualCheck = (completion) =>
2093
- visualVerificationStatus() === 'unavailable'
2094
- ? {
2095
- ...completion,
2096
- checks: [
2097
- ...(Array.isArray(completion?.checks) ? completion.checks : []),
2098
- {
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
+ ? [{
2099
1949
  command: 'Visual verification',
2100
1950
  status: 'skipped',
2101
1951
  output:
2102
1952
  'Screenshots of the final canvas could not be delivered to the model, so the visual result is unverified.',
2103
- },
2104
- ],
2105
- }
2106
- : 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
+ };
2107
1966
 
2108
1967
  const finalizeCompletion = (completion) => {
2109
1968
  if (completion?.status !== 'completed' || mutationCalls === 0) {
2110
1969
  return completion;
2111
1970
  }
2112
- if (verification().completed) return withUnavailableVisualCheck(completion);
1971
+ if (verification().completed) return withUnavailableChecks(completion);
2113
1972
  const missing = missingVerificationChecks().map((check) => {
2114
1973
  if (check === 'task-plan') return 'a harness task and verification plan';
2115
1974
  if (check === 'structural') return 'a final structural read';
2116
1975
  if (check === 'visual') return 'post-mutation screenshot verification';
2117
1976
  return `post-mutation ${check} verification`;
2118
1977
  });
2119
- const interactionDetails = interactionVerification.behaviors
2120
- .filter((behavior) => behavior.verdict !== 'verified')
2121
- .map((behavior) => {
2122
- const detail =
2123
- behavior.contradictions[0]
2124
- || behavior.unknowns[0]
2125
- || 'no conclusive live evidence';
2126
- return `${behavior.behaviorId}=${behavior.verdict} (${detail})`;
2127
- });
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'}`);
2128
1982
  const reason = [
2129
1983
  `The harness ended before completing ${missing.join(' and ')}.`,
2130
1984
  ...(interactionDetails.length > 0
2131
1985
  ? [`Interaction evidence: ${interactionDetails.join('; ')}.`]
2132
1986
  : []),
2133
1987
  ].join(' ');
2134
- const failed = withUnavailableVisualCheck(completion);
1988
+ const failed = withUnavailableChecks(completion);
2135
1989
  return {
2136
1990
  ...failed,
2137
1991
  status: 'failed',
@@ -2150,6 +2004,22 @@ export function createFramerAgentToolRuntime({
2150
2004
  };
2151
2005
  };
2152
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
+
2153
2023
  const verificationRepairPrompt = () => {
2154
2024
  const current = verification();
2155
2025
  if (current.completed || mutationCalls === 0) return null;
@@ -2165,27 +2035,33 @@ export function createFramerAgentToolRuntime({
2165
2035
  missing.push(
2166
2036
  'capture and inspect a screenshot of the affected canvas after the latest mutation',
2167
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 '));
2168
2056
  } else {
2169
- const interactionState = interactionVerification.behaviors
2170
- .map((behavior) =>
2171
- `${behavior.behaviorId}:${behavior.verdict}`)
2172
- .join(', ');
2173
- const exhausted =
2174
- interactionVerification.budgetExhaustedBehaviorIds.length > 0;
2175
- missing.push(
2176
- check === 'interactions'
2177
- ? exhausted
2178
- ? `report interaction verification as incomplete without further mutation or repeated verification (${interactionState || 'unknown'})`
2179
- : `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'})`
2180
- : `perform a focused read tagged with verifies:["${check}"] after the latest mutation`,
2181
- );
2057
+ missing.push(`perform a focused read tagged with verifies:["${check}"] after the latest mutation`);
2182
2058
  }
2183
2059
  }
2184
2060
  return [
2185
2061
  'Continue the same Framer task from its current project state.',
2186
2062
  'Do not redo successful work.',
2187
2063
  `Before returning, ${missing.join(' and ')}.`,
2188
- '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.',
2189
2065
  ].join(' ');
2190
2066
  };
2191
2067
 
@@ -2230,109 +2106,13 @@ export function createFramerAgentToolRuntime({
2230
2106
  .update(`${name}\n${JSON.stringify(rawArguments)}`)
2231
2107
  .digest('hex');
2232
2108
 
2233
- const representativeRepairTargets = (changes) => {
2234
- const commands = summarizeFramerChanges(changes).commands;
2235
- if (
2236
- commands.length === 0
2237
- || commands.some((command) =>
2238
- command.operation !== 'set'
2239
- || !command.nodeId
2240
- || !representativeRepairNodeIds.has(command.nodeId))
2241
- ) {
2242
- return [];
2243
- }
2244
- return uniqueStrings(commands.map((command) => command.nodeId));
2245
- };
2246
-
2247
- const requireTaskPlanForMutation = ({ changes = '' } = {}) => {
2109
+ const requireTaskPlanForMutation = () => {
2248
2110
  if (!taskPlan) {
2249
2111
  throw toolError(
2250
2112
  'FRAMER_HARNESS_PLAN_REQUIRED',
2251
2113
  'Inspect the live Framer target, then call framer_plan_task before making a mutation.',
2252
2114
  );
2253
2115
  }
2254
- const repairTargets = representativeRepairTargets(changes);
2255
- const isRepresentativeRepair = repairTargets.length > 0;
2256
- const rejectedSelectedMechanisms = taskPlan.behaviors.filter((behavior) => {
2257
- const selected = taskPlan.mechanisms.find((decision) =>
2258
- decision.behaviorId === behavior.id);
2259
- return selected && taskPlan.rejectedMechanisms.some((rejected) =>
2260
- rejected.mechanism === selected.mechanism
2261
- && (
2262
- rejected.behaviorId === behavior.id
2263
- || rejected.behaviorSignature === behavior.signature
2264
- ));
2265
- });
2266
- if (rejectedSelectedMechanisms.length > 0) {
2267
- throw toolError(
2268
- 'FRAMER_BEHAVIOR_MECHANISM_REPLAN_REQUIRED',
2269
- `Choose a different mechanism for ${rejectedSelectedMechanisms.map((behavior) => behavior.id).join(', ')} before making another mutation.`,
2270
- );
2271
- }
2272
- const changedNodeIds = new Set(
2273
- summarizeFramerChanges(changes).commands
2274
- .flatMap((command) => [
2275
- command.nodeId,
2276
- command.sourceNodeId,
2277
- ])
2278
- .filter(Boolean),
2279
- );
2280
- const rawChanges = String(changes || '');
2281
- const pendingRepresentative = taskPlan.behaviors
2282
- .filter((behavior) => behavior.expectedTargets > 1)
2283
- .map((behavior) => ({
2284
- behavior,
2285
- decision: taskPlan.mechanisms.find((decision) =>
2286
- decision.behaviorId === behavior.id),
2287
- }))
2288
- .filter(({ decision }) => decision?.representativeFirst)
2289
- .filter(({ behavior }) =>
2290
- !representativeVerifiedBehaviorIds.has(behavior.id));
2291
- for (const { behavior, decision } of pendingRepresentative) {
2292
- const touchedTargets = decision.targetNodeIds.filter((nodeId) =>
2293
- rawChanges.includes(nodeId));
2294
- const touchedEvidence = [...changedNodeIds].filter((nodeId) =>
2295
- representativeEvidenceBehaviorByNode.get(nodeId) === behavior.id);
2296
- if (
2297
- touchedTargets.length > 1
2298
- || (
2299
- touchedTargets.length === 1
2300
- && touchedTargets[0] !== decision.representativeTargetNodeId
2301
- )
2302
- ) {
2303
- throw toolError(
2304
- 'FRAMER_BEHAVIOR_REPRESENTATIVE_MUTATION_REQUIRED',
2305
- `Implement only representative target ${decision.representativeTargetNodeId} for ${behavior.id} before changing the remaining targets.`,
2306
- );
2307
- }
2308
- if (touchedTargets.length === 0 && touchedEvidence.length === 0) {
2309
- continue;
2310
- }
2311
- if (!representativeMutatedBehaviorIds.has(behavior.id)) continue;
2312
- const attempt = behaviorVerificationAttempts.get(behavior.id);
2313
- const repairForBehavior =
2314
- isRepresentativeRepair
2315
- && repairTargets.every((nodeId) =>
2316
- representativeRepairBehaviorByNode.get(nodeId) === behavior.id);
2317
- if (
2318
- !repairForBehavior
2319
- || Number(attempt?.repairs || 0)
2320
- >= MAX_INTERACTION_REPAIRS_PER_BEHAVIOR
2321
- ) {
2322
- throw toolError(
2323
- 'FRAMER_BEHAVIOR_REPRESENTATIVE_VERIFICATION_REQUIRED',
2324
- `Verify representative target ${decision.representativeTargetNodeId} for ${behavior.id} before changing it again or batching the remaining targets.`,
2325
- );
2326
- }
2327
- behaviorVerificationAttempts.set(behavior.id, {
2328
- ...attempt,
2329
- repairs: Number(attempt?.repairs || 0) + 1,
2330
- });
2331
- }
2332
- return pendingRepresentative
2333
- .filter(({ decision }) =>
2334
- rawChanges.includes(decision.representativeTargetNodeId))
2335
- .map(({ behavior }) => behavior.id);
2336
2116
  };
2337
2117
 
2338
2118
  async function invokeUnsafe(name, rawArguments = {}) {
@@ -2424,7 +2204,40 @@ export function createFramerAgentToolRuntime({
2424
2204
  const queries = Array.isArray(rawArguments.queries)
2425
2205
  ? uniqueStrings(rawArguments.queries, 8)
2426
2206
  : [];
2427
- 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) {
2428
2241
  try {
2429
2242
  result = await executeCli(
2430
2243
  ['docs', ...queries],
@@ -2467,22 +2280,7 @@ export function createFramerAgentToolRuntime({
2467
2280
  'Inspect the live Framer target before declaring the task and verification plan.',
2468
2281
  );
2469
2282
  }
2470
- const nextPlan = normalizeTaskPlan(
2471
- rawArguments,
2472
- inheritedRejectedMechanisms,
2473
- );
2474
- const previousMechanisms = new Map(
2475
- (taskPlan?.mechanisms || []).map((decision) => [
2476
- decision.behaviorId,
2477
- JSON.stringify({
2478
- mechanism: decision.mechanism,
2479
- targetNodeIds: decision.targetNodeIds,
2480
- representativeTargetNodeId:
2481
- decision.representativeTargetNodeId,
2482
- }),
2483
- ]),
2484
- );
2485
- const behaviorPlan = mergeBehaviorPlans(taskPlan, nextPlan);
2283
+ const nextPlan = normalizeTaskPlan(rawArguments);
2486
2284
  taskPlan = {
2487
2285
  summary: nextPlan.summary,
2488
2286
  domains: uniqueStrings([
@@ -2493,280 +2291,55 @@ export function createFramerAgentToolRuntime({
2493
2291
  ...(taskPlan?.verification || []),
2494
2292
  ...nextPlan.verification,
2495
2293
  ]),
2496
- ...behaviorPlan,
2294
+ ...mergeBehaviorPlans(taskPlan, nextPlan),
2497
2295
  };
2498
- for (const decision of taskPlan.mechanisms) {
2499
- const prior = previousMechanisms.get(decision.behaviorId);
2500
- const current = JSON.stringify({
2501
- mechanism: decision.mechanism,
2502
- targetNodeIds: decision.targetNodeIds,
2503
- representativeTargetNodeId:
2504
- decision.representativeTargetNodeId,
2505
- });
2506
- if (prior && prior !== current) {
2507
- representativeVerifiedBehaviorIds.delete(decision.behaviorId);
2508
- representativeMutatedBehaviorIds.delete(decision.behaviorId);
2509
- behaviorVerificationAttempts.delete(decision.behaviorId);
2510
- }
2511
- }
2512
2296
  result = {
2513
- stdout: JSON.stringify({
2514
- plan: {
2515
- summary: taskPlan.summary,
2516
- domains: [...taskPlan.domains],
2517
- verification: [...taskPlan.verification],
2518
- behaviors: taskPlan.behaviors.map((behavior) => ({
2519
- ...behavior,
2520
- expectedContent: [...behavior.expectedContent],
2521
- })),
2522
- mechanisms: taskPlan.mechanisms.map((decision) => ({
2523
- ...decision,
2524
- })),
2525
- compatibility: taskPlan.compatibility.map((entry) => ({
2526
- ...entry,
2527
- requiredCapabilities: [...entry.requiredCapabilities],
2528
- })),
2529
- rejectedMechanisms: taskPlan.rejectedMechanisms.map((entry) => ({
2530
- ...entry,
2531
- })),
2532
- },
2533
- instruction:
2534
- 'Implement one representative first when required, verify it, then complete coverage and gather every derived verification result after the final mutation.',
2535
- }),
2297
+ stdout: JSON.stringify({ plan: taskPlanSnapshot() }),
2536
2298
  stderr: '',
2537
2299
  };
2538
2300
  } else if (name === 'framer_verify_interactions') {
2539
- if (mutationCalls === 0) {
2540
- throw toolError(
2541
- 'FRAMER_HARNESS_INTERACTION_MUTATION_REQUIRED',
2542
- 'Verify interactions only after the relevant Framer mutation has succeeded.',
2543
- );
2544
- }
2545
2301
  if (!taskPlan?.behaviors?.length) {
2546
2302
  throw toolError(
2547
2303
  'FRAMER_BEHAVIOR_CONTRACT_REQUIRED',
2548
- 'Declare observable behaviors and compatible mechanisms with framer_plan_task before interaction verification.',
2549
- );
2550
- }
2551
- const checks = normalizeBehaviorEvidence(
2552
- rawArguments.checks,
2553
- taskPlan,
2554
- );
2555
- const exhaustedBehaviorIds = uniqueStrings(
2556
- checks
2557
- .map((check) => check.behaviorId)
2558
- .filter((behaviorId) =>
2559
- behaviorVerificationAttempts.get(behaviorId)?.exhausted),
2560
- );
2561
- if (exhaustedBehaviorIds.length > 0) {
2562
- throw toolError(
2563
- 'FRAMER_INTERACTION_VERIFICATION_BUDGET_EXHAUSTED',
2564
- `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.',
2565
2305
  );
2566
2306
  }
2307
+ const checks = normalizeBehaviorEvidence(rawArguments.checks, taskPlan);
2567
2308
  const pagePath = normalizePagePath(rawArguments.pagePath);
2568
- const requestedIds = evidenceNodeIds(checks);
2569
- const componentIds = uniqueStrings(
2570
- checks.map((check) => check.componentNodeId),
2571
- );
2572
2309
  const liveResult = await execCode(
2573
2310
  [
2574
- `const requestedIds = ${JSON.stringify(requestedIds)};`,
2575
- `const componentIds = ${JSON.stringify(componentIds)};`,
2576
- 'const [nodes, componentControls] = await Promise.all([',
2577
- ' Promise.all(requestedIds.map(async (id) => ({',
2578
- ' id,',
2579
- ' node: await framer.agent.serialize({ id, depth: 8 },',
2580
- ` { pagePath: ${JSON.stringify(pagePath)} }).catch(() => null),`,
2581
- ' parentId: await framer.agent.getParentNode({ id },',
2582
- ` { pagePath: ${JSON.stringify(pagePath)} }).then((node) => node?.id ?? null).catch(() => null),`,
2583
- ' ancestorIds: await framer.agent.getAncestors({ id },',
2584
- ` { pagePath: ${JSON.stringify(pagePath)} }).then((nodes) => Array.isArray(nodes) ? nodes.map((node) => node?.id).filter(Boolean) : []).catch(() => []),`,
2585
- ' }))),',
2586
- ' componentIds.length > 0',
2587
- ' ? framer.agent.readComponentControls({ componentIds },',
2588
- ` { pagePath: ${JSON.stringify(pagePath)} }).catch(() => null)`,
2589
- ' : null,',
2590
- ']);',
2591
- '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));',
2592
2318
  ].join('\n'),
2593
2319
  );
2594
2320
  const parsed = parseStructuredOutput(liveResult.stdout);
2595
- const nodesById = new Map(
2321
+ const nodes = new Map(
2596
2322
  (Array.isArray(parsed?.nodes) ? parsed.nodes : [])
2597
- .filter((entry) => isRecord(entry) && typeof entry.id === 'string')
2598
- .map((entry) => [entry.id, {
2599
- node: entry.node || null,
2600
- parentId:
2601
- typeof entry.parentId === 'string'
2602
- ? entry.parentId
2603
- : null,
2604
- ancestorIds: Array.isArray(entry.ancestorIds)
2605
- ? entry.ancestorIds
2606
- : [],
2607
- }]),
2323
+ .filter((entry) => isRecord(entry) && typeof entry.id === 'string' && entry.node)
2324
+ .map((entry) => [entry.id, entry.node]),
2608
2325
  );
2609
- const verified = verifyBehaviorEvidence({
2610
- plan: taskPlan,
2611
- checks,
2612
- snapshots: nodesById,
2613
- context: {
2614
- lowLevelWriteCalls,
2615
- authorizedInstanceContainerIds,
2616
- componentControls: parsed?.componentControls || null,
2617
- semanticVerifierMode,
2618
- },
2619
- });
2326
+ const results = verifyBehaviorChecks({ checks, nodes });
2620
2327
  markInspection();
2621
2328
  interactionVerificationCalls += 1;
2622
- representativeRepairNodeIds.clear();
2623
- representativeRepairBehaviorByNode.clear();
2624
- representativeEvidenceBehaviorByNode.clear();
2625
- const checkedBehaviorIds = new Set(
2626
- checks.map((check) => check.behaviorId),
2627
- );
2628
- for (const behaviorResult of verified.behaviors) {
2629
- if (!checkedBehaviorIds.has(behaviorResult.behaviorId)) continue;
2630
- const behaviorChecks = verified.checks.filter((check) =>
2631
- check.behaviorId === behaviorResult.behaviorId);
2632
- for (const check of behaviorChecks) {
2633
- evidenceNodeIds([check]).forEach((nodeId) =>
2634
- representativeEvidenceBehaviorByNode.set(
2635
- nodeId,
2636
- behaviorResult.behaviorId,
2637
- ));
2638
- }
2639
- const deterministicallyContradicted = behaviorChecks.some((check) =>
2640
- check.deterministicVerdict === 'contradicted');
2641
- const prior = behaviorVerificationAttempts.get(
2642
- behaviorResult.behaviorId,
2643
- ) || {
2644
- attempts: 0,
2645
- unchangedEvidenceCount: 0,
2646
- repairs: 0,
2647
- fingerprint: null,
2648
- exhausted: false,
2649
- };
2650
- if (behaviorResult.verdict === 'verified') {
2651
- behaviorVerificationAttempts.delete(behaviorResult.behaviorId);
2652
- Object.assign(behaviorResult, {
2653
- verificationAttempts: prior.attempts + 1,
2654
- verificationAttemptsRemaining: Math.max(
2655
- 0,
2656
- MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR
2657
- - prior.attempts
2658
- - 1,
2659
- ),
2660
- repairAttempts: prior.repairs,
2661
- budgetExhausted: false,
2662
- });
2663
- continue;
2664
- }
2665
- const evidenceFingerprint = crypto
2666
- .createHash('sha256')
2667
- .update(JSON.stringify({
2668
- mechanism: behaviorResult.mechanism,
2669
- verdict: behaviorResult.verdict,
2670
- contradictions: [...behaviorResult.contradictions].sort(),
2671
- unknowns: [...behaviorResult.unknowns].sort(),
2672
- checks: behaviorChecks.map((check) => ({
2673
- verdict: check.verdict,
2674
- nodeIds: evidenceNodeIds([check]).sort(),
2675
- observations: check.observations,
2676
- })),
2677
- }))
2678
- .digest('hex');
2679
- const attempts = prior.attempts + 1;
2680
- const unchangedEvidenceCount =
2681
- prior.fingerprint === evidenceFingerprint
2682
- ? prior.unchangedEvidenceCount + 1
2683
- : 1;
2684
- const exhausted =
2685
- attempts >= MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR
2686
- || unchangedEvidenceCount >= 2
2687
- || (
2688
- behaviorResult.verdict === 'contradicted'
2689
- && prior.repairs >= MAX_INTERACTION_REPAIRS_PER_BEHAVIOR
2690
- );
2691
- behaviorVerificationAttempts.set(behaviorResult.behaviorId, {
2692
- attempts,
2693
- unchangedEvidenceCount,
2694
- repairs: prior.repairs,
2695
- fingerprint: evidenceFingerprint,
2696
- exhausted,
2697
- });
2698
- Object.assign(behaviorResult, {
2699
- verificationAttempts: attempts,
2700
- verificationAttemptsRemaining: Math.max(
2701
- 0,
2702
- MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR - attempts,
2703
- ),
2704
- repairAttempts: prior.repairs,
2705
- budgetExhausted: exhausted,
2706
- });
2707
- for (const check of behaviorChecks) {
2708
- check.repairAllowed =
2709
- check.deterministicVerdict === 'contradicted'
2710
- && prior.repairs < MAX_INTERACTION_REPAIRS_PER_BEHAVIOR
2711
- && !exhausted;
2712
- if (!check.repairAllowed) continue;
2713
- evidenceNodeIds([check]).forEach((nodeId) => {
2714
- representativeRepairNodeIds.add(nodeId);
2715
- representativeRepairBehaviorByNode.set(
2716
- nodeId,
2717
- behaviorResult.behaviorId,
2718
- );
2719
- });
2720
- }
2721
- if (
2722
- !deterministicallyContradicted
2723
- || !exhausted
2724
- || !behaviorResult.mechanism
2725
- ) continue;
2726
- const alreadyRejected = taskPlan.rejectedMechanisms.some((rejected) =>
2727
- rejected.mechanism === behaviorResult.mechanism
2728
- && (
2729
- rejected.behaviorId === behaviorResult.behaviorId
2730
- || rejected.behaviorSignature
2731
- === behaviorResult.behaviorSignature
2732
- ));
2733
- if (alreadyRejected) continue;
2734
- taskPlan = {
2735
- ...taskPlan,
2736
- rejectedMechanisms: [
2737
- ...taskPlan.rejectedMechanisms,
2738
- {
2739
- behaviorId: behaviorResult.behaviorId,
2740
- behaviorSignature: behaviorResult.behaviorSignature,
2741
- mechanism: behaviorResult.mechanism,
2742
- reason:
2743
- `Repeated live evidence contradicted this mechanism: ${behaviorChecks
2744
- .flatMap((check) => check.contradictions)
2745
- .join('; ')
2746
- .slice(0, 700)}`,
2747
- source: 'verification_failure',
2748
- },
2749
- ],
2750
- };
2751
- }
2752
- const budgetExhaustedBehaviorIds = [...behaviorVerificationAttempts]
2753
- .filter(([, attempt]) => attempt.exhausted)
2754
- .map(([behaviorId]) => behaviorId);
2755
- interactionVerification = {
2756
- ...verified,
2757
- semanticVerifierMode,
2758
- budgetExhaustedBehaviorIds,
2759
- verifiedAt: new Date().toISOString(),
2760
- };
2761
- verified.representativeBehaviorIds.forEach((behaviorId) =>
2762
- representativeVerifiedBehaviorIds.add(behaviorId));
2763
- if (interactionVerification.completed) {
2764
- verificationEvidence.set('interactions', operationSequence);
2765
- } else {
2766
- 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
+ );
2767
2335
  }
2336
+ const summary = interactionSummary();
2768
2337
  result = {
2769
- stdout: JSON.stringify(interactionVerification),
2338
+ stdout: JSON.stringify({
2339
+ status: summary.status,
2340
+ checks: results,
2341
+ behaviors: summary.behaviors,
2342
+ }),
2770
2343
  stderr: liveResult.stderr,
2771
2344
  };
2772
2345
  } else if (name === 'framer_read_project') {
@@ -3059,7 +2632,7 @@ export function createFramerAgentToolRuntime({
3059
2632
  );
3060
2633
  }
3061
2634
  mutationCalls += 1;
3062
- markMutation();
2635
+ markMutation({ code: true });
3063
2636
  const mutationSequence = operationSequence;
3064
2637
  markInspection({
3065
2638
  verifies: parsed.typecheck?.passed === true ? ['code'] : [],
@@ -3096,8 +2669,7 @@ export function createFramerAgentToolRuntime({
3096
2669
  'Framer Agent changes are required.',
3097
2670
  );
3098
2671
  }
3099
- const mutatedRepresentativeBehaviorIds =
3100
- requireTaskPlanForMutation({ changes });
2672
+ requireTaskPlanForMutation();
3101
2673
  const pagePath = normalizePagePath(rawArguments.pagePath);
3102
2674
  result = await execCode(
3103
2675
  [
@@ -3108,8 +2680,6 @@ export function createFramerAgentToolRuntime({
3108
2680
  mutationCalls += 1;
3109
2681
  applyChangesCalls += 1;
3110
2682
  markMutation();
3111
- mutatedRepresentativeBehaviorIds.forEach((behaviorId) =>
3112
- representativeMutatedBehaviorIds.add(behaviorId));
3113
2683
  const parsedResult = parseStructuredOutput(result.stdout);
3114
2684
  mutationReceipts.push({
3115
2685
  sequence: operationSequence,
@@ -3183,6 +2753,9 @@ export function createFramerAgentToolRuntime({
3183
2753
  name === 'framer_read_code_file'
3184
2754
  ? normalizedCodeFileReadResult(result)
3185
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;
3186
2759
  if (name !== 'framer_read_project') return normalized;
3187
2760
  const screenshotCount = screenshotQueries(rawArguments).length;
3188
2761
  return {
@@ -3333,12 +2906,14 @@ export function createFramerAgentToolRuntime({
3333
2906
  inspect: inspectFinalState,
3334
2907
  finalizeCompletion,
3335
2908
  verificationRepairPrompt,
2909
+ unblockedFailurePrompt,
3336
2910
  sourceHash: () => null,
3337
2911
  hasPendingChanges: () => false,
3338
2912
  directWorkspace: true,
3339
2913
  summary: () => ({
3340
2914
  projectId,
3341
2915
  projectUrl,
2916
+ unresolvedTargets: unresolvedTargets.map((target) => ({ ...target })),
3342
2917
  mutationCalls,
3343
2918
  applyChangesCalls,
3344
2919
  lowLevelWriteCalls,
@@ -3392,10 +2967,6 @@ export function createFramerAgentToolRuntime({
3392
2967
  lowLevelWriteDisabled,
3393
2968
  lowLevelWriteBlockedCalls,
3394
2969
  instructionFallbacks,
3395
- interactionVerificationAttemptLimit:
3396
- MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR,
3397
- interactionRepairLimit:
3398
- MAX_INTERACTION_REPAIRS_PER_BEHAVIOR,
3399
2970
  },
3400
2971
  branch: branchChange,
3401
2972
  lastInspectionAt,
@@ -3422,6 +2993,7 @@ export function createFramerAgentToolRuntime({
3422
2993
  projectContextHash: guidanceMetadata.projectContextHash,
3423
2994
  files: [...guidanceMetadata.files],
3424
2995
  bytes: guidanceMetadata.bytes,
2996
+ guidesRead: [...guidesRead],
3425
2997
  },
3426
2998
  doctrine: { ...promptMetadata },
3427
2999
  }),