@gakim-digital/dexter-bridge 0.12.2 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +16 -0
- package/package.json +1 -1
- package/src/agent.js +166 -55
- package/src/agentOutput.js +6 -0
- package/src/api.js +75 -0
- package/src/cli.js +160 -110
- package/src/config.js +22 -2
- package/src/framerRequirementContract.js +303 -0
- package/src/framerTools.js +215 -56
- package/src/protocol.js +66 -5
- package/src/providers/codexAppServer.js +50 -34
- package/src/taskEvidence.js +245 -0
- package/src/webflowTools.js +1045 -0
- package/src/framerBehaviorContract.js +0 -229
package/LICENSE
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Dexter Bridge License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 InstaWeb AI. All rights reserved.
|
|
4
|
+
|
|
5
|
+
Permission is granted to download, install, and run this software solely to
|
|
6
|
+
connect your local machine to the Dexter / InstaWeb AI service. You may not
|
|
7
|
+
modify, redistribute, sublicense, or sell this software or use it to build a
|
|
8
|
+
competing service.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
11
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
12
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
13
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
14
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
15
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
16
|
+
SOFTWARE.
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -6,7 +6,10 @@ import path from 'node:path';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import crossSpawn from 'cross-spawn';
|
|
8
8
|
import {
|
|
9
|
+
callWebflowPanelTool,
|
|
10
|
+
findWebflowImages,
|
|
9
11
|
getFramerProjectAuthorization,
|
|
12
|
+
webflowMcpEndpoint,
|
|
10
13
|
readFramerWebsite,
|
|
11
14
|
isCompanionPairingInvalidError,
|
|
12
15
|
postRunEvent,
|
|
@@ -29,6 +32,7 @@ import {
|
|
|
29
32
|
assertOutcomeResponseContract,
|
|
30
33
|
buildFramerOutcomePrompt,
|
|
31
34
|
buildOutcomePrompt,
|
|
35
|
+
buildWebflowOutcomePrompt,
|
|
32
36
|
buildModelTurnDeltaPrompt,
|
|
33
37
|
buildModelTurnFallbackPrompt,
|
|
34
38
|
buildModelTurnPrompt,
|
|
@@ -65,6 +69,7 @@ import {
|
|
|
65
69
|
createHarnessToolRuntime,
|
|
66
70
|
} from './harnessTools.js';
|
|
67
71
|
import { createFramerAgentToolRuntime } from './framerTools.js';
|
|
72
|
+
import { assertWebflowHarnessAgent, createWebflowToolRuntime } from './webflowTools.js';
|
|
68
73
|
|
|
69
74
|
const HARNESS_MCP_SERVER_PATH = fileURLToPath(
|
|
70
75
|
new URL('./harnessMcpServer.js', import.meta.url),
|
|
@@ -356,6 +361,18 @@ function writeClaudeSystemPromptFile(content) {
|
|
|
356
361
|
return file;
|
|
357
362
|
}
|
|
358
363
|
|
|
364
|
+
function claudeWebflowOutcomeSystemPrompt(product) {
|
|
365
|
+
const name = normalizeBridgeProduct(product).name;
|
|
366
|
+
return [
|
|
367
|
+
`You are an autonomous Webflow harness embedded inside ${name}.`,
|
|
368
|
+
'Work only through the Dexter Webflow tools connected to the user-selected site.',
|
|
369
|
+
'Inspect the site, perform the requested work, verify the result, and return the structured outcome.',
|
|
370
|
+
'Do not access local credentials, unrelated files, other sites, account settings, or billing.',
|
|
371
|
+
'Do not publish unless the assignment explicitly authorizes publishing.',
|
|
372
|
+
`Return only the structured outcome result requested by ${name}.`,
|
|
373
|
+
].join(' ');
|
|
374
|
+
}
|
|
375
|
+
|
|
359
376
|
function claudeOutcomeSystemPrompt(product) {
|
|
360
377
|
const name = normalizeBridgeProduct(product).name;
|
|
361
378
|
if (normalizeBridgeProduct(product).id === 'dexter') {
|
|
@@ -445,10 +462,11 @@ function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
|
445
462
|
const harnessTools = harnessToolNames.map(
|
|
446
463
|
(name) => `mcp__instawebai__${name}`,
|
|
447
464
|
);
|
|
448
|
-
const
|
|
449
|
-
|
|
465
|
+
const designHarness = options.designHarness || options.framerHarness;
|
|
466
|
+
const webHosts = designHarness
|
|
467
|
+
? framerWebHosts(designHarness)
|
|
450
468
|
: [];
|
|
451
|
-
const nativeTools =
|
|
469
|
+
const nativeTools = designHarness
|
|
452
470
|
? [
|
|
453
471
|
...(options.nativeSkillsEnabled ? ['Skill'] : []),
|
|
454
472
|
// Web access is limited to the sites the user named. WebSearch cannot
|
|
@@ -482,10 +500,10 @@ function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
|
482
500
|
const permissionMode = argsWithoutFlagValue(projectSettingsOnly, '--permission-mode');
|
|
483
501
|
permissionMode.push(
|
|
484
502
|
'--permission-mode',
|
|
485
|
-
options.harnessMode && !
|
|
503
|
+
options.harnessMode && !designHarness ? 'acceptEdits' : 'dontAsk',
|
|
486
504
|
);
|
|
487
505
|
if (options.harnessMode) {
|
|
488
|
-
const allowedTools =
|
|
506
|
+
const allowedTools = designHarness
|
|
489
507
|
? [
|
|
490
508
|
...(options.nativeSkillsEnabled ? ['Skill'] : []),
|
|
491
509
|
...webHosts.map((host) => `WebFetch(domain:${host})`),
|
|
@@ -505,8 +523,8 @@ function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
|
505
523
|
// Framer run with skills leaves it off. The explicit tool list above is what
|
|
506
524
|
// keeps the shell and file tools out; it grants the Framer tools and Skill
|
|
507
525
|
// only, and settings stay limited to the workspace the bridge created.
|
|
508
|
-
const
|
|
509
|
-
if (options.harnessMode && !
|
|
526
|
+
const designRunWithSkills = Boolean(designHarness && options.nativeSkillsEnabled);
|
|
527
|
+
if (options.harnessMode && !designRunWithSkills && !argsIncludeFlag(configured, '--restricted')) {
|
|
510
528
|
configured.push('--restricted');
|
|
511
529
|
}
|
|
512
530
|
return configured;
|
|
@@ -1188,6 +1206,15 @@ export function agentFailureMessage(command, code, stdout = '', stderr = '') {
|
|
|
1188
1206
|
return agentFailureDetails(command, code, stdout, stderr).message;
|
|
1189
1207
|
}
|
|
1190
1208
|
|
|
1209
|
+
// Agent CLIs run in their own process group, so a signal that stops the
|
|
1210
|
+
// bridge never reaches them; stopping the bridge aborts them explicitly.
|
|
1211
|
+
const bridgeShutdown = new AbortController();
|
|
1212
|
+
|
|
1213
|
+
export function stopAgentProcesses() {
|
|
1214
|
+
if (bridgeShutdown.signal.aborted) return;
|
|
1215
|
+
bridgeShutdown.abort(new CompanionRunCancelledError('The Dexter bridge stopped.', 'device_disconnect'));
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1191
1218
|
function runProcess(
|
|
1192
1219
|
command,
|
|
1193
1220
|
args,
|
|
@@ -1206,8 +1233,9 @@ function runProcess(
|
|
|
1206
1233
|
} = {},
|
|
1207
1234
|
) {
|
|
1208
1235
|
return new Promise((resolve, reject) => {
|
|
1209
|
-
|
|
1210
|
-
|
|
1236
|
+
const stopped = [signal, bridgeShutdown.signal].find((candidate) => candidate?.aborted);
|
|
1237
|
+
if (stopped) {
|
|
1238
|
+
const error = stopped.reason instanceof Error ? stopped.reason : new Error('The model process was cancelled.');
|
|
1211
1239
|
error.code = error.code || 'RUN_CANCELLED';
|
|
1212
1240
|
reject(error);
|
|
1213
1241
|
return;
|
|
@@ -1278,6 +1306,7 @@ function runProcess(
|
|
|
1278
1306
|
clearTimeout(forceKillTimer);
|
|
1279
1307
|
clearTimeout(terminationFallbackTimer);
|
|
1280
1308
|
signal?.removeEventListener('abort', abort);
|
|
1309
|
+
bridgeShutdown.signal.removeEventListener('abort', abort);
|
|
1281
1310
|
error.stdout = stdout;
|
|
1282
1311
|
error.stderr = stderr;
|
|
1283
1312
|
reject(error);
|
|
@@ -1294,10 +1323,11 @@ function runProcess(
|
|
|
1294
1323
|
}, killGraceMs);
|
|
1295
1324
|
forceKillTimer.unref?.();
|
|
1296
1325
|
};
|
|
1297
|
-
const abort = () => {
|
|
1326
|
+
const abort = (event) => {
|
|
1298
1327
|
if (settled || terminationError) return;
|
|
1299
1328
|
clearTimers();
|
|
1300
|
-
const
|
|
1329
|
+
const reason = event?.target?.reason;
|
|
1330
|
+
const error = reason instanceof Error ? reason : new Error('The model process was cancelled.');
|
|
1301
1331
|
error.code = error.code || 'RUN_CANCELLED';
|
|
1302
1332
|
terminationError = error;
|
|
1303
1333
|
terminateChildTree(error);
|
|
@@ -1342,6 +1372,7 @@ function runProcess(
|
|
|
1342
1372
|
);
|
|
1343
1373
|
}, hardDeadlineMs);
|
|
1344
1374
|
signal?.addEventListener('abort', abort, { once: true });
|
|
1375
|
+
bridgeShutdown.signal.addEventListener('abort', abort, { once: true });
|
|
1345
1376
|
armInactivityTimer();
|
|
1346
1377
|
|
|
1347
1378
|
child.stdout.on('data', (chunk) => {
|
|
@@ -1392,6 +1423,7 @@ function runProcess(
|
|
|
1392
1423
|
clearTimeout(forceKillTimer);
|
|
1393
1424
|
clearTimeout(terminationFallbackTimer);
|
|
1394
1425
|
signal?.removeEventListener('abort', abort);
|
|
1426
|
+
bridgeShutdown.signal.removeEventListener('abort', abort);
|
|
1395
1427
|
stdoutProgress?.flush();
|
|
1396
1428
|
const meta = {
|
|
1397
1429
|
command,
|
|
@@ -1570,8 +1602,15 @@ class CompanionRunCancelledError extends Error {
|
|
|
1570
1602
|
}
|
|
1571
1603
|
|
|
1572
1604
|
function cancellationMessage(error) {
|
|
1605
|
+
const webflow = error?.webflow || error?.companionUsage?.webflow;
|
|
1606
|
+
if (webflow) {
|
|
1607
|
+
const mutationCalls = Number(webflow.mutationCalls) || 0;
|
|
1608
|
+
if (mutationCalls <= 0) return 'Stopped before Dexter completed a Webflow change.';
|
|
1609
|
+
return `Stopped after ${mutationCalls} successful Webflow change ${mutationCalls === 1 ? 'call' : 'calls'}. Those changes remain in Webflow.`;
|
|
1610
|
+
}
|
|
1573
1611
|
const framer = error?.framer || error?.companionUsage?.framer;
|
|
1574
|
-
|
|
1612
|
+
if (!framer) return 'Dexter stopped this request.';
|
|
1613
|
+
const mutationCalls = Number(framer.mutationCalls) || 0;
|
|
1575
1614
|
if (mutationCalls <= 0) {
|
|
1576
1615
|
return 'Stopped before Dexter completed a Framer change.';
|
|
1577
1616
|
}
|
|
@@ -1907,7 +1946,7 @@ async function callLocalJsonAgent(agent, prompt, options = {}) {
|
|
|
1907
1946
|
onProgress: options.onProgress,
|
|
1908
1947
|
stdoutProgress:
|
|
1909
1948
|
definition.id === 'claude-code'
|
|
1910
|
-
? createClaudeProgressParser(options.onProgress, { cwd: options.cwd })
|
|
1949
|
+
? createClaudeProgressParser(options.onProgress, { cwd: options.cwd, trace: options.trace })
|
|
1911
1950
|
: undefined,
|
|
1912
1951
|
});
|
|
1913
1952
|
}
|
|
@@ -2241,7 +2280,7 @@ async function executeModelTurn(run, send, agent, options = {}) {
|
|
|
2241
2280
|
});
|
|
2242
2281
|
} catch (error) {
|
|
2243
2282
|
if (cliCancelled || error?.code === 'RUN_CANCELLED') {
|
|
2244
|
-
throw new CompanionRunCancelledError();
|
|
2283
|
+
throw new CompanionRunCancelledError(undefined, error?.cancellationSource);
|
|
2245
2284
|
}
|
|
2246
2285
|
const failure = agentErrorWithOutputUsage(definition.id, error);
|
|
2247
2286
|
if (callContextMode !== 'delta' || !resumeFailure(failure)) throw failedModelCall(failure);
|
|
@@ -2273,7 +2312,7 @@ async function executeModelTurn(run, send, agent, options = {}) {
|
|
|
2273
2312
|
onProgress: reportCliProgress,
|
|
2274
2313
|
}).catch((error) => {
|
|
2275
2314
|
if (cliCancelled || error?.code === 'RUN_CANCELLED') {
|
|
2276
|
-
throw new CompanionRunCancelledError();
|
|
2315
|
+
throw new CompanionRunCancelledError(undefined, error?.cancellationSource);
|
|
2277
2316
|
}
|
|
2278
2317
|
throw failedModelCall(agentErrorWithOutputUsage(definition.id, error));
|
|
2279
2318
|
});
|
|
@@ -2346,6 +2385,10 @@ async function executeModelTurn(run, send, agent, options = {}) {
|
|
|
2346
2385
|
});
|
|
2347
2386
|
}
|
|
2348
2387
|
|
|
2388
|
+
// Rounds in which a model that finished with unmet checks is sent back to fix
|
|
2389
|
+
// them, within the run's time limit.
|
|
2390
|
+
const OUTCOME_REPAIR_ROUNDS = 3;
|
|
2391
|
+
|
|
2349
2392
|
async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
2350
2393
|
const product = normalizeBridgeProduct(run?.product || options.product);
|
|
2351
2394
|
const definition = definitionForRuntimeProfile(run?.runtimeProfile, agent);
|
|
@@ -2383,10 +2426,13 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2383
2426
|
const framerAgentOutcome =
|
|
2384
2427
|
run?.outcome?.kind === 'framer-project'
|
|
2385
2428
|
|| run?.outcome?.context?.executionMode === 'framer-mcp';
|
|
2429
|
+
const webflowOutcome = run?.outcome?.kind === 'webflow-site';
|
|
2430
|
+
if (webflowOutcome) assertWebflowHarnessAgent(definition.id);
|
|
2431
|
+
const designOutcome = framerAgentOutcome || webflowOutcome;
|
|
2386
2432
|
// Every Dexter message starts a fresh provider session. The assignment
|
|
2387
2433
|
// already carries the recent conversation, and resuming would make every
|
|
2388
2434
|
// turn of this run re-read the whole previous run as well.
|
|
2389
|
-
const sessionKey =
|
|
2435
|
+
const sessionKey = designOutcome
|
|
2390
2436
|
? run?.runId || companionSessionKey(run)
|
|
2391
2437
|
: companionSessionKey(run);
|
|
2392
2438
|
const infrastructureAbortController = new AbortController();
|
|
@@ -2536,10 +2582,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2536
2582
|
Boolean(event.message || event.name) ||
|
|
2537
2583
|
event.kind === 'turn_started';
|
|
2538
2584
|
if (!meaningful) return;
|
|
2539
|
-
//
|
|
2585
|
+
// Design runs narrate in plain text rather than through a progress tool
|
|
2540
2586
|
// (a tool call costs a whole turn), so that text is the user's progress.
|
|
2541
2587
|
const narration =
|
|
2542
|
-
|
|
2588
|
+
designOutcome
|
|
2543
2589
|
&& event.kind === 'output_delta'
|
|
2544
2590
|
&& String(event.activity || '').startsWith('assistant:');
|
|
2545
2591
|
const userFacingUpdate =
|
|
@@ -2603,7 +2649,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2603
2649
|
}
|
|
2604
2650
|
};
|
|
2605
2651
|
const controlMonitor = monitorControl();
|
|
2606
|
-
const rememberedSession =
|
|
2652
|
+
const rememberedSession = designOutcome
|
|
2607
2653
|
? null
|
|
2608
2654
|
: rememberedCompanionSession(sessionKey) ||
|
|
2609
2655
|
(run?.outcome?.session?.providerSessionId
|
|
@@ -2704,6 +2750,61 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2704
2750
|
})
|
|
2705
2751
|
: undefined,
|
|
2706
2752
|
})
|
|
2753
|
+
: webflowOutcome
|
|
2754
|
+
? createWebflowToolRuntime({
|
|
2755
|
+
assignment: executionOutcome,
|
|
2756
|
+
trace: options.trace,
|
|
2757
|
+
onActivity: progress,
|
|
2758
|
+
mcpEndpoint:
|
|
2759
|
+
options.apiBaseUrl && options.deviceToken
|
|
2760
|
+
? webflowMcpEndpoint(options.apiBaseUrl, {
|
|
2761
|
+
deviceToken: options.deviceToken,
|
|
2762
|
+
runId: run.runId,
|
|
2763
|
+
allowInsecureHttp: options.allowInsecureHttp,
|
|
2764
|
+
})
|
|
2765
|
+
: undefined,
|
|
2766
|
+
callPanelTool:
|
|
2767
|
+
options.apiBaseUrl && options.deviceToken
|
|
2768
|
+
? ({ callId, name, arguments: toolArguments, signal }) =>
|
|
2769
|
+
callWebflowPanelTool(options.apiBaseUrl, {
|
|
2770
|
+
deviceToken: options.deviceToken,
|
|
2771
|
+
runId: run.runId,
|
|
2772
|
+
callId,
|
|
2773
|
+
name,
|
|
2774
|
+
arguments: toolArguments,
|
|
2775
|
+
fetchImpl: options.fetchImpl,
|
|
2776
|
+
signal,
|
|
2777
|
+
allowInsecureHttp: options.allowInsecureHttp,
|
|
2778
|
+
})
|
|
2779
|
+
: undefined,
|
|
2780
|
+
findImages:
|
|
2781
|
+
options.apiBaseUrl && options.deviceToken
|
|
2782
|
+
? ({ query, orientation, count, signal }) =>
|
|
2783
|
+
findWebflowImages(options.apiBaseUrl, {
|
|
2784
|
+
deviceToken: options.deviceToken,
|
|
2785
|
+
runId: run.runId,
|
|
2786
|
+
query,
|
|
2787
|
+
orientation,
|
|
2788
|
+
count,
|
|
2789
|
+
fetchImpl: options.fetchImpl,
|
|
2790
|
+
signal,
|
|
2791
|
+
allowInsecureHttp: options.allowInsecureHttp,
|
|
2792
|
+
})
|
|
2793
|
+
: undefined,
|
|
2794
|
+
readWebsite:
|
|
2795
|
+
options.apiBaseUrl && options.deviceToken
|
|
2796
|
+
? ({ url, screenshotTop, signal }) =>
|
|
2797
|
+
readFramerWebsite(options.apiBaseUrl, {
|
|
2798
|
+
deviceToken: options.deviceToken,
|
|
2799
|
+
runId: run.runId,
|
|
2800
|
+
url,
|
|
2801
|
+
screenshotTop,
|
|
2802
|
+
fetchImpl: options.fetchImpl,
|
|
2803
|
+
signal,
|
|
2804
|
+
allowInsecureHttp: options.allowInsecureHttp,
|
|
2805
|
+
})
|
|
2806
|
+
: undefined,
|
|
2807
|
+
})
|
|
2707
2808
|
: createHarnessToolRuntime({
|
|
2708
2809
|
assignment: executionOutcome,
|
|
2709
2810
|
materialized,
|
|
@@ -2725,10 +2826,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2725
2826
|
let providerAttempts = 0;
|
|
2726
2827
|
let result;
|
|
2727
2828
|
try {
|
|
2728
|
-
const preflight =
|
|
2829
|
+
const preflight = designOutcome
|
|
2729
2830
|
? await Promise.race([harnessTools.preflight(), cancellation])
|
|
2730
2831
|
: null;
|
|
2731
|
-
const
|
|
2832
|
+
const projectGuidance = designOutcome
|
|
2732
2833
|
? await Promise.race([
|
|
2733
2834
|
harnessTools.loadProjectGuidance(),
|
|
2734
2835
|
cancellation,
|
|
@@ -2737,8 +2838,9 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2737
2838
|
const promptContext = {
|
|
2738
2839
|
doctrine: run?.doctrine,
|
|
2739
2840
|
doctrineVersion: run?.doctrineVersion,
|
|
2740
|
-
framerGuidance:
|
|
2741
|
-
projectInventory:
|
|
2841
|
+
framerGuidance: framerAgentOutcome ? projectGuidance?.content : undefined,
|
|
2842
|
+
projectInventory: framerAgentOutcome ? projectGuidance?.inventory : undefined,
|
|
2843
|
+
webflowGuidance: webflowOutcome ? projectGuidance?.content : undefined,
|
|
2742
2844
|
};
|
|
2743
2845
|
// Claude Code gets the run-independent instructions as its system prompt,
|
|
2744
2846
|
// which it caches across sessions: later runs and follow-up messages read
|
|
@@ -2746,16 +2848,14 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2746
2848
|
let systemPromptFile = null;
|
|
2747
2849
|
let systemPromptBytes = 0;
|
|
2748
2850
|
if (
|
|
2749
|
-
|
|
2851
|
+
designOutcome
|
|
2750
2852
|
&& definition.id === 'claude-code'
|
|
2751
2853
|
&& !options.providerAdapter?.runOutcome
|
|
2752
2854
|
) {
|
|
2753
|
-
const { instructions, task } =
|
|
2754
|
-
executionOutcome,
|
|
2755
|
-
product,
|
|
2756
|
-
|
|
2757
|
-
);
|
|
2758
|
-
const systemPrompt = `${claudeOutcomeSystemPrompt(product)}\n\n${instructions}`;
|
|
2855
|
+
const { instructions, task } = webflowOutcome
|
|
2856
|
+
? buildWebflowOutcomePrompt(executionOutcome, product, promptContext)
|
|
2857
|
+
: buildFramerOutcomePrompt(executionOutcome, product, promptContext);
|
|
2858
|
+
const systemPrompt = `${(webflowOutcome ? claudeWebflowOutcomeSystemPrompt : claudeOutcomeSystemPrompt)(product)}\n\n${instructions}`;
|
|
2759
2859
|
systemPromptFile = writeClaudeSystemPromptFile(systemPrompt);
|
|
2760
2860
|
systemPromptBytes = Buffer.byteLength(systemPrompt, 'utf8');
|
|
2761
2861
|
prompt = task;
|
|
@@ -2837,7 +2937,8 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2837
2937
|
cwd: materialized.directory,
|
|
2838
2938
|
nativeSkills,
|
|
2839
2939
|
framerHarness: framerAgentOutcome ? executionOutcome : false,
|
|
2840
|
-
|
|
2940
|
+
designHarness: designOutcome ? executionOutcome : false,
|
|
2941
|
+
webHosts: designOutcome ? framerWebHosts(executionOutcome) : [],
|
|
2841
2942
|
imageFiles,
|
|
2842
2943
|
harnessTools: {
|
|
2843
2944
|
definitions: codexDynamicToolSpecs(harnessTools.definitions),
|
|
@@ -2880,6 +2981,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2880
2981
|
harnessMode: true,
|
|
2881
2982
|
systemPromptFile,
|
|
2882
2983
|
framerHarness: framerAgentOutcome ? executionOutcome : false,
|
|
2984
|
+
designHarness: designOutcome ? executionOutcome : false,
|
|
2883
2985
|
harnessToolNames: harnessTools.toolNames,
|
|
2884
2986
|
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
2885
2987
|
imageFiles,
|
|
@@ -2904,6 +3006,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2904
3006
|
harnessMode: true,
|
|
2905
3007
|
systemPromptFile,
|
|
2906
3008
|
framerHarness: framerAgentOutcome ? executionOutcome : false,
|
|
3009
|
+
designHarness: designOutcome ? executionOutcome : false,
|
|
2907
3010
|
harnessToolNames: harnessTools.toolNames,
|
|
2908
3011
|
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
2909
3012
|
imageFiles,
|
|
@@ -2973,29 +3076,34 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2973
3076
|
) {
|
|
2974
3077
|
synchronization = await harnessTools.synchronize();
|
|
2975
3078
|
}
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
repairPrompt
|
|
2989
|
-
|
|
2990
|
-
|
|
3079
|
+
// The model is sent back until the checks pass or the rounds run out; each
|
|
3080
|
+
// round names exactly what is still wrong.
|
|
3081
|
+
let assessedCompletion = null;
|
|
3082
|
+
for (let round = 0; round < OUTCOME_REPAIR_ROUNDS; round += 1) {
|
|
3083
|
+
if (typeof harnessTools.assessCompletion === 'function') {
|
|
3084
|
+
await harnessTools.assessCompletion(rawCompletion);
|
|
3085
|
+
assessedCompletion = rawCompletion;
|
|
3086
|
+
}
|
|
3087
|
+
const unappliedChangePrompt =
|
|
3088
|
+
rawCompletion.status === successStatus
|
|
3089
|
+
? harnessTools.unappliedChangePrompt?.(rawCompletion) || null
|
|
3090
|
+
: null;
|
|
3091
|
+
const repairPrompt =
|
|
3092
|
+
rawCompletion.status === successStatus
|
|
3093
|
+
? harnessTools.verificationRepairPrompt?.() || unappliedChangePrompt
|
|
3094
|
+
: harnessTools.unblockedFailurePrompt?.(rawCompletion) || null;
|
|
3095
|
+
if (
|
|
3096
|
+
!repairPrompt
|
|
3097
|
+
|| Date.now() - callStartedAt >= outcomeMaxDurationMs - 5_000
|
|
3098
|
+
) break;
|
|
2991
3099
|
progress({
|
|
2992
3100
|
kind: 'status_update',
|
|
2993
3101
|
source: 'harness',
|
|
2994
3102
|
message:
|
|
2995
3103
|
repairPrompt === unappliedChangePrompt
|
|
2996
|
-
?
|
|
3104
|
+
? `Nothing has changed in ${webflowOutcome ? 'Webflow' : 'Framer'} yet, so I’m making the change before I finish.`
|
|
2997
3105
|
: rawCompletion.status === successStatus
|
|
2998
|
-
? 'I’m
|
|
3106
|
+
? 'I’m checking the result against your request before I finish.'
|
|
2999
3107
|
: 'Nothing is blocking this change, so I’m continuing the work.',
|
|
3000
3108
|
});
|
|
3001
3109
|
const repairResult = await Promise.race([
|
|
@@ -3037,7 +3145,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
3037
3145
|
updatedAt: Date.now(),
|
|
3038
3146
|
});
|
|
3039
3147
|
}
|
|
3040
|
-
if (typeof harnessTools.assessCompletion === 'function') {
|
|
3148
|
+
if (typeof harnessTools.assessCompletion === 'function' && assessedCompletion !== rawCompletion) {
|
|
3041
3149
|
await harnessTools.assessCompletion(rawCompletion);
|
|
3042
3150
|
}
|
|
3043
3151
|
const completion =
|
|
@@ -3055,7 +3163,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
3055
3163
|
synchronization?.sourceHash || harnessTools.sourceHash() || null,
|
|
3056
3164
|
...(typeof harnessTools.summary === 'function'
|
|
3057
3165
|
? {
|
|
3058
|
-
framer: {
|
|
3166
|
+
[webflowOutcome ? 'webflow' : 'framer']: {
|
|
3059
3167
|
...harnessTools.summary(),
|
|
3060
3168
|
webCalls: webCallSummary(),
|
|
3061
3169
|
},
|
|
@@ -3098,19 +3206,22 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
3098
3206
|
const failure =
|
|
3099
3207
|
error instanceof Error
|
|
3100
3208
|
? error
|
|
3101
|
-
: new Error(String(error || 'The
|
|
3209
|
+
: new Error(String(error || 'The Dexter run failed.'));
|
|
3102
3210
|
usageAccumulator.add(failure.companionUsage || {});
|
|
3103
|
-
const
|
|
3211
|
+
const harnessSummary =
|
|
3104
3212
|
typeof harnessTools.summary === 'function'
|
|
3105
3213
|
? {
|
|
3106
3214
|
...harnessTools.summary(),
|
|
3107
3215
|
webCalls: webCallSummary(),
|
|
3108
3216
|
}
|
|
3109
3217
|
: undefined;
|
|
3110
|
-
failure.
|
|
3218
|
+
if (webflowOutcome) failure.webflow = harnessSummary;
|
|
3219
|
+
else failure.framer = harnessSummary;
|
|
3111
3220
|
failure.companionUsage = {
|
|
3112
3221
|
...usageAccumulator.snapshot(),
|
|
3113
|
-
...(
|
|
3222
|
+
...(harnessSummary
|
|
3223
|
+
? { [webflowOutcome ? 'webflow' : 'framer']: harnessSummary }
|
|
3224
|
+
: {}),
|
|
3114
3225
|
};
|
|
3115
3226
|
throw failure;
|
|
3116
3227
|
} finally {
|
|
@@ -3118,7 +3229,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
3118
3229
|
controlMonitorAbort.abort();
|
|
3119
3230
|
await controlMonitor.catch(() => undefined);
|
|
3120
3231
|
await harnessTools
|
|
3121
|
-
.close({ waitForPending: !cancelRequested })
|
|
3232
|
+
.close({ waitForPending: !cancelRequested && !bridgeShutdown.signal.aborted })
|
|
3122
3233
|
.catch(() => undefined);
|
|
3123
3234
|
removeOutcomeReferenceAssets(materialized);
|
|
3124
3235
|
removeOutcomeWorkspace(materialized);
|
package/src/agentOutput.js
CHANGED
|
@@ -521,6 +521,12 @@ export function createClaudeProgressParser(onProgress, options = {}) {
|
|
|
521
521
|
if (!trimmed) return;
|
|
522
522
|
try {
|
|
523
523
|
const row = JSON.parse(trimmed);
|
|
524
|
+
if (row?.type === 'system' && row.subtype !== 'init') {
|
|
525
|
+
options.trace?.warn?.('claude_system_event', { event: JSON.stringify(row).slice(0, 1_000) });
|
|
526
|
+
}
|
|
527
|
+
if (row?.type === 'rate_limit_event' && row.rate_limit_info?.status !== 'allowed') {
|
|
528
|
+
options.trace?.warn?.('claude_rate_limit', { rateLimit: row.rate_limit_info });
|
|
529
|
+
}
|
|
524
530
|
if (row?.type === 'stream_event') processStreamEvent(row);
|
|
525
531
|
if (row?.type === 'assistant') finishDrafting(row);
|
|
526
532
|
if (row?.type === 'assistant') processAssistant(row);
|
package/src/api.js
CHANGED
|
@@ -184,6 +184,7 @@ export function pollRun(apiBaseUrl, {
|
|
|
184
184
|
model,
|
|
185
185
|
metadata,
|
|
186
186
|
runtimeProfiles,
|
|
187
|
+
refreshingModelCatalog = false,
|
|
187
188
|
fetchImpl,
|
|
188
189
|
allowInsecureHttp,
|
|
189
190
|
}) {
|
|
@@ -202,6 +203,7 @@ export function pollRun(apiBaseUrl, {
|
|
|
202
203
|
status: 'polling',
|
|
203
204
|
metadata: bridgeMetadata(normalizedAgent, normalizedModel, metadata),
|
|
204
205
|
runtimeProfiles,
|
|
206
|
+
refreshingModelCatalog,
|
|
205
207
|
},
|
|
206
208
|
});
|
|
207
209
|
}
|
|
@@ -265,6 +267,79 @@ export function readFramerWebsite(apiBaseUrl, {
|
|
|
265
267
|
);
|
|
266
268
|
}
|
|
267
269
|
|
|
270
|
+
// The run's Webflow MCP endpoint on Dexter's API. The Webflow token never leaves the API.
|
|
271
|
+
export function webflowMcpEndpoint(apiBaseUrl, { deviceToken, runId, allowInsecureHttp }) {
|
|
272
|
+
return {
|
|
273
|
+
url: `${normalizeApiBaseUrl(apiBaseUrl, { allowInsecureHttp })}/ai/framer/companion/runs/${encodeURIComponent(runId)}/webflow-mcp`,
|
|
274
|
+
headers: {
|
|
275
|
+
...bearerHeaders(deviceToken),
|
|
276
|
+
'X-Dexter-Companion-Version': BRIDGE_VERSION,
|
|
277
|
+
'X-Dexter-Platform': process.platform,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function callWebflowPanelTool(apiBaseUrl, {
|
|
283
|
+
deviceToken,
|
|
284
|
+
runId,
|
|
285
|
+
callId,
|
|
286
|
+
name,
|
|
287
|
+
arguments: toolArguments,
|
|
288
|
+
fetchImpl,
|
|
289
|
+
signal,
|
|
290
|
+
allowInsecureHttp,
|
|
291
|
+
}) {
|
|
292
|
+
return requestJson(
|
|
293
|
+
apiBaseUrl,
|
|
294
|
+
`/ai/framer/companion/runs/${encodeURIComponent(runId)}/panel-tools`,
|
|
295
|
+
{
|
|
296
|
+
method: 'POST',
|
|
297
|
+
deviceToken,
|
|
298
|
+
fetchImpl,
|
|
299
|
+
signal,
|
|
300
|
+
allowInsecureHttp,
|
|
301
|
+
timeoutMs: 45_000,
|
|
302
|
+
body: {
|
|
303
|
+
callId,
|
|
304
|
+
name,
|
|
305
|
+
arguments: toolArguments || {},
|
|
306
|
+
platform: process.platform,
|
|
307
|
+
companionVersion: BRIDGE_VERSION,
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function findWebflowImages(apiBaseUrl, {
|
|
314
|
+
deviceToken,
|
|
315
|
+
runId,
|
|
316
|
+
query,
|
|
317
|
+
orientation,
|
|
318
|
+
count,
|
|
319
|
+
fetchImpl,
|
|
320
|
+
signal,
|
|
321
|
+
allowInsecureHttp,
|
|
322
|
+
}) {
|
|
323
|
+
return requestJson(
|
|
324
|
+
apiBaseUrl,
|
|
325
|
+
`/ai/framer/companion/runs/${encodeURIComponent(runId)}/find-images`,
|
|
326
|
+
{
|
|
327
|
+
method: 'POST',
|
|
328
|
+
deviceToken,
|
|
329
|
+
fetchImpl,
|
|
330
|
+
signal,
|
|
331
|
+
allowInsecureHttp,
|
|
332
|
+
body: {
|
|
333
|
+
query,
|
|
334
|
+
...(orientation ? { orientation } : {}),
|
|
335
|
+
...(count ? { count } : {}),
|
|
336
|
+
platform: process.platform,
|
|
337
|
+
companionVersion: BRIDGE_VERSION,
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
268
343
|
export function postRunEvent(apiBaseUrl, {
|
|
269
344
|
deviceToken,
|
|
270
345
|
runId,
|