@gakim-digital/dexter-bridge 0.11.0 → 0.11.2

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/README.md CHANGED
@@ -39,7 +39,7 @@ progress, cancellation, and result interface.
39
39
  The Framer harness receives only these project-scoped tools:
40
40
 
41
41
  - `framer_instructions` and `framer_context`
42
- - `framer_read_project` and `framer_apply_changes`
42
+ - `framer_read_project`, `framer_verify_interactions`, and `framer_apply_changes`
43
43
  - restricted `framer_read` and `framer_write`
44
44
  - `progress_update`
45
45
 
@@ -47,21 +47,34 @@ Native shell, filesystem, web, and unrelated-project access are disabled for
47
47
  Framer outcome runs. Publishing is denied unless a future assignment explicitly
48
48
  authorizes it.
49
49
 
50
- Project connection is automatic and completes before the model starts:
51
-
52
- - The run carries the project ID that Dexter read from the open Framer project.
53
- - The bridge first reuses an active Framer Agent session or locally authorized
54
- project.
55
- - If needed, the active paired device redeems the matching encrypted
56
- per-project credential from the API and installs it through Framer’s official
57
- CLI.
58
- - If no credential exists, or Framer rejects a stale one, Dexter opens Framer’s
59
- browser authorization and resumes the same run automatically after approval.
50
+ Bridges advertise `framer-interaction-verification-v1`,
51
+ `framer-behavior-contract-v1`, and `framer-mechanism-verification-v1` only
52
+ when interaction work is declared as observable behavior, checked against the
53
+ selected Framer mechanism before mutation, and verified from live canonical
54
+ Framer nodes afterward. Repeated behavior is verified on one representative
55
+ target before the remaining targets are changed.
56
+
57
+ Project connection is handled inside Dexter and completes before the model
58
+ starts:
59
+
60
+ - The plugin reads the exact active project or branch URL directly from Framer.
61
+ - The user adds a Framer project API key once in Dexter. The API verifies it
62
+ through Framer's documented `connect(projectId, apiKey)` plus
63
+ `getProjectInfo()` flow, then encrypts and stores it only after that succeeds.
64
+ - The active hosted runtime redeems that verified credential only for the
65
+ matching run and device, then installs it with Framer’s official
66
+ `project auth` command against the authoritative 20-character project ID.
67
+ - The runtime opens an official Framer Agent session, reads the active branch
68
+ back from Framer, and confirms the authoritative project ID again.
69
+ - Rejected credentials are revoked. Replacing a key creates a new credential
70
+ version, so a cached runtime session cannot reuse the replacement.
71
+ - Hosted worker images bundle the Framer skills and seed them into each
72
+ runtime's private writable home before processing runs.
60
73
 
61
74
  The user never runs `@framer/agent setup`, `project auth`, or `session new`.
62
75
  The raw project credential is scoped to the matching active run and paired
63
76
  device, is excluded from run payloads and logs, and is never sent to the Framer
64
- plugin.
77
+ plugin after the one-time authenticated submission.
65
78
 
66
79
  The bridge reports provider token usage on terminal events, including
67
80
  interrupted and cancelled runs. Claude-reported
@@ -291,10 +304,10 @@ Recommended release env:
291
304
 
