@gakim-digital/dexter-bridge 0.11.9 → 0.11.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.11.9",
3
+ "version": "0.11.10",
4
4
  "description": "Local bridge for InstaWebAI and Dexter — runs Codex, Claude Code, or OpenCode on your machine.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,8 @@ const MAX_MODEL_RESULT_BYTES = 120 * 1024;
23
23
  const MAX_SCREENSHOT_BYTES = 4 * 1024 * 1024;
24
24
  const MAX_SCREENSHOTS_PER_CALL = 4;
25
25
  const SCREENSHOT_FETCH_TIMEOUT_MS = 20_000;
26
+ const SESSION_SCREENSHOT_TIMEOUT_MS = 60_000;
27
+ const MAX_SCREENSHOT_FAILURES_REPORTED = 8;
26
28
  const SCREENSHOT_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']);
27
29
  const DEFAULT_COMMAND_TIMEOUT_MS = 5 * 60_000;
28
30
  const SESSION_TIMEOUT_MS = 10 * 60_000;
@@ -155,6 +157,8 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
155
157
  minItems: 1,
156
158
  uniqueItems: true,
157
159
  items: { type: 'string', enum: VERIFICATION_CHECKS },
160
+ description:
161
+ 'The evidence you will produce before completion. Dexter enforces exactly this list and adds nothing to it. Include "visual" when the result must be judged by eye, such as layout or styling work or a build from a reference image, then inspect a screenshot after your final mutation.',
158
162
  },
159
163
  behaviors: {
160
164
  type: 'array',
@@ -706,16 +710,9 @@ function normalizeTaskPlan(rawArguments, inheritedRejectedMechanisms = []) {
706
710
  'Describe the understood task and declare at least one task domain.',
707
711
  );
708
712
  }
713
+ // The model's declared verification is authoritative; domains describe the
714
+ // work and never add checks on their own.
709
715
  const verification = new Set(['structural', ...requestedVerification]);
710
- if (
711
- domains.some((domain) =>
712
- ['visual', 'interactions', 'responsive'].includes(domain))
713
- ) verification.add('visual');
714
- if (domains.includes('links')) verification.add('links');
715
- if (domains.includes('interactions')) verification.add('interactions');
716
- if (domains.includes('responsive')) verification.add('responsive');
717
- if (domains.includes('code')) verification.add('code');
718
- if (domains.includes('data')) verification.add('data');
719
716
  const behaviorPlan = normalizeBehaviorPlan(
720
717
  rawArguments,
721
718
  inheritedRejectedMechanisms,
@@ -906,13 +903,6 @@ function abortError() {
906
903
  });
907
904
  }
908
905
 
909
- function screenshotQueryCount(rawArguments) {
910
- return (Array.isArray(rawArguments?.queries) ? rawArguments.queries : [])
911
- .filter((query) => query?.type === 'screenshot')
912
- .slice(0, MAX_SCREENSHOTS_PER_CALL)
913
- .length;
914
- }
915
-
916
906
  function isFramerScreenshotUrl(value) {
917
907
  try {
918
908
  const url = new URL(String(value || ''));
@@ -924,32 +914,36 @@ function isFramerScreenshotUrl(value) {
924
914
  }
925
915
  }
926
916
 
927
- // Screenshot results are URLs the model cannot open, so the bridge downloads
928
- // them and hands the pixels to the model as image content.
929
- async function downloadScreenshotImages(parsed, fetchImpl) {
930
- const urls = (Array.isArray(parsed?.results) ? parsed.results : [])
931
- .map((entry) => entry?.image_url)
932
- .filter(isFramerScreenshotUrl)
917
+ function screenshotQueries(rawArguments) {
918
+ return (Array.isArray(rawArguments?.queries) ? rawArguments.queries : [])
919
+ .filter((query) => query?.type === 'screenshot')
933
920
  .slice(0, MAX_SCREENSHOTS_PER_CALL);
934
- const images = [];
935
- for (const url of urls) {
936
- try {
937
- const response = await fetchImpl(url, {
938
- signal: AbortSignal.timeout(SCREENSHOT_FETCH_TIMEOUT_MS),
939
- });
940
- const mimeType = String(response.headers.get('content-type') || '')
941
- .split(';')[0]
942
- .trim()
943
- .toLowerCase();
944
- if (!response.ok || !SCREENSHOT_MIME_TYPES.has(mimeType)) continue;
945
- const bytes = Buffer.from(await response.arrayBuffer());
946
- if (!bytes.length || bytes.length > MAX_SCREENSHOT_BYTES) continue;
947
- images.push({ url, mimeType, data: bytes.toString('base64') });
948
- } catch {
949
- // The URL stays in the text result; the model is told the image is missing.
950
- }
921
+ }
922
+
923
+ // Screenshot results are URLs the model cannot open, so the bridge downloads
924
+ // them and hands the pixels to the model as image content. Failures carry a
925
+ // short reason so delivery problems are visible in run telemetry.
926
+ async function downloadScreenshotImage(url, fetchImpl) {
927
+ if (!isFramerScreenshotUrl(url)) return { error: 'untrusted_url' };
928
+ try {
929
+ const response = await fetchImpl(url, {
930
+ signal: AbortSignal.timeout(SCREENSHOT_FETCH_TIMEOUT_MS),
931
+ });
932
+ if (!response.ok) return { error: `http_${response.status}` };
933
+ const mimeType = String(response.headers.get('content-type') || '')
934
+ .split(';')[0]
935
+ .trim()
936
+ .toLowerCase();
937
+ if (!SCREENSHOT_MIME_TYPES.has(mimeType)) return { error: 'unsupported_type' };
938
+ const bytes = Buffer.from(await response.arrayBuffer());
939
+ if (!bytes.length) return { error: 'empty' };
940
+ if (bytes.length > MAX_SCREENSHOT_BYTES) return { error: 'too_large' };
941
+ return { image: { url, mimeType, data: bytes.toString('base64') } };
942
+ } catch (error) {
943
+ return {
944
+ error: String(error?.cause?.code || error?.name || 'fetch_failed').slice(0, 60),
945
+ };
951
946
  }
952
- return images;
953
947
  }
954
948
 
955
949
  function parseStructuredOutput(value) {
@@ -1239,8 +1233,12 @@ export function createFramerAgentToolRuntime({
1239
1233
  const representativeEvidenceBehaviorByNode = new Map();
1240
1234
  const behaviorVerificationAttempts = new Map();
1241
1235
  const mutationReceipts = [];
1242
- let screenshotBeforeMutation = false;
1243
1236
  let lastScreenshotSequence = 0;
1237
+ let lastScreenshotAttemptSequence = 0;
1238
+ let screenshotsViaDownload = 0;
1239
+ let screenshotsViaSession = 0;
1240
+ let screenshotsUndelivered = 0;
1241
+ const screenshotFailures = [];
1244
1242
  let taskPlan = null;
1245
1243
  const verificationEvidence = new Map();
1246
1244
  let guidanceMetadata = {
@@ -1664,6 +1662,78 @@ export function createFramerAgentToolRuntime({
1664
1662
  }
1665
1663
  };
1666
1664
 
1665
+ // Captures a node through the Framer session itself. This needs no access
1666
+ // to the screenshot CDN, so it works where that download is blocked.
1667
+ const captureSessionScreenshot = async (nodeId) => {
1668
+ for (const scale of [1, 0.5]) {
1669
+ try {
1670
+ const result = await execCode(
1671
+ [
1672
+ `const shot = await framer.screenshot(${JSON.stringify(nodeId)}, { format: 'jpeg', quality: 80, scale: ${scale} });`,
1673
+ 'console.log(JSON.stringify({ mimeType: shot.mimeType, data: Buffer.from(shot.data).toString("base64") }));',
1674
+ ].join('\n'),
1675
+ { timeoutMs: SESSION_SCREENSHOT_TIMEOUT_MS },
1676
+ );
1677
+ const parsed = parseStructuredOutput(result.stdout);
1678
+ const mimeType = String(parsed?.mimeType || '').toLowerCase();
1679
+ const data = typeof parsed?.data === 'string' ? parsed.data : '';
1680
+ if (!SCREENSHOT_MIME_TYPES.has(mimeType)) return { error: 'unsupported_type' };
1681
+ const bytes = Buffer.from(data, 'base64').length;
1682
+ if (!bytes) return { error: 'empty' };
1683
+ if (bytes > MAX_SCREENSHOT_BYTES) return { error: 'too_large' };
1684
+ return { image: { url: null, mimeType, data } };
1685
+ } catch (error) {
1686
+ if (error?.code === 'FRAMER_AGENT_OUTPUT_TOO_LARGE' && scale === 1) continue;
1687
+ return { error: String(error?.code || 'capture_failed').slice(0, 60) };
1688
+ }
1689
+ }
1690
+ return { error: 'too_large' };
1691
+ };
1692
+
1693
+ const deliverScreenshots = async (queries, parsed) => {
1694
+ const results = (Array.isArray(parsed?.results) ? parsed.results : [])
1695
+ .filter((entry) => typeof entry?.image_url === 'string');
1696
+ const images = [];
1697
+ const failures = [];
1698
+ let viaDownload = 0;
1699
+ let viaSession = 0;
1700
+ let undelivered = 0;
1701
+ for (const [index, query] of queries.entries()) {
1702
+ const nodeId = typeof query.id === 'string' ? query.id.trim() : '';
1703
+ const entry =
1704
+ (nodeId && results.find((candidate) => candidate.id === nodeId))
1705
+ || results[index];
1706
+ const downloaded = entry
1707
+ ? await downloadScreenshotImage(entry.image_url, fetchImpl)
1708
+ : { error: 'no_image_url' };
1709
+ if (downloaded.image) {
1710
+ images.push(downloaded.image);
1711
+ viaDownload += 1;
1712
+ continue;
1713
+ }
1714
+ const captured = nodeId
1715
+ ? await captureSessionScreenshot(nodeId)
1716
+ : { error: 'no_node_id' };
1717
+ if (captured.image) {
1718
+ images.push(captured.image);
1719
+ viaSession += 1;
1720
+ failures.push({
1721
+ target: nodeId,
1722
+ download: downloaded.error,
1723
+ recoveredVia: 'session',
1724
+ });
1725
+ continue;
1726
+ }
1727
+ undelivered += 1;
1728
+ failures.push({
1729
+ target: nodeId || 'url',
1730
+ download: downloaded.error,
1731
+ capture: captured.error,
1732
+ });
1733
+ }
1734
+ return { images, viaDownload, viaSession, undelivered, failures };
1735
+ };
1736
+
1667
1737
  const referenceAssetFiles = Array.isArray(assignment?.context?.referenceAssetFiles)
1668
1738
  ? assignment.context.referenceAssetFiles
1669
1739
  : [];
@@ -1764,16 +1834,20 @@ export function createFramerAgentToolRuntime({
1764
1834
  };
1765
1835
  };
1766
1836
 
1767
- const markInspection = ({ visual = false, verifies = [] } = {}) => {
1837
+ const markInspection = ({
1838
+ visual = false,
1839
+ visualAttempted = false,
1840
+ verifies = [],
1841
+ } = {}) => {
1768
1842
  operationSequence += 1;
1769
1843
  lastInspectionSequence = operationSequence;
1770
1844
  lastInspectionAt = new Date().toISOString();
1771
1845
  verificationEvidence.set('structural', operationSequence);
1846
+ if (visualAttempted) lastScreenshotAttemptSequence = operationSequence;
1772
1847
  if (visual) {
1773
1848
  screenshotCalls += 1;
1774
1849
  lastScreenshotSequence = operationSequence;
1775
1850
  verificationEvidence.set('visual', operationSequence);
1776
- if (lastMutationSequence === 0) screenshotBeforeMutation = true;
1777
1851
  }
1778
1852
  for (const check of normalizeVerificationValues(
1779
1853
  verifies,
@@ -1824,34 +1898,30 @@ export function createFramerAgentToolRuntime({
1824
1898
  const verificationCheckCompleted = (check) => {
1825
1899
  if (mutationCalls === 0) return true;
1826
1900
  if (check === 'structural') return synchronized();
1827
- if (check === 'visual') {
1828
- return (
1829
- screenshotBeforeMutation
1830
- && lastScreenshotSequence > lastMutationSequence
1831
- );
1832
- }
1833
1901
  return Number(verificationEvidence.get(check) || 0) > lastMutationSequence;
1834
1902
  };
1835
1903
  const visualVerificationRequired = () =>
1836
- applyChangesCalls > 0
1837
- || requiredVerificationChecks().includes('visual');
1838
- const visualVerificationCompleted = () =>
1839
- !visualVerificationRequired()
1840
- || mutationCalls === 0
1841
- || (
1842
- screenshotBeforeMutation
1843
- && lastScreenshotSequence > lastMutationSequence
1844
- );
1904
+ requiredVerificationChecks().includes('visual');
1905
+ // The model asked for the final canvas, but no screenshot could be handed to
1906
+ // it. That is reported as unverified rather than blamed on the model.
1907
+ const visualVerificationUnavailable = () =>
1908
+ mutationCalls > 0
1909
+ && lastScreenshotAttemptSequence > lastMutationSequence
1910
+ && lastScreenshotSequence <= lastMutationSequence;
1911
+ const visualVerificationStatus = () => {
1912
+ if (!visualVerificationRequired() || mutationCalls === 0) return 'not_required';
1913
+ if (verificationCheckCompleted('visual')) return 'seen';
1914
+ return visualVerificationUnavailable() ? 'unavailable' : 'missing';
1915
+ };
1916
+ const requiredChecks = () => [
1917
+ ...new Set(['structural', ...requiredVerificationChecks()]),
1918
+ ];
1845
1919
  const missingVerificationChecks = () => {
1846
1920
  if (mutationCalls === 0) return [];
1847
1921
  const missing = [];
1848
1922
  if (!taskPlan) missing.push('task-plan');
1849
- const required = new Set([
1850
- 'structural',
1851
- ...requiredVerificationChecks(),
1852
- ...(applyChangesCalls > 0 ? ['visual'] : []),
1853
- ]);
1854
- for (const check of required) {
1923
+ for (const check of requiredChecks()) {
1924
+ if (check === 'visual' && visualVerificationUnavailable()) continue;
1855
1925
  if (!verificationCheckCompleted(check)) missing.push(check);
1856
1926
  }
1857
1927
  return missing;
@@ -1862,8 +1932,18 @@ export function createFramerAgentToolRuntime({
1862
1932
  completed: missingVerificationChecks().length === 0,
1863
1933
  structuralCompleted: synchronized(),
1864
1934
  visualRequired: visualVerificationRequired() && mutationCalls > 0,
1865
- visualCompleted: visualVerificationCompleted(),
1935
+ visualCompleted: visualVerificationStatus() !== 'missing',
1936
+ visualStatus: visualVerificationStatus(),
1866
1937
  screenshotCalls,
1938
+ // Every failed CDN download is listed; recoveredVia marks the ones the
1939
+ // session capture still delivered to the model.
1940
+ screenshotDelivery: {
1941
+ delivered: screenshotsViaDownload + screenshotsViaSession,
1942
+ viaDownload: screenshotsViaDownload,
1943
+ viaSession: screenshotsViaSession,
1944
+ undelivered: screenshotsUndelivered,
1945
+ failures: screenshotFailures.map((failure) => ({ ...failure })),
1946
+ },
1867
1947
  ...(requiredVerificationChecks().includes('interactions')
1868
1948
  || interactionVerificationCalls > 0
1869
1949
  ? {
@@ -1907,20 +1987,10 @@ export function createFramerAgentToolRuntime({
1907
1987
  }
1908
1988
  : {}),
1909
1989
  taskPlan: taskPlanSnapshot(),
1910
- requiredChecks: [
1911
- ...new Set([
1912
- 'structural',
1913
- ...requiredVerificationChecks(),
1914
- ...(applyChangesCalls > 0 ? ['visual'] : []),
1915
- ]),
1916
- ],
1917
- completedChecks: [
1918
- ...new Set([
1919
- 'structural',
1920
- ...requiredVerificationChecks(),
1921
- ...(applyChangesCalls > 0 ? ['visual'] : []),
1922
- ]),
1923
- ].filter(verificationCheckCompleted),
1990
+ requiredChecks: requiredChecks(),
1991
+ completedChecks: requiredChecks().filter(verificationCheckCompleted),
1992
+ unavailableChecks:
1993
+ visualVerificationStatus() === 'unavailable' ? ['visual'] : [],
1924
1994
  missingChecks: missingVerificationChecks(),
1925
1995
  lastMutationAt,
1926
1996
  lastInspectionAt,
@@ -1942,18 +2012,31 @@ export function createFramerAgentToolRuntime({
1942
2012
  };
1943
2013
  };
1944
2014
 
2015
+ const withUnavailableVisualCheck = (completion) =>
2016
+ visualVerificationStatus() === 'unavailable'
2017
+ ? {
2018
+ ...completion,
2019
+ checks: [
2020
+ ...(Array.isArray(completion?.checks) ? completion.checks : []),
2021
+ {
2022
+ command: 'Visual verification',
2023
+ status: 'skipped',
2024
+ output:
2025
+ 'Screenshots of the final canvas could not be delivered to the model, so the visual result is unverified.',
2026
+ },
2027
+ ],
2028
+ }
2029
+ : completion;
2030
+
1945
2031
  const finalizeCompletion = (completion) => {
1946
- if (
1947
- completion?.status !== 'completed'
1948
- || mutationCalls === 0
1949
- || verification().completed
1950
- ) {
2032
+ if (completion?.status !== 'completed' || mutationCalls === 0) {
1951
2033
  return completion;
1952
2034
  }
2035
+ if (verification().completed) return withUnavailableVisualCheck(completion);
1953
2036
  const missing = missingVerificationChecks().map((check) => {
1954
2037
  if (check === 'task-plan') return 'a harness task and verification plan';
1955
2038
  if (check === 'structural') return 'a final structural read';
1956
- if (check === 'visual') return 'before-and-after screenshot verification';
2039
+ if (check === 'visual') return 'post-mutation screenshot verification';
1957
2040
  return `post-mutation ${check} verification`;
1958
2041
  });
1959
2042
  const interactionDetails = interactionVerification.behaviors
@@ -1971,14 +2054,15 @@ export function createFramerAgentToolRuntime({
1971
2054
  ? [`Interaction evidence: ${interactionDetails.join('; ')}.`]
1972
2055
  : []),
1973
2056
  ].join(' ');
2057
+ const failed = withUnavailableVisualCheck(completion);
1974
2058
  return {
1975
- ...completion,
2059
+ ...failed,
1976
2060
  status: 'failed',
1977
2061
  summary:
1978
2062
  String(completion?.summary || '').trim()
1979
2063
  || 'The requested changes were applied.',
1980
2064
  checks: [
1981
- ...(Array.isArray(completion?.checks) ? completion.checks : []),
2065
+ ...(Array.isArray(failed?.checks) ? failed.checks : []),
1982
2066
  {
1983
2067
  command: 'Harness-owned final verification',
1984
2068
  status: 'failed',
@@ -2002,9 +2086,7 @@ export function createFramerAgentToolRuntime({
2002
2086
  missing.push('perform a focused live read after the latest mutation');
2003
2087
  } else if (check === 'visual') {
2004
2088
  missing.push(
2005
- screenshotBeforeMutation
2006
- ? 'capture and inspect the affected canvas after the latest mutation'
2007
- : 'report that the required baseline screenshot was missed; do not claim visual verification',
2089
+ 'capture and inspect a screenshot of the affected canvas after the latest mutation',
2008
2090
  );
2009
2091
  } else {
2010
2092
  const interactionState = interactionVerification.behaviors
@@ -2084,10 +2166,7 @@ export function createFramerAgentToolRuntime({
2084
2166
  return uniqueStrings(commands.map((command) => command.nodeId));
2085
2167
  };
2086
2168
 
2087
- const requireTaskPlanForMutation = ({
2088
- canvas = false,
2089
- changes = '',
2090
- } = {}) => {
2169
+ const requireTaskPlanForMutation = ({ changes = '' } = {}) => {
2091
2170
  if (!taskPlan) {
2092
2171
  throw toolError(
2093
2172
  'FRAMER_HARNESS_PLAN_REQUIRED',
@@ -2096,15 +2175,6 @@ export function createFramerAgentToolRuntime({
2096
2175
  }
2097
2176
  const repairTargets = representativeRepairTargets(changes);
2098
2177
  const isRepresentativeRepair = repairTargets.length > 0;
2099
- if (
2100
- (canvas || taskPlan.verification.includes('visual'))
2101
- && !screenshotBeforeMutation
2102
- ) {
2103
- throw toolError(
2104
- 'FRAMER_HARNESS_BASELINE_SCREENSHOT_REQUIRED',
2105
- 'Capture and inspect a screenshot of the affected canvas before the first visual mutation.',
2106
- );
2107
- }
2108
2178
  const rejectedSelectedMechanisms = taskPlan.behaviors.filter((behavior) => {
2109
2179
  const selected = taskPlan.mechanisms.find((decision) =>
2110
2180
  decision.behaviorId === behavior.id);
@@ -2319,16 +2389,6 @@ export function createFramerAgentToolRuntime({
2319
2389
  rawArguments,
2320
2390
  inheritedRejectedMechanisms,
2321
2391
  );
2322
- if (
2323
- mutationCalls > 0
2324
- && nextPlan.verification.includes('visual')
2325
- && !screenshotBeforeMutation
2326
- ) {
2327
- throw toolError(
2328
- 'FRAMER_HARNESS_BASELINE_SCREENSHOT_REQUIRED',
2329
- 'Visual verification cannot be added after mutations when no baseline screenshot was captured.',
2330
- );
2331
- }
2332
2392
  const previousMechanisms = new Map(
2333
2393
  (taskPlan?.mechanisms || []).map((decision) => [
2334
2394
  decision.behaviorId,
@@ -2644,12 +2704,25 @@ export function createFramerAgentToolRuntime({
2644
2704
  'console.log(JSON.stringify(result, null, 2));',
2645
2705
  ].join('\n'),
2646
2706
  );
2647
- modelImages = await downloadScreenshotImages(
2648
- parseStructuredOutput(result.stdout),
2649
- fetchImpl,
2707
+ const requestedScreenshots = screenshotQueries(rawArguments);
2708
+ const delivery = requestedScreenshots.length
2709
+ ? await deliverScreenshots(
2710
+ requestedScreenshots,
2711
+ parseStructuredOutput(result.stdout),
2712
+ )
2713
+ : { images: [], viaDownload: 0, viaSession: 0, undelivered: 0, failures: [] };
2714
+ modelImages = delivery.images;
2715
+ screenshotsViaDownload += delivery.viaDownload;
2716
+ screenshotsViaSession += delivery.viaSession;
2717
+ screenshotsUndelivered += delivery.undelivered;
2718
+ screenshotFailures.push(...delivery.failures);
2719
+ screenshotFailures.splice(
2720
+ 0,
2721
+ Math.max(0, screenshotFailures.length - MAX_SCREENSHOT_FAILURES_REPORTED),
2650
2722
  );
2651
2723
  markInspection({
2652
2724
  visual: modelImages.length > 0,
2725
+ visualAttempted: requestedScreenshots.length > 0,
2653
2726
  verifies: rawArguments.verifies,
2654
2727
  });
2655
2728
  } else if (name === 'framer_query_images') {
@@ -2694,7 +2767,7 @@ export function createFramerAgentToolRuntime({
2694
2767
  );
2695
2768
  }
2696
2769
  const mutatedRepresentativeBehaviorIds =
2697
- requireTaskPlanForMutation({ canvas: true, changes });
2770
+ requireTaskPlanForMutation({ changes });
2698
2771
  const pagePath = normalizePagePath(rawArguments.pagePath);
2699
2772
  result = await execCode(
2700
2773
  [
@@ -2770,7 +2843,7 @@ export function createFramerAgentToolRuntime({
2770
2843
  });
2771
2844
  const normalized = normalizedResult(result);
2772
2845
  if (name !== 'framer_read_project') return normalized;
2773
- const screenshotCount = screenshotQueryCount(rawArguments);
2846
+ const screenshotCount = screenshotQueries(rawArguments).length;
2774
2847
  return {
2775
2848
  ...normalized,
2776
2849
  ...(modelImages.length ? { modelImages } : {}),
@@ -1455,6 +1455,18 @@ export function summarizeFramerChanges(changes) {
1455
1455
  sourceNodeId: variant[2],
1456
1456
  };
1457
1457
  }
1458
+ const removed = command.match(/^DEL\s+([^\s;]+)/i);
1459
+ if (removed) return { operation: 'delete', nodeId: removed[1] };
1460
+ const duplicated = command.match(/^DUPE\s+([^\s]+).*?\bnewId="([^"]+)"/i);
1461
+ if (duplicated) {
1462
+ return {
1463
+ operation: 'duplicate',
1464
+ nodeId: duplicated[2],
1465
+ sourceNodeId: duplicated[1],
1466
+ };
1467
+ }
1468
+ const moved = command.match(/^MOVE\s+([^\s]+)/i);
1469
+ if (moved) return { operation: 'move', nodeId: moved[1] };
1458
1470
  const operation = text(command.split(/\s+/)[0], 40).toLowerCase();
1459
1471
  return { operation: operation || 'unknown' };
1460
1472
  });
package/src/protocol.js CHANGED
@@ -488,7 +488,7 @@ export function buildOutcomePrompt(outcome = {}, product, promptContext = {}) {
488
488
  'Do not publish or deploy unless the assignment explicitly says publishing is authorized.',
489
489
  'You own verification. Your framer_plan_task declaration determines the required evidence, and the harness enforces it before accepting completion.',
490
490
  'framer_read_project screenshots are returned to you as images; judge the result from what you see in them. If a result carries screenshotWarning, you did not see that screenshot, so do not describe it or claim it as visual verification.',
491
- 'Every framer_apply_changes canvas mutation requires a screenshot before the first mutation and after the final mutation. Use verifies on final reads to establish link, responsive, code, or data checks when your plan requires them.',
491
+ 'You decide which evidence the task needs. Declare visual verification in framer_plan_task when the result must be judged by eye, such as layout or styling work or a build from a reference image, and then inspect a screenshot after your final mutation. When building from a reference attachment, compare that final screenshot against the reference. A screenshot before editing is optional; take one when you need to preserve or compare the current look. Use verifies on final reads to establish link, responsive, code, or data checks when your plan requires them.',
492
492
  'A generic read cannot verify interactions. framer_verify_interactions derives success requirements from the behavior contract and chosen mechanism; supply canonical evidence node IDs rather than defining your own success test. It returns verified, contradicted, or unknown. Unknown means the available representation could not prove the behavior; it is not a defect and must not trigger mutation. A grounded semanticAssessment may resolve unknown evidence when enabled, but it cannot override a deterministic contradiction or authorize writes. Native links are verified from href, URL, route, destination, or component-control evidence. Native effects are verified from reflected hoverEffect, tapEffect, or appearEffect properties and are structural evidence, not runtime playback.',
493
493
  'For repeated behavior, declare every targetNodeId and one representativeTargetNodeId in the mechanism plan. Implement and verify only that representative before fan-out, then verify complete target coverage after the final mutation. Only a contradicted result permits one evidence-scoped repair. Repeated unchanged evidence or exhausted verification budget must end as partial rather than starting another loop.',
494
494
  'Do not reuse a mechanism recorded as rejected by the user or repeated contradictory live evidence. Unknown evidence never rejects a mechanism.',