292
305
  ```bash
293
306
  FRAMER_COMPANION_DOWNLOAD_URL=https://instawebai.com/dexter-bridge
294
- NEXT_PUBLIC_DEXTER_BRIDGE_VERSION=0.11.0
307
+ NEXT_PUBLIC_DEXTER_BRIDGE_VERSION=0.11.1
295
308
  NEXT_PUBLIC_DEXTER_BRIDGE_DOWNLOAD_BASE_URL=https://instawebai.com/downloads/dexter-bridge
296
- NEXT_PUBLIC_DEXTER_BRIDGE_MAC_ARM64_URL=https://instawebai.com/downloads/dexter-bridge/Dexter-Bridge-0.11.0-mac-arm64.dmg
297
- NEXT_PUBLIC_DEXTER_BRIDGE_MAC_X64_URL=https://instawebai.com/downloads/dexter-bridge/Dexter-Bridge-0.11.0-mac-x64.dmg
298
- NEXT_PUBLIC_DEXTER_BRIDGE_MAC_ZIP_URL=https://instawebai.com/downloads/dexter-bridge/Dexter-Bridge-0.11.0-mac-universal.zip
299
- NEXT_PUBLIC_DEXTER_BRIDGE_WIN_X64_URL=https://instawebai.com/downloads/dexter-bridge/Dexter-Bridge-0.11.0-win-x64.exe
309
+ NEXT_PUBLIC_DEXTER_BRIDGE_MAC_ARM64_URL=https://instawebai.com/downloads/dexter-bridge/Dexter-Bridge-0.11.1-mac-arm64.dmg
310
+ NEXT_PUBLIC_DEXTER_BRIDGE_MAC_X64_URL=https://instawebai.com/downloads/dexter-bridge/Dexter-Bridge-0.11.1-mac-x64.dmg
311
+ NEXT_PUBLIC_DEXTER_BRIDGE_MAC_ZIP_URL=https://instawebai.com/downloads/dexter-bridge/Dexter-Bridge-0.11.1-mac-universal.zip
312
+ NEXT_PUBLIC_DEXTER_BRIDGE_WIN_X64_URL=https://instawebai.com/downloads/dexter-bridge/Dexter-Bridge-0.11.1-win-x64.exe
300
313
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
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": {
package/src/agent.js CHANGED
@@ -4,7 +4,11 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import crossSpawn from 'cross-spawn';
7
- import { getFramerProjectAuthorization, postRunEvent } from './api.js';
7
+ import {
8
+ getFramerProjectAuthorization,
9
+ postRunEvent,
10
+ reportFramerProjectAuthorizationVerification,
11
+ } from './api.js';
8
12
  import {
9
13
  createCompanionUsageAccumulator,
10
14
  createClaudeProgressParser,
@@ -60,15 +64,15 @@ const HARNESS_MCP_SERVER_PATH = fileURLToPath(
60
64
  export function harnessMcpProcessEnvironment(
61
65
  gateway,
62
66
  versions = process.versions,
63
- allowedToolNames,
67
+ toolDefinitions,
64
68
  ) {
65
69
  return {
66
70
  INSTAWEB_HARNESS_GATEWAY_URL: gateway.url,
67
71
  INSTAWEB_HARNESS_GATEWAY_TOKEN: gateway.token,
68
- ...(Array.isArray(allowedToolNames)
72
+ ...(Array.isArray(toolDefinitions)
69
73
  ? {
70
- INSTAWEB_HARNESS_ALLOWED_TOOLS:
71
- JSON.stringify(allowedToolNames),
74
+ INSTAWEB_HARNESS_TOOL_DEFINITIONS:
75
+ JSON.stringify(toolDefinitions),
72
76
  }
73
77
  : {}),
74
78
  ...(versions?.electron ? { ELECTRON_RUN_AS_NODE: '1' } : {}),
@@ -92,8 +96,8 @@ function deltaModelSessionsEnabled(env = process.env) {
92
96
  }
93
97
 
94
98
  function companionSessionKey(run) {
95
- return run?.outcome?.session?.goalSessionId
96
- || run?.outcome?.session?.sessionId
99
+ return run?.outcome?.session?.sessionId
100
+ || run?.outcome?.session?.goalSessionId
97
101
  || run?.modelTurn?.session?.goalSessionId
98
102
  || run?.modelTurn?.session?.sessionId
99
103
  || run?.turnId
@@ -318,7 +322,8 @@ function claudeOutcomeSystemPrompt(product) {
318
322
  'Inspect the project, perform the requested work, verify the result, and return the structured outcome.',
319
323
  'Do not access local credentials, unrelated files, other projects, account settings, or billing.',
320
324
  'Do not publish unless the assignment explicitly authorizes publishing.',
321
- 'Call progress_update before the first inspection or edit and at meaningful phase changes.',
325
+ 'Call progress_update before the first inspection or edit and at meaningful phase changes. Use one or two natural first-person sentences that name the specific page area, component, or behavior and the concrete change you are about to make or are making now; optionally say what you will check next. Avoid generic updates such as "working on the next step", "thinking", "loading", or "making changes".',
326
+ 'If framer_write is rejected by the safety boundary, do not retry JavaScript variants. Switch to framer_apply_changes, inspect the relevant state, or report the concrete blocker.',
322
327
  `Return only the structured outcome result requested by ${name}.`,
323
328
  ].join(' ');
324
329
  }
@@ -1317,6 +1322,13 @@ function adapterInvocationModel(run, selectedModel, agent) {
1317
1322
  if (typeof runtimeInvocation === 'string' && runtimeInvocation.trim()) {
1318
1323
  return runtimeInvocation.trim();
1319
1324
  }
1325
+ if (
1326
+ run?.companion?.modelPolicy?.owner === 'harness'
1327
+ && typeof selectedModel?.invocationName === 'string'
1328
+ && selectedModel.invocationName.trim()
1329
+ ) {
1330
+ return selectedModel.invocationName.trim();
1331
+ }
1320
1332
  const explicit = run?.companion?.model?.invocationName;
1321
1333
  if (typeof explicit === 'string' && explicit.trim()) return explicit.trim();
1322
1334
  if (selectedModel?.invocationName) return selectedModel.invocationName;
@@ -1326,12 +1338,63 @@ function adapterInvocationModel(run, selectedModel, agent) {
1326
1338
  return raw.startsWith(prefix) ? raw.slice(prefix.length) : raw || undefined;
1327
1339
  }
1328
1340
 
1341
+ function harnessOutcomeModel(run, definition) {
1342
+ const requestedModel =
1343
+ run?.runtimeProfile?.model?.id
1344
+ || run?.companion?.model?.id
1345
+ || run?.model;
1346
+ const requested =
1347
+ run?.runtimeProfile?.model
1348
+ || companionModelDefinition(requestedModel, definition.id);
1349
+ if (run?.companion?.modelPolicy?.owner !== 'harness') return requested;
1350
+ const candidates = (
1351
+ Array.isArray(run?.companion?.models)
1352
+ ? run.companion.models
1353
+ : []
1354
+ ).filter((model) =>
1355
+ model
1356
+ && typeof model === 'object'
1357
+ && String(model.agent || definition.id) === definition.id);
1358
+ if (!candidates.length) return requested;
1359
+ const mode = String(run.companion.modelPolicy.mode || 'auto');
1360
+ if (mode === 'advanced') {
1361
+ return candidates.find((model) => model.id === requested.id) || requested;
1362
+ }
1363
+ if (mode === 'auto') {
1364
+ return (
1365
+ candidates.find((model) => model.isDefault === true)
1366
+ || candidates.find((model) => model.id === requested.id)
1367
+ || requested
1368
+ );
1369
+ }
1370
+ const tier = mode === 'fast' ? 'fast' : 'director';
1371
+ const costRank = { '$': 1, '$$': 2, '$$$': 3 };
1372
+ return (
1373
+ candidates.find((model) => model.routingTier === tier)
1374
+ || [...candidates].sort((left, right) => {
1375
+ const leftRank = costRank[left.costTier] || 2;
1376
+ const rightRank = costRank[right.costTier] || 2;
1377
+ return mode === 'fast'
1378
+ ? leftRank - rightRank
1379
+ : rightRank - leftRank;
1380
+ })[0]
1381
+ || requested
1382
+ );
1383
+ }
1384
+
1329
1385
  function controlRequestsCancellation(response) {
1330
1386
  return Boolean(
1331
1387
  response?.control?.cancelRequested || response?.status === 'cancelled' || response?.status === 'expired',
1332
1388
  );
1333
1389
  }
1334
1390
 
1391
+ function controlCancellationSource(response) {
1392
+ const source = String(response?.control?.cancelSource || '').trim();
1393
+ if (source) return source;
1394
+ if (response?.status === 'expired') return 'deadline';
1395
+ return 'unknown';
1396
+ }
1397
+
1335
1398
  function boundedDurationMs(value, fallback, minimum = 10, maximum = 60 * 60 * 1000) {
1336
1399
  const parsed = Number(value);
1337
1400
  if (!Number.isFinite(parsed)) return fallback;
@@ -1357,11 +1420,31 @@ function waitForControl(delayMs, signal) {
1357
1420
  }
1358
1421
 
1359
1422
  class CompanionRunCancelledError extends Error {
1360
- constructor(message = 'The local bridge model turn was cancelled.') {
1423
+ constructor(
1424
+ message = 'The local bridge model turn was cancelled.',
1425
+ cancellationSource = 'unknown',
1426
+ ) {
1361
1427
  super(message);
1362
1428
  this.name = 'CompanionRunCancelledError';
1363
1429
  this.code = 'RUN_CANCELLED';
1430
+ this.cancellationSource = cancellationSource;
1431
+ }
1432
+ }
1433
+
1434
+ function cancellationMessage(error) {
1435
+ const framer = error?.framer || error?.companionUsage?.framer;
1436
+ const mutationCalls = Number(framer?.mutationCalls) || 0;
1437
+ if (mutationCalls <= 0) {
1438
+ return 'Stopped before Dexter completed a Framer change.';
1364
1439
  }
1440
+ const verificationCompleted = framer?.verification?.completed === true;
1441
+ return [
1442
+ `Stopped after ${mutationCalls} successful Framer change ${mutationCalls === 1 ? 'batch' : 'batches'}.`,
1443
+ 'Those changes remain in Framer.',
1444
+ verificationCompleted
1445
+ ? 'The harness verified the latest project state before the run stopped.'
1446
+ : 'The harness did not verify the final project state before the run stopped.',
1447
+ ].join(' ');
1365
1448
  }
1366
1449
 
1367
1450
  async function callProviderAdapter(
@@ -2093,13 +2176,14 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2093
2176
  || run?.companion?.model?.id
2094
2177
  || run?.model
2095
2178
  || options.selectedModel;
2096
- const selectedModel =
2097
- run?.runtimeProfile?.model
2098
- || companionModelDefinition(requestedModel, definition.id);
2179
+ const selectedModel = harnessOutcomeModel(run, definition);
2099
2180
  const selectedModelId =
2100
- typeof requestedModel === 'string' && requestedModel.trim()
2101
- ? requestedModel.trim()
2102
- : selectedModel?.id || definition.model;
2181
+ selectedModel?.id
2182
+ || (
2183
+ typeof requestedModel === 'string' && requestedModel.trim()
2184
+ ? requestedModel.trim()
2185
+ : definition.model
2186
+ );
2103
2187
  const outcomeMaxDurationMs = boundedDurationMs(
2104
2188
  run?.outcome?.maxDurationMs
2105
2189
  ?? options.maxDurationMs
@@ -2120,6 +2204,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2120
2204
  const controlMonitorAbort = new AbortController();
2121
2205
  let finished = false;
2122
2206
  let cancelRequested = false;
2207
+ let cancellationSource = 'unknown';
2123
2208
  let cancelAction = null;
2124
2209
  let rejectCancellation;
2125
2210
  let latestTokenUsage = normalizeCompanionTokenUsage();
@@ -2128,7 +2213,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2128
2213
  rejectCancellation = reject;
2129
2214
  });
2130
2215
  const cancellationError = () => {
2131
- const error = new CompanionRunCancelledError();
2216
+ const error = new CompanionRunCancelledError(
2217
+ 'The local bridge model turn was cancelled.',
2218
+ cancellationSource,
2219
+ );
2132
2220
  error.companionUsage = {
2133
2221
  tokenUsage: latestTokenUsage,
2134
2222
  usageAvailable: latestTokenUsage.totalTokens > 0,
@@ -2137,7 +2225,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2137
2225
  };
2138
2226
  return error;
2139
2227
  };
2140
- const requestCancel = () => {
2228
+ const requestCancel = (source = 'unknown') => {
2229
+ if (cancellationSource === 'unknown' && source) {
2230
+ cancellationSource = source;
2231
+ }
2141
2232
  if (cancelRequested) return cancelAction;
2142
2233
  cancelRequested = true;
2143
2234
  infrastructureAbortController.abort();
@@ -2153,7 +2244,9 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2153
2244
  .then(async () => {
2154
2245
  if (finished) return;
2155
2246
  const response = await send('activity', payload);
2156
- if (controlRequestsCancellation(response)) await requestCancel();
2247
+ if (controlRequestsCancellation(response)) {
2248
+ await requestCancel(controlCancellationSource(response));
2249
+ }
2157
2250
  })
2158
2251
  .catch((error) => {
2159
2252
  if (!finished) {
@@ -2185,7 +2278,9 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2185
2278
  Boolean(event.message || event.name) ||
2186
2279
  event.kind === 'turn_started';
2187
2280
  if (!meaningful) return;
2188
- const userFacingUpdate = event.kind === 'status_update';
2281
+ const userFacingUpdate =
2282
+ event.kind === 'status_update'
2283
+ && event.source === 'harness';
2189
2284
  void postActivity({
2190
2285
  stage: 'outcome',
2191
2286
  outcomeId: run?.outcome?.outcomeId,
@@ -2226,7 +2321,9 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2226
2321
  occurredAt: new Date().toISOString(),
2227
2322
  message: `${definition.label} is still working.`,
2228
2323
  });
2229
- if (controlRequestsCancellation(response)) await requestCancel();
2324
+ if (controlRequestsCancellation(response)) {
2325
+ await requestCancel(controlCancellationSource(response));
2326
+ }
2230
2327
  } catch (error) {
2231
2328
  if (!controlMonitorAbort.signal.aborted && !cancelRequested) {
2232
2329
  options.trace?.warn?.('outcome_control_poll_failed', {
@@ -2291,7 +2388,29 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2291
2388
  allowInsecureHttp: options.allowInsecureHttp,
2292
2389
  })
2293
2390
  : undefined,
2294
- openAuthorizationUrl: options.openAuthorizationUrl,
2391
+ verifyProjectAuthorization:
2392
+ options.apiBaseUrl && options.deviceToken
2393
+ ? ({
2394
+ status,
2395
+ framerProjectId,
2396
+ framerProjectUrl,
2397
+ credentialVersion,
2398
+ errorCode,
2399
+ signal,
2400
+ }) =>
2401
+ reportFramerProjectAuthorizationVerification(options.apiBaseUrl, {
2402
+ deviceToken: options.deviceToken,
2403
+ runId: run.runId,
2404
+ status,
2405
+ framerProjectId,
2406
+ framerProjectUrl,
2407
+ credentialVersion,
2408
+ errorCode,
2409
+ fetchImpl: options.fetchImpl,
2410
+ signal,
2411
+ allowInsecureHttp: options.allowInsecureHttp,
2412
+ })
2413
+ : undefined,
2295
2414
  })
2296
2415
  : createHarnessToolRuntime({
2297
2416
  assignment: run?.outcome,
@@ -2300,10 +2419,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2300
2419
  trace: options.trace,
2301
2420
  onActivity: progress,
2302
2421
  });
2303
- let prompt = buildOutcomePrompt(run.outcome, product);
2304
- if (materializedNativeSkills) {
2305
- prompt = claudeNativeSkillPrompt(prompt, materializedNativeSkills);
2306
- }
2422
+ let prompt = '';
2307
2423
  const schema = outcomeOutputSchema();
2308
2424
  assertOutcomeResponseContract(
2309
2425
  STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
@@ -2317,6 +2433,24 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2317
2433
  if (framerAgentOutcome) {
2318
2434
  await Promise.race([harnessTools.preflight(), cancellation]);
2319
2435
  }
2436
+ const framerGuidance = framerAgentOutcome
2437
+ ? await Promise.race([
2438
+ harnessTools.loadProjectGuidance(),
2439
+ cancellation,
2440
+ ])
2441
+ : null;
2442
+ harnessTools.configurePromptContext?.({
2443
+ doctrineLoaded: Boolean(run?.doctrine),
2444
+ doctrineVersion: run?.doctrineVersion,
2445
+ });
2446
+ prompt = buildOutcomePrompt(run.outcome, product, {
2447
+ doctrine: run?.doctrine,
2448
+ doctrineVersion: run?.doctrineVersion,
2449
+ framerGuidance: framerGuidance?.content,
2450
+ });
2451
+ if (materializedNativeSkills) {
2452
+ prompt = claudeNativeSkillPrompt(prompt, materializedNativeSkills);
2453
+ }
2320
2454
  const gateway =
2321
2455
  definition.id === 'codex' ? null : await harnessTools.startGateway();
2322
2456
  const mcpServer = gateway
@@ -2327,7 +2461,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2327
2461
  environment: harnessMcpProcessEnvironment(
2328
2462
  gateway,
2329
2463
  process.versions,
2330
- harnessTools.toolNames,
2464
+ harnessTools.definitions,
2331
2465
  ),
2332
2466
  }
2333
2467
  : null;
@@ -2349,15 +2483,22 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2349
2483
  });
2350
2484
  if (controlRequestsCancellation(statusResponse)) {
2351
2485
  await options.providerAdapter?.cancel?.(sessionKey);
2352
- throw new CompanionRunCancelledError();
2486
+ throw new CompanionRunCancelledError(
2487
+ undefined,
2488
+ controlCancellationSource(statusResponse),
2489
+ );
2353
2490
  }
2354
- const providerExecution = (async () => {
2491
+ const executeProviderOutcome = async (
2492
+ turnPrompt,
2493
+ resumeSessionIdOverride,
2494
+ step = 0,
2495
+ ) => {
2355
2496
  if (options.providerAdapter?.runOutcome) {
2356
2497
  const providerResult = await options.providerAdapter.runOutcome({
2357
2498
  runId: run.runId,
2358
2499
  sessionId: sessionKey,
2359
- resumeSessionId: rememberedSession?.providerSessionId,
2360
- prompt,
2500
+ resumeSessionId: resumeSessionIdOverride,
2501
+ prompt: turnPrompt,
2361
2502
  model: adapterInvocationModel(run, selectedModel, definition.id),
2362
2503
  outputSchema: schema,
2363
2504
  responseContract: STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
@@ -2387,12 +2528,12 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2387
2528
  }
2388
2529
  const resumeSessionId =
2389
2530
  agentSessionResumeEnabled(definition, options.env)
2390
- ? rememberedSession?.providerSessionId
2531
+ ? resumeSessionIdOverride
2391
2532
  : undefined;
2392
2533
  let processResult;
2393
2534
  try {
2394
2535
  providerAttempts += 1;
2395
- processResult = await callLocalJsonAgent(definition.id, prompt, {
2536
+ processResult = await callLocalJsonAgent(definition.id, turnPrompt, {
2396
2537
  ...options,
2397
2538
  model: selectedModelId,
2398
2539
  timeoutMs: outcomeInactivityTimeoutMs,
@@ -2401,7 +2542,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2401
2542
  outputSchema: schema,
2402
2543
  responseContract: STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
2403
2544
  product,
2404
- step: 0,
2545
+ step,
2405
2546
  maxDurationMs: outcomeMaxDurationMs,
2406
2547
  cwd: materialized.directory,
2407
2548
  harnessMode: true,
@@ -2415,7 +2556,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2415
2556
  } catch (error) {
2416
2557
  if (!resumeSessionId || !resumeFailure(error)) throw error;
2417
2558
  providerAttempts += 1;
2418
- processResult = await callLocalJsonAgent(definition.id, prompt, {
2559
+ processResult = await callLocalJsonAgent(definition.id, turnPrompt, {
2419
2560
  ...options,
2420
2561
  model: selectedModelId,
2421
2562
  timeoutMs: outcomeInactivityTimeoutMs,
@@ -2423,7 +2564,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2423
2564
  outputSchema: schema,
2424
2565
  responseContract: STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
2425
2566
  product,
2426
- step: 0,
2567
+ step,
2427
2568
  maxDurationMs: outcomeMaxDurationMs,
2428
2569
  cwd: materialized.directory,
2429
2570
  harnessMode: true,
@@ -2441,7 +2582,11 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2441
2582
  text: parsedOutput.resultText,
2442
2583
  sessionId: parsedOutput.sessionId,
2443
2584
  };
2444
- })();
2585
+ };
2586
+ const providerExecution = executeProviderOutcome(
2587
+ prompt,
2588
+ rememberedSession?.providerSessionId,
2589
+ );
2445
2590
  const infrastructureFailure = harnessTools
2446
2591
  .waitForFatalInfrastructure()
2447
2592
  .then(async (error) => {
@@ -2466,7 +2611,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2466
2611
  if (cancelRequested) throw cancellationError();
2467
2612
  throw error;
2468
2613
  }
2469
- const providerSessionId = result?.threadId || result?.sessionId;
2614
+ let providerSessionId = result?.threadId || result?.sessionId;
2470
2615
  if (providerSessionId || options.providerAdapter) {
2471
2616
  rememberCompanionSession(sessionKey, {
2472
2617
  providerSessionId:
@@ -2478,7 +2623,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2478
2623
  });
2479
2624
  }
2480
2625
  usageAccumulator.add(result);
2481
- const completion = normalizeOutcomeCompletion(
2626
+ let rawCompletion = normalizeOutcomeCompletion(
2482
2627
  extractJsonObject(result?.text || result?.resultText || ''),
2483
2628
  run?.outcome?.outcomeId,
2484
2629
  );
@@ -2486,11 +2631,68 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2486
2631
  if (harnessTools.directWorkspace) {
2487
2632
  synchronization = await harnessTools.inspect();
2488
2633
  } else if (
2489
- completion.status === 'ready_for_verification'
2634
+ rawCompletion.status === 'completed'
2490
2635
  || harnessTools.hasPendingChanges()
2491
2636
  ) {
2492
2637
  synchronization = await harnessTools.synchronize();
2493
2638
  }
2639
+ const repairPrompt =
2640
+ rawCompletion.status === 'completed'
2641
+ && typeof harnessTools.verificationRepairPrompt === 'function'
2642
+ ? harnessTools.verificationRepairPrompt()
2643
+ : null;
2644
+ if (
2645
+ repairPrompt
2646
+ && Date.now() - callStartedAt < outcomeMaxDurationMs - 5_000
2647
+ ) {
2648
+ progress({
2649
+ kind: 'status_update',
2650
+ source: 'harness',
2651
+ message:
2652
+ 'I’m completing the final Framer verification before I finish.',
2653
+ });
2654
+ const repairResult = await Promise.race([
2655
+ executeProviderOutcome(
2656
+ repairPrompt,
2657
+ providerSessionId
2658
+ || rememberedSession?.providerSessionId
2659
+ || sessionKey,
2660
+ 1,
2661
+ ),
2662
+ infrastructureFailure,
2663
+ cancellation,
2664
+ ]);
2665
+ usageAccumulator.add(repairResult);
2666
+ result = repairResult;
2667
+ providerSessionId =
2668
+ repairResult?.threadId
2669
+ || repairResult?.sessionId
2670
+ || providerSessionId;
2671
+ rawCompletion = normalizeOutcomeCompletion(
2672
+ extractJsonObject(
2673
+ repairResult?.text || repairResult?.resultText || '',
2674
+ ),
2675
+ run?.outcome?.outcomeId,
2676
+ );
2677
+ synchronization = harnessTools.directWorkspace
2678
+ ? await harnessTools.inspect()
2679
+ : await harnessTools.synchronize();
2680
+ await Promise.race([activityChain, cancellation]);
2681
+ }
2682
+ if (providerSessionId || options.providerAdapter) {
2683
+ rememberCompanionSession(sessionKey, {
2684
+ providerSessionId:
2685
+ providerSessionId
2686
+ || rememberedSession?.providerSessionId
2687
+ || sessionKey,
2688
+ agent: definition.id,
2689
+ updatedAt: Date.now(),
2690
+ });
2691
+ }
2692
+ const completion =
2693
+ typeof harnessTools.finalizeCompletion === 'function'
2694
+ ? harnessTools.finalizeCompletion(rawCompletion)
2695
+ : rawCompletion;
2494
2696
  const files = harnessTools.directWorkspace
2495
2697
  ? []
2496
2698
  : collectOutcomeWorkspaceChanges(run.outcome, materialized);
@@ -2524,7 +2726,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2524
2726
  outcomeId: run?.outcome?.outcomeId,
2525
2727
  model: selectedModelId,
2526
2728
  status:
2527
- normalized.status === 'ready_for_verification'
2729
+ normalized.status === 'completed'
2528
2730
  ? 'succeeded'
2529
2731
  : normalized.status,
2530
2732
  durationMs: Date.now() - callStartedAt,
@@ -2534,6 +2736,22 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2534
2736
  },
2535
2737
  ],
2536
2738
  });
2739
+ } catch (error) {
2740
+ const failure =
2741
+ error instanceof Error
2742
+ ? error
2743
+ : new Error(String(error || 'The Framer outcome failed.'));
2744
+ usageAccumulator.add(failure.companionUsage || {});
2745
+ const framer =
2746
+ typeof harnessTools.summary === 'function'
2747
+ ? harnessTools.summary()
2748
+ : undefined;
2749
+ failure.framer = framer;
2750
+ failure.companionUsage = {
2751
+ ...usageAccumulator.snapshot(),
2752
+ ...(framer ? { framer } : {}),
2753
+ };
2754
+ throw failure;
2537
2755
  } finally {
2538
2756
  finished = true;
2539
2757
  controlMonitorAbort.abort();
@@ -2563,7 +2781,6 @@ export async function executeRun(
2563
2781
  inspectAgentAuthentication,
2564
2782
  runtimeProfile: suppliedRuntimeProfile,
2565
2783
  framerAgentRunCli,
2566
- openAuthorizationUrl,
2567
2784
  allowInsecureHttp = false,
2568
2785
  } = {},
2569
2786
  ) {
@@ -2685,7 +2902,6 @@ export async function executeRun(
2685
2902
  apiBaseUrl,
2686
2903
  deviceToken,
2687
2904
  fetchImpl,
2688
- openAuthorizationUrl,
2689
2905
  allowInsecureHttp,
2690
2906
  };
2691
2907
  if (outcomeRun) {
@@ -2699,12 +2915,17 @@ export async function executeRun(
2699
2915
  return { cancelled: false };
2700
2916
  } catch (error) {
2701
2917
  if (error?.code === 'RUN_CANCELLED') {
2702
- trace.info('run_cancelled', { message: error.message });
2918
+ const message = cancellationMessage(error);
2919
+ trace.info('run_cancelled', {
2920
+ message,
2921
+ cancellationSource: error?.cancellationSource,
2922
+ });
2703
2923
  await send('error', {
2704
2924
  operationType: 'cancelled',
2705
2925
  outcome: 'cancelled',
2706
- message: error.message,
2926
+ message,
2707
2927
  code: 'RUN_CANCELLED',
2928
+ cancellationSource: error?.cancellationSource || 'unknown',
2708
2929
  ...(error?.companionUsage || usageAccumulator.snapshot()),
2709
2930
  }).catch(() => undefined);
2710
2931
  return { cancelled: true };
package/src/api.js CHANGED
@@ -200,6 +200,40 @@ export function getFramerProjectAuthorization(apiBaseUrl, {
200
200
  );
201
201
  }
202
202
 
203
+ export function reportFramerProjectAuthorizationVerification(apiBaseUrl, {
204
+ deviceToken,
205
+ runId,
206
+ status,
207
+ framerProjectId,
208
+ framerProjectUrl,
209
+ credentialVersion,
210
+ errorCode,
211
+ fetchImpl,
212
+ signal,
213
+ allowInsecureHttp,
214
+ }) {
215
+ return requestJson(
216
+ apiBaseUrl,
217
+ `/ai/framer/companion/runs/${encodeURIComponent(runId)}/framer-authorization/verification`,
218
+ {
219
+ method: 'POST',
220
+ deviceToken,
221
+ fetchImpl,
222
+ signal,
223
+ allowInsecureHttp,
224
+ body: {
225
+ status,
226
+ framerProjectId,
227
+ ...(framerProjectUrl ? { framerProjectUrl } : {}),
228
+ credentialVersion,
229
+ ...(errorCode ? { errorCode } : {}),
230
+ platform: process.platform,
231
+ companionVersion: BRIDGE_VERSION,
232
+ },
233
+ },
234
+ );
235
+ }
236
+
203
237
  export function postRunEvent(apiBaseUrl, {
204
238
  deviceToken,
205
239
  runId,