@hamp10/agentforge 0.2.28 → 0.2.30
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 +1 -1
- package/scripts/check-task-semantics.js +15 -0
- package/src/OpenClawCLI.js +207 -18
- package/src/default-task-guides.js +2 -1
- package/src/worker.js +102 -6
package/package.json
CHANGED
|
@@ -949,6 +949,21 @@ assert.match(
|
|
|
949
949
|
/taskId,\n\s*iteration,/i,
|
|
950
950
|
'direct fast path should receive taskId so UI peer context is task-scoped'
|
|
951
951
|
);
|
|
952
|
+
assert.match(
|
|
953
|
+
openClawSource,
|
|
954
|
+
/const semanticTask = String\(directOptions\.rawUserTask \|\| task \|\| ''\);[\s\S]*_guardDirectFileWritePath\(fileWriteTarget, workDir, \{ task: semanticTask \}\)/i,
|
|
955
|
+
'direct scope guards should use the raw user task, not expanded retry/platform context'
|
|
956
|
+
);
|
|
957
|
+
assert.match(
|
|
958
|
+
openClawSource,
|
|
959
|
+
/rawUserTask: rawUserTask \|\| task/i,
|
|
960
|
+
'direct fast path should pass the raw task separately from the expanded runtime prompt'
|
|
961
|
+
);
|
|
962
|
+
assert.match(
|
|
963
|
+
workerSource,
|
|
964
|
+
/imageGenerationModel \|\| null,\n\s*scopeAwareUserMessage/i,
|
|
965
|
+
'worker should pass the resolved raw user scope into direct agent execution'
|
|
966
|
+
);
|
|
952
967
|
assert.match(
|
|
953
968
|
openClawSource,
|
|
954
969
|
/persistDirectUiContext\(\);/i,
|
package/src/OpenClawCLI.js
CHANGED
|
@@ -625,6 +625,29 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
625
625
|
return true;
|
|
626
626
|
}
|
|
627
627
|
|
|
628
|
+
_isDirectScopedReferenceSourcePath(filePath, workDir, relativePath, slugs, pageOnly, task) {
|
|
629
|
+
if (!pageOnly || !this._allowsDirectScopedPageSourcesToRemainDeleted(task)) return false;
|
|
630
|
+
if (!Array.isArray(slugs) || slugs.length === 0) return false;
|
|
631
|
+
if (!this._isDirectPageSourcePath(relativePath)) return false;
|
|
632
|
+
|
|
633
|
+
const relative = path.relative(workDir, filePath);
|
|
634
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return false;
|
|
635
|
+
|
|
636
|
+
const contents = [];
|
|
637
|
+
try {
|
|
638
|
+
if (existsSync(filePath)) contents.push(readFileSync(filePath, 'utf-8'));
|
|
639
|
+
} catch {}
|
|
640
|
+
try {
|
|
641
|
+
contents.push(String(execFileSync('git', ['-C', workDir, 'show', `HEAD:${relativePath}`], {
|
|
642
|
+
encoding: 'utf-8',
|
|
643
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
644
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
645
|
+
}) || ''));
|
|
646
|
+
} catch {}
|
|
647
|
+
|
|
648
|
+
return contents.some(content => this._directScopeSlugsMatchingText(String(content || '').toLowerCase(), slugs).length > 0);
|
|
649
|
+
}
|
|
650
|
+
|
|
628
651
|
_directTaskScopeAllowsPath(filePath, workDir, task) {
|
|
629
652
|
const { slugs, pageOnly } = this._extractDirectExplicitScope(task);
|
|
630
653
|
if (slugs.length === 0) return true;
|
|
@@ -635,6 +658,7 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
635
658
|
const lower = String(scopePath || '').toLowerCase();
|
|
636
659
|
const slugAllowed = this._directScopeSlugsMatchingText(lower, slugs).length > 0;
|
|
637
660
|
if (!slugAllowed) {
|
|
661
|
+
if (this._isDirectScopedReferenceSourcePath(filePath, workDir, scopePath, slugs, pageOnly, task)) return true;
|
|
638
662
|
return this._isDirectNewPageOwnedAssetPath(filePath, workDir, scopePath, pageOnly);
|
|
639
663
|
}
|
|
640
664
|
return !pageOnly || this._isDirectPageSourcePath(scopePath);
|
|
@@ -875,9 +899,21 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
875
899
|
}
|
|
876
900
|
|
|
877
901
|
_isDirectBroadUiQualityTask(task) {
|
|
902
|
+
if (this._allowsDirectScopedPageSourcesToRemainDeleted(task)) return false;
|
|
878
903
|
return /\b(ui|ux|visual|design|frontend|css|html|pages?|dashboard|website|web app|app screen|layout|component|responsive|professional|polish(?:ed)?|vibecoded|vibe-coded|interface|screen|arena|competitive|competition|scoreboard|ranking|visual hierarchy|user experience|design system|readability|readable|legibility|contrast|accessibility)\b/i.test(String(task || ''));
|
|
879
904
|
}
|
|
880
905
|
|
|
906
|
+
_allowsDirectScopedPageSourcesToRemainDeleted(task) {
|
|
907
|
+
const text = String(task || '');
|
|
908
|
+
const asksForDeletion = /\b(delete|remove|drop|unpublish|decommission|take down|take offline)\b/i.test(text);
|
|
909
|
+
if (!asksForDeletion) return false;
|
|
910
|
+
|
|
911
|
+
const rebuildNearDeletion =
|
|
912
|
+
/\b(delete|remove|drop|unpublish|decommission|take down|take offline)\b[^.!?\n]{0,180}\b(rebuild|recreate|remake|rewrite|replace|restore|redesign|build|create|implement|start over|from scratch|clean start|fresh start|same urls?|same paths?|preserv(?:e|ing) the same urls?)\b/i.test(text) ||
|
|
913
|
+
/\b(rebuild|recreate|remake|rewrite|replace|restore|redesign|build|create|implement|start over|from scratch|clean start|fresh start|same urls?|same paths?|preserv(?:e|ing) the same urls?)\b[^.!?\n]{0,180}\b(delete|remove|drop|unpublish|decommission|take down|take offline)\b/i.test(text);
|
|
914
|
+
return !rebuildNearDeletion;
|
|
915
|
+
}
|
|
916
|
+
|
|
881
917
|
_gitHeadContentForPath(filePath) {
|
|
882
918
|
try {
|
|
883
919
|
const rawTargetPath = String(filePath || '');
|
|
@@ -1346,6 +1382,137 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
1346
1382
|
return '';
|
|
1347
1383
|
}
|
|
1348
1384
|
|
|
1385
|
+
_directTaskForbidsGitDelivery(task) {
|
|
1386
|
+
return /\b(do not|don't|dont|without|skip|avoid)\b[^.!?\n]{0,80}\b(commit|push|publish|deploy|release)\b/i.test(String(task || '')) ||
|
|
1387
|
+
/\b(local only|leave uncommitted|do not ship|don't ship|dont ship)\b/i.test(String(task || ''));
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
_directGitHasRemote(workDir) {
|
|
1391
|
+
try {
|
|
1392
|
+
execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
1393
|
+
cwd: workDir,
|
|
1394
|
+
encoding: 'utf-8',
|
|
1395
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1396
|
+
});
|
|
1397
|
+
const remote = String(execFileSync('git', ['remote'], {
|
|
1398
|
+
cwd: workDir,
|
|
1399
|
+
encoding: 'utf-8',
|
|
1400
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1401
|
+
}) || '').trim();
|
|
1402
|
+
return remote.length > 0;
|
|
1403
|
+
} catch {
|
|
1404
|
+
return false;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
_directGitPendingDeliveryMessage(workDir, task, { directMutationCount = 0, taskRequiresFileChange = false } = {}) {
|
|
1409
|
+
if (this._directTaskForbidsGitDelivery(task)) return '';
|
|
1410
|
+
if (!directMutationCount && !taskRequiresFileChange) return '';
|
|
1411
|
+
if (!this._directGitHasRemote(workDir)) return '';
|
|
1412
|
+
|
|
1413
|
+
const status = this._directGitStatusPorcelain(workDir).trim();
|
|
1414
|
+
if (!status) return '';
|
|
1415
|
+
|
|
1416
|
+
return [
|
|
1417
|
+
'The repo still has uncommitted changes after the task work.',
|
|
1418
|
+
'This is not complete delivery for a git-backed project with a remote.',
|
|
1419
|
+
'Review git status, stage only the intended task changes, commit them with a clear message, push to the configured upstream, then verify the pushed/live state.',
|
|
1420
|
+
`Current git status:\n${status}`,
|
|
1421
|
+
].join('\n');
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
_directGitStatusPaths(statusText) {
|
|
1425
|
+
const paths = [];
|
|
1426
|
+
for (const rawLine of String(statusText || '').split('\n')) {
|
|
1427
|
+
if (!rawLine.trim()) continue;
|
|
1428
|
+
let rawPath = rawLine.length > 3 ? rawLine.slice(3).trim() : '';
|
|
1429
|
+
if (!rawPath) continue;
|
|
1430
|
+
if (rawPath.includes(' -> ')) rawPath = rawPath.split(' -> ').pop().trim();
|
|
1431
|
+
rawPath = rawPath.replace(/^"|"$/g, '');
|
|
1432
|
+
if (rawPath && !paths.includes(rawPath)) paths.push(rawPath);
|
|
1433
|
+
}
|
|
1434
|
+
return paths;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
_directGitAutoDeliveryEligible(workDir, task, statusText) {
|
|
1438
|
+
const { slugs } = this._extractDirectExplicitScope(task);
|
|
1439
|
+
if (slugs.length === 0) return { ok: false, reason: 'No explicit task scope was detected for automatic delivery.' };
|
|
1440
|
+
const paths = this._directGitStatusPaths(statusText);
|
|
1441
|
+
if (paths.length === 0) return { ok: false, reason: 'No git status paths to deliver.' };
|
|
1442
|
+
const outOfScope = paths.filter(rel => !this._directTaskScopeAllowsPath(path.resolve(workDir, rel), workDir, task));
|
|
1443
|
+
if (outOfScope.length > 0) {
|
|
1444
|
+
return { ok: false, reason: `Pending git changes include paths outside the resolved task scope: ${outOfScope.join(', ')}` };
|
|
1445
|
+
}
|
|
1446
|
+
return { ok: true, paths };
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
_directCommitMessageForTask(task) {
|
|
1450
|
+
const cleaned = String(task || '')
|
|
1451
|
+
.replace(/\s+/g, ' ')
|
|
1452
|
+
.replace(/[.?!]+$/g, '')
|
|
1453
|
+
.trim();
|
|
1454
|
+
if (!cleaned) return 'Update project files';
|
|
1455
|
+
const message = cleaned.length > 86 ? cleaned.slice(0, 83).trimEnd() + '...' : cleaned;
|
|
1456
|
+
return message.charAt(0).toUpperCase() + message.slice(1);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
_directMaybeAutoDeliverGitChanges(workDir, task, { directMutationCount = 0, taskRequiresFileChange = false } = {}) {
|
|
1460
|
+
const pending = this._directGitPendingDeliveryMessage(workDir, task, { directMutationCount, taskRequiresFileChange });
|
|
1461
|
+
if (!pending) return { delivered: false, pending: '' };
|
|
1462
|
+
|
|
1463
|
+
const status = this._directGitStatusPorcelain(workDir);
|
|
1464
|
+
if (!status.trim()) return { delivered: false, pending: '' };
|
|
1465
|
+
const eligibility = this._directGitAutoDeliveryEligible(workDir, task, status);
|
|
1466
|
+
if (!eligibility.ok) return { delivered: false, pending, reason: eligibility.reason };
|
|
1467
|
+
|
|
1468
|
+
const message = this._directCommitMessageForTask(task);
|
|
1469
|
+
try {
|
|
1470
|
+
execFileSync('git', ['add', '--', ...eligibility.paths], {
|
|
1471
|
+
cwd: workDir,
|
|
1472
|
+
encoding: 'utf-8',
|
|
1473
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1474
|
+
});
|
|
1475
|
+
const stagedDiff = execFileSync('git', ['diff', '--cached', '--stat'], {
|
|
1476
|
+
cwd: workDir,
|
|
1477
|
+
encoding: 'utf-8',
|
|
1478
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1479
|
+
}).trim();
|
|
1480
|
+
if (!stagedDiff) return { delivered: false, pending: 'No staged changes remained after staging intended task paths.' };
|
|
1481
|
+
|
|
1482
|
+
execFileSync('git', ['commit', '-m', message], {
|
|
1483
|
+
cwd: workDir,
|
|
1484
|
+
encoding: 'utf-8',
|
|
1485
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1486
|
+
});
|
|
1487
|
+
execFileSync('git', ['push'], {
|
|
1488
|
+
cwd: workDir,
|
|
1489
|
+
encoding: 'utf-8',
|
|
1490
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
const head = String(execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
|
1494
|
+
cwd: workDir,
|
|
1495
|
+
encoding: 'utf-8',
|
|
1496
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1497
|
+
}) || '').trim();
|
|
1498
|
+
const remaining = this._directGitStatusPorcelain(workDir).trim();
|
|
1499
|
+
return {
|
|
1500
|
+
delivered: true,
|
|
1501
|
+
message: [
|
|
1502
|
+
`Committed and pushed intended task changes${head ? ` at ${head}` : ''}.`,
|
|
1503
|
+
stagedDiff ? `Delivered diff:\n${stagedDiff}` : null,
|
|
1504
|
+
remaining ? `Remaining git status:\n${remaining}` : 'Working tree is clean after push.',
|
|
1505
|
+
].filter(Boolean).join('\n'),
|
|
1506
|
+
};
|
|
1507
|
+
} catch (err) {
|
|
1508
|
+
return {
|
|
1509
|
+
delivered: false,
|
|
1510
|
+
pending,
|
|
1511
|
+
reason: `Automatic git delivery failed: ${err.stderr?.toString?.().trim() || err.message || err}`,
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1349
1516
|
/**
|
|
1350
1517
|
* Resolve the actual JSONL file path for a task session.
|
|
1351
1518
|
* The embedded runner uses embeddedSessionId = "task-{taskId}", so the file is
|
|
@@ -4183,9 +4350,12 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
4183
4350
|
'You are a helpful AI assistant running inside AgentForge. For implementation tasks, inspect the local project, make concrete file changes, test them, and then summarize. Use tools when the task requires executing commands, reading/writing files, or checking the browser. Do not stop with a plan when code changes are required.',
|
|
4184
4351
|
'The AgentForge dashboard is the orchestration control surface. Do not inspect or use it as product context unless the user task is explicitly about AgentForge itself. For user-project work, inspect the target project files and the actual app/site URL or local preview page.',
|
|
4185
4352
|
'For local browser verification, use the URL or port proven by project code, server output, or an already successful browser result. Do not try arbitrary localhost ports such as 3000 or 5000 just because they are common defaults.',
|
|
4353
|
+
'For git-backed projects with a remote, do not finish with intended changes only on disk. Stage the final intended changes, commit them, push to the configured upstream, and verify the resulting state unless the user explicitly asked not to. Do not create confirmation/status files just to prove the task is done.',
|
|
4186
4354
|
].join('\n');
|
|
4187
|
-
const
|
|
4188
|
-
const
|
|
4355
|
+
const semanticTask = String(directOptions.rawUserTask || task || '');
|
|
4356
|
+
const taskRequiresFileChange = /\b(make|create|build|add|implement|update|edit|change|fix|improve|redesign|refactor|write|generate|scaffold|wire|ship|work on|delete|remove|drop|unpublish|decommission|take down|take offline)\b/i.test(semanticTask);
|
|
4357
|
+
const taskAllowsDeletedScopedPages = this._allowsDirectScopedPageSourcesToRemainDeleted(semanticTask);
|
|
4358
|
+
const taskRequiresVisualVerification = !taskAllowsDeletedScopedPages && /\b(ui|ux|visual|design|frontend|css|html|pages?|dashboard|website|web app|app screen|layout|component|responsive|professional|polish(?:ed)?|vibecoded|vibe-coded|interface|screen|arena|competitive|competition|scoreboard|ranking|visual hierarchy|user experience|design system|readability|readable|legibility|contrast|accessibility)\b/i.test(semanticTask);
|
|
4189
4359
|
const directRequestTimeoutMs = Math.max(
|
|
4190
4360
|
30_000,
|
|
4191
4361
|
Number(process.env.AGENTFORGE_DIRECT_LLM_REQUEST_TIMEOUT_MS || (taskRequiresVisualVerification ? 120_000 : 90_000))
|
|
@@ -4201,7 +4371,7 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
4201
4371
|
let directMutationCount = 0;
|
|
4202
4372
|
let lastCleanLocalVerificationMutationCount = 0;
|
|
4203
4373
|
let lastDirectVisualWarning = '';
|
|
4204
|
-
const explicitScopeForTask = this._extractDirectExplicitScope(
|
|
4374
|
+
const explicitScopeForTask = this._extractDirectExplicitScope(semanticTask);
|
|
4205
4375
|
const explicitScopeSlugsForTask = explicitScopeForTask.slugs || [];
|
|
4206
4376
|
const taskRequiresComparableUiContext =
|
|
4207
4377
|
taskRequiresVisualVerification &&
|
|
@@ -4646,7 +4816,7 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
4646
4816
|
});
|
|
4647
4817
|
const result = await this._runDirectTool(agentId, 'browser', { action: 'open', url }, workDir, {
|
|
4648
4818
|
signal: directAbortController.signal,
|
|
4649
|
-
task,
|
|
4819
|
+
task: semanticTask,
|
|
4650
4820
|
imageModel: directOptions.imageModel,
|
|
4651
4821
|
agentModel: directOptions.agentModel || model,
|
|
4652
4822
|
fallbackVisionModels: directOptions.fallbackVisionModels,
|
|
@@ -4687,7 +4857,7 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
4687
4857
|
const match = text.match(/^URL:\s*(.+)$/mi);
|
|
4688
4858
|
const urlText = match?.[1]?.trim() || '';
|
|
4689
4859
|
if (!urlText) return false;
|
|
4690
|
-
const { slugs } = this._extractDirectExplicitScope(
|
|
4860
|
+
const { slugs } = this._extractDirectExplicitScope(semanticTask);
|
|
4691
4861
|
if (slugs.length > 0 && scopeSlugsMatchingText(urlText, slugs).length === 0) return false;
|
|
4692
4862
|
return /^(?:https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?(?:\/|$)|file:)/i.test(urlText);
|
|
4693
4863
|
};
|
|
@@ -4699,12 +4869,13 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
4699
4869
|
return warnings.join('\n').slice(0, 1200);
|
|
4700
4870
|
};
|
|
4701
4871
|
const directStopResponse = (reason, forceIncomplete = false) => {
|
|
4702
|
-
const
|
|
4872
|
+
const pendingGitDelivery = this._directGitPendingDeliveryMessage(workDir, semanticTask, { directMutationCount, taskRequiresFileChange });
|
|
4873
|
+
const noDeliverable = taskRequiresFileChange && directMutationCount === 0 && !pendingGitDelivery;
|
|
4703
4874
|
const missingScopedMutation = missingScopedMutationSlugs();
|
|
4704
4875
|
const missingVisualVerification = taskRequiresVisualVerification && directMutationCount > 0 && !hasCleanLocalVerificationForLatestMutation();
|
|
4705
4876
|
const visualQualityBlocked = taskRequiresVisualVerification && directMutationCount > 0 && !!lastDirectVisualWarning;
|
|
4706
|
-
const stoppedBeforeSummary = forceIncomplete && !noDeliverable && missingScopedMutation.length === 0 && !missingVisualVerification && !visualQualityBlocked;
|
|
4707
|
-
const incomplete = forceIncomplete || noDeliverable || missingScopedMutation.length > 0 || missingVisualVerification || visualQualityBlocked;
|
|
4877
|
+
const stoppedBeforeSummary = forceIncomplete && !noDeliverable && missingScopedMutation.length === 0 && !missingVisualVerification && !visualQualityBlocked && !pendingGitDelivery;
|
|
4878
|
+
const incomplete = forceIncomplete || noDeliverable || missingScopedMutation.length > 0 || missingVisualVerification || visualQualityBlocked || !!pendingGitDelivery;
|
|
4708
4879
|
return {
|
|
4709
4880
|
text: [
|
|
4710
4881
|
noDeliverable
|
|
@@ -4715,6 +4886,8 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
4715
4886
|
? `Native AgentForge tools changed files, but the latest visual verification still reported visible UI problems. ${lastDirectVisualWarning} ${reason} This is not complete for a UI task. Fix the visible issues, reload the changed screen, and verify again.`
|
|
4716
4887
|
: missingVisualVerification
|
|
4717
4888
|
? `Native AgentForge tools changed files, but the changed UI was not successfully loaded and inspected after the edits. ${missingScopedVerificationText()} ${reason} This is not complete for a UI task. Start or repair the local app server, open the real app URL, inspect the changed screen, fix visible issues, then finish. After a clean visual pass, do not make another file edit before delivery; if you edit again, verify again.`
|
|
4889
|
+
: pendingGitDelivery
|
|
4890
|
+
? `Native AgentForge tools changed files, but the repo has not been delivered yet. ${reason}\n${pendingGitDelivery}`
|
|
4718
4891
|
: stoppedBeforeSummary
|
|
4719
4892
|
? `Native AgentForge tools completed ${directToolCount} action(s), but the model stopped before producing a final summary. ${reason} This is not complete. Continue from the current files and browser state, verify the result, and finish with a clear final summary.`
|
|
4720
4893
|
: `Native AgentForge tools completed ${directToolCount} action(s), including any edits or browser checks listed below. ${reason}`,
|
|
@@ -4783,6 +4956,20 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
4783
4956
|
usedTools: directToolCount > 0,
|
|
4784
4957
|
};
|
|
4785
4958
|
}
|
|
4959
|
+
const delivery = this._directMaybeAutoDeliverGitChanges(workDir, semanticTask, { directMutationCount, taskRequiresFileChange });
|
|
4960
|
+
if (delivery.delivered) {
|
|
4961
|
+
recordDirectToolSummary('git', delivery.message);
|
|
4962
|
+
} else if (delivery.pending) {
|
|
4963
|
+
return {
|
|
4964
|
+
text: [
|
|
4965
|
+
summary || fallbackSummary,
|
|
4966
|
+
delivery.pending,
|
|
4967
|
+
delivery.reason ? `Delivery blocker: ${delivery.reason}` : null,
|
|
4968
|
+
].filter(Boolean).join('\n'),
|
|
4969
|
+
hasToolCalls: false,
|
|
4970
|
+
usedTools: directToolCount > 0,
|
|
4971
|
+
};
|
|
4972
|
+
}
|
|
4786
4973
|
return {
|
|
4787
4974
|
text: [
|
|
4788
4975
|
summary || fallbackSummary,
|
|
@@ -4804,7 +4991,7 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
4804
4991
|
return `I am checking the live browser${url} to judge the actual product surface before deciding the next change.`;
|
|
4805
4992
|
}
|
|
4806
4993
|
if (name === 'read_file' || name === 'read') {
|
|
4807
|
-
const scope = extractExplicitScope(
|
|
4994
|
+
const scope = extractExplicitScope(semanticTask);
|
|
4808
4995
|
const targetText = String(targetPath || base || '').toLowerCase();
|
|
4809
4996
|
const looksLikePageSource = /\.(html?|css|s[ac]ss|jsx?|tsx?|vue|svelte|astro|mdx?)$/i.test(targetText);
|
|
4810
4997
|
const isNamedTarget = scope.slugs.length > 0 && scopeSlugsMatchingText(targetText, scope.slugs).length > 0;
|
|
@@ -4887,7 +5074,8 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
4887
5074
|
const text = parts.filter(p => p.text).map(p => p.text).join('');
|
|
4888
5075
|
if (!functionCalls.length) {
|
|
4889
5076
|
if (step > 0) {
|
|
4890
|
-
|
|
5077
|
+
const pendingGitDelivery = this._directGitPendingDeliveryMessage(workDir, semanticTask, { directMutationCount, taskRequiresFileChange });
|
|
5078
|
+
if (taskRequiresFileChange && directMutationCount === 0 && !pendingGitDelivery && directInternalNudgeCount < 2) {
|
|
4891
5079
|
directInternalNudgeCount += 1;
|
|
4892
5080
|
contents.push(content);
|
|
4893
5081
|
contents.push({
|
|
@@ -4902,7 +5090,7 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
4902
5090
|
});
|
|
4903
5091
|
continue;
|
|
4904
5092
|
}
|
|
4905
|
-
if (taskRequiresFileChange && directMutationCount === 0) {
|
|
5093
|
+
if (taskRequiresFileChange && directMutationCount === 0 && !pendingGitDelivery) {
|
|
4906
5094
|
const inspectedText = stripCompletionMarker(text);
|
|
4907
5095
|
return {
|
|
4908
5096
|
text: [
|
|
@@ -5028,7 +5216,7 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
5028
5216
|
const fileWriteTarget = directFileWriteTargetPath(name, args);
|
|
5029
5217
|
if (fileWriteTarget) {
|
|
5030
5218
|
try {
|
|
5031
|
-
this._guardDirectFileWritePath(fileWriteTarget, workDir, { task });
|
|
5219
|
+
this._guardDirectFileWritePath(fileWriteTarget, workDir, { task: semanticTask });
|
|
5032
5220
|
} catch (err) {
|
|
5033
5221
|
if (err.code === 'AGENTFORGE_SCOPE_VIOLATION') {
|
|
5034
5222
|
directScopeViolationCount += 1;
|
|
@@ -5083,7 +5271,7 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
5083
5271
|
responses.push({ functionResponse: { name, response: { error: message } } });
|
|
5084
5272
|
continue;
|
|
5085
5273
|
}
|
|
5086
|
-
const dirtyDeliveryMessage = this._directGitDirtyDeliveryMessage(workDir,
|
|
5274
|
+
const dirtyDeliveryMessage = this._directGitDirtyDeliveryMessage(workDir, semanticTask, command);
|
|
5087
5275
|
if (/\bgit\b[\s\S]*\b(?:commit|push)\b/i.test(command) && dirtyDeliveryMessage) {
|
|
5088
5276
|
emitDirectThought('AgentForge blocked commit/push because the staged delivery does not match the final working tree.');
|
|
5089
5277
|
this.emit('tool_activity', {
|
|
@@ -5108,7 +5296,7 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
5108
5296
|
try {
|
|
5109
5297
|
const result = await this._runDirectTool(agentId, name, args, workDir, {
|
|
5110
5298
|
signal: directAbortController.signal,
|
|
5111
|
-
task,
|
|
5299
|
+
task: semanticTask,
|
|
5112
5300
|
imageModel: directOptions.imageModel,
|
|
5113
5301
|
agentModel: directOptions.agentModel || model,
|
|
5114
5302
|
fallbackVisionModels: directOptions.fallbackVisionModels,
|
|
@@ -5514,7 +5702,7 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
5514
5702
|
* Run an agent task
|
|
5515
5703
|
* Images are saved to workspace and referenced in message for vision model analysis
|
|
5516
5704
|
*/
|
|
5517
|
-
async runAgentTask(agentId, task, workDir, sessionId = null, image = null, browserProfile = null, imageWorkDir = null, agentModel = null, customSystemPrompt = null, conversationHistory = null, allImages = null, providerKeys = null, imageModel = null, taskId = null, iteration = 1, fallbackVisionModels = null, imageGenerationModel = null) {
|
|
5705
|
+
async runAgentTask(agentId, task, workDir, sessionId = null, image = null, browserProfile = null, imageWorkDir = null, agentModel = null, customSystemPrompt = null, conversationHistory = null, allImages = null, providerKeys = null, imageModel = null, taskId = null, iteration = 1, fallbackVisionModels = null, imageGenerationModel = null, rawUserTask = null) {
|
|
5518
5706
|
// Gateway auth comes from per-agent auth-profiles.json, not subprocess env.
|
|
5519
5707
|
// Seed real Anthropic API keys up front so anthropic/* models do not fall back
|
|
5520
5708
|
// to Claude account/OAuth tokens and fail with claude.ai "extra usage" limits.
|
|
@@ -5653,10 +5841,10 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
5653
5841
|
// OpenClaw subprocess after a model compatibility fallback.
|
|
5654
5842
|
// If LLM answers with plain text → done in ~1-3s.
|
|
5655
5843
|
// If LLM returns tool calls → fall through to gateway/subprocess as usual.
|
|
5656
|
-
const
|
|
5657
|
-
const directTaskRequiresFileChange = /\b(make|create|build|add|implement|update|edit|change|fix|improve|redesign|refactor|write|generate|scaffold|wire|ship|work on)\b/i.test(
|
|
5844
|
+
const directSemanticTaskText = String(rawUserTask || task || taskForRuntime || '');
|
|
5845
|
+
const directTaskRequiresFileChange = /\b(make|create|build|add|implement|update|edit|change|fix|improve|redesign|refactor|write|generate|scaffold|wire|ship|work on)\b/i.test(directSemanticTaskText);
|
|
5658
5846
|
const directTaskRequiresVisualImplementation = directTaskRequiresFileChange &&
|
|
5659
|
-
/\b(ui|ux|visual|design|frontend|css|html|pages?|dashboard|website|web app|app screen|layout|component|responsive|professional|polish(?:ed)?|vibecoded|vibe-coded|interface|screen|visual hierarchy|user experience|design system)\b/i.test(
|
|
5847
|
+
/\b(ui|ux|visual|design|frontend|css|html|pages?|dashboard|website|web app|app screen|layout|component|responsive|professional|polish(?:ed)?|vibecoded|vibe-coded|interface|screen|visual hierarchy|user experience|design system)\b/i.test(directSemanticTaskText);
|
|
5660
5848
|
const forceSubprocessForVisual = /^(1|true|yes)$/i.test(process.env.AGENTFORGE_FORCE_OPENCLAW_VISUAL || '');
|
|
5661
5849
|
const skipDirectFastPath = directTaskRequiresVisualImplementation && forceSubprocessForVisual;
|
|
5662
5850
|
if (skipDirectFastPath && agentModel) {
|
|
@@ -5671,6 +5859,7 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
5671
5859
|
fallbackVisionModels,
|
|
5672
5860
|
taskId,
|
|
5673
5861
|
iteration,
|
|
5862
|
+
rawUserTask: rawUserTask || task,
|
|
5674
5863
|
});
|
|
5675
5864
|
if (fastResult !== null) {
|
|
5676
5865
|
if (fastResult.hasToolCalls) {
|
|
@@ -73,7 +73,8 @@ export const DEFAULT_TASK_GUIDES = Object.freeze({
|
|
|
73
73
|
- Run the relevant checks or explain exactly why they could not run.
|
|
74
74
|
- Do not create throwaway proof files such as verification notes, screenshots, or status artifacts inside the user's repo unless the user asked for them.
|
|
75
75
|
- Do not create or switch git branches unless the user asked for a branch or pull request. If the user asks to commit and push, deliver on the current branch/upstream.
|
|
76
|
-
- If
|
|
76
|
+
- If you intentionally change files in a git repo that has a configured remote/upstream, delivery includes staging the final verified changes, committing them with a clear message, pushing them, and verifying the target state after the push. Do not report completion while intended changes are only local unless the user explicitly asked not to push, the repo has no remote/upstream, authentication blocks the push, or verification/checks failed. Report those as blockers with exact evidence.
|
|
77
|
+
- If the user asks to commit, push, publish, deploy, release, or update a live site, that delivery step is explicitly part of the task. Do not report completion until you have run the matching git/deploy/publish command, verified the target state, and reported exact evidence.
|
|
77
78
|
- Report changed files and remaining blockers only after implementation is complete.`,
|
|
78
79
|
}),
|
|
79
80
|
});
|
package/src/worker.js
CHANGED
|
@@ -506,8 +506,10 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
506
506
|
const asksForDeletion = /\b(delete|remove|drop|unpublish|decommission|take down|take offline)\b/i.test(text);
|
|
507
507
|
if (!asksForDeletion) return false;
|
|
508
508
|
|
|
509
|
-
const
|
|
510
|
-
|
|
509
|
+
const rebuildNearDeletion =
|
|
510
|
+
/\b(delete|remove|drop|unpublish|decommission|take down|take offline)\b[^.!?\n]{0,180}\b(rebuild|recreate|remake|rewrite|replace|restore|redesign|build|create|implement|start over|from scratch|clean start|fresh start|same urls?|same paths?|preserv(?:e|ing) the same urls?)\b/i.test(text) ||
|
|
511
|
+
/\b(rebuild|recreate|remake|rewrite|replace|restore|redesign|build|create|implement|start over|from scratch|clean start|fresh start|same urls?|same paths?|preserv(?:e|ing) the same urls?)\b[^.!?\n]{0,180}\b(delete|remove|drop|unpublish|decommission|take down|take offline)\b/i.test(text);
|
|
512
|
+
return !rebuildNearDeletion;
|
|
511
513
|
}
|
|
512
514
|
|
|
513
515
|
_parseNumstat(output, source) {
|
|
@@ -701,10 +703,28 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
701
703
|
return scopedSources.length > 0 && outOfScopeMentions.length === 0;
|
|
702
704
|
}
|
|
703
705
|
|
|
704
|
-
|
|
706
|
+
_isScopedReferenceSourcePath(baseline, rel, allowedSlugs, pageOnly, userMessage) {
|
|
707
|
+
if (!baseline?.root || !pageOnly || !this._allowsScopedPageSourcesToRemainDeleted(userMessage)) return false;
|
|
708
|
+
if (!Array.isArray(allowedSlugs) || allowedSlugs.length === 0) return false;
|
|
709
|
+
if (!this._isPageSourcePath(rel)) return false;
|
|
710
|
+
|
|
711
|
+
let content = '';
|
|
712
|
+
try {
|
|
713
|
+
content = this._gitPathExistsAtRef(baseline.root, baseline.head || 'HEAD', rel)
|
|
714
|
+
? this._gitOutput(baseline.root, ['show', `${baseline.head || 'HEAD'}:${rel}`], 1000000)
|
|
715
|
+
: readFileSync(path.join(baseline.root, rel), 'utf-8');
|
|
716
|
+
} catch {
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return this._scopeSlugsMatchingText(String(content || '').toLowerCase(), allowedSlugs).length > 0;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
_scopeAllowsChangedPath(baseline, rel, allowedSlugs, pageOnly, userMessage = '') {
|
|
705
724
|
const lower = String(rel || '').toLowerCase();
|
|
706
725
|
const slugAllowed = this._scopeSlugsMatchingText(lower, allowedSlugs).length > 0;
|
|
707
726
|
if (slugAllowed) return !pageOnly || this._isPageSourcePath(rel);
|
|
727
|
+
if (this._isScopedReferenceSourcePath(baseline, rel, allowedSlugs, pageOnly, userMessage)) return true;
|
|
708
728
|
return this._isNewScopedPageOwnedAsset(baseline, rel, allowedSlugs, pageOnly);
|
|
709
729
|
}
|
|
710
730
|
|
|
@@ -734,7 +754,7 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
734
754
|
const initialDirty = new Set(baseline.initialDirtyPaths || []);
|
|
735
755
|
const outOfScope = [...names]
|
|
736
756
|
.filter(rel => !initialDirty.has(rel))
|
|
737
|
-
.filter(rel => !this._scopeAllowsChangedPath(baseline, rel, allowedSlugs, pageOnly))
|
|
757
|
+
.filter(rel => !this._scopeAllowsChangedPath(baseline, rel, allowedSlugs, pageOnly, userMessage))
|
|
738
758
|
.sort();
|
|
739
759
|
if (outOfScope.length > 0) {
|
|
740
760
|
warnings.push({ repo: baseline.root, head: baseline.head, allowedSlugs, pageOnly, files: outOfScope });
|
|
@@ -800,7 +820,7 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
800
820
|
const initialDirty = new Set(baseline.initialDirtyPaths || []);
|
|
801
821
|
const files = [...names]
|
|
802
822
|
.filter(rel => resetPreexistingScopedTargets || !initialDirty.has(rel))
|
|
803
|
-
.filter(rel => this._scopeAllowsChangedPath(baseline, rel, allowedSlugs, pageOnly))
|
|
823
|
+
.filter(rel => this._scopeAllowsChangedPath(baseline, rel, allowedSlugs, pageOnly, userMessage))
|
|
804
824
|
.sort();
|
|
805
825
|
if (files.length === 0) continue;
|
|
806
826
|
|
|
@@ -966,6 +986,40 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
966
986
|
return warnings.length > 0 ? this._formatDeliveryStateNudge(warnings) : '';
|
|
967
987
|
}
|
|
968
988
|
|
|
989
|
+
_repoBaselinesHaveDeliveredScopedChanges(repoBaselines, userMessage) {
|
|
990
|
+
if (!Array.isArray(repoBaselines) || repoBaselines.length === 0) return false;
|
|
991
|
+
const { slugs: allowedSlugs, pageOnly } = this._extractExplicitScope(userMessage);
|
|
992
|
+
|
|
993
|
+
for (const baseline of repoBaselines) {
|
|
994
|
+
if (!baseline?.root || !baseline.head) continue;
|
|
995
|
+
if (this._gitStatusPorcelain(baseline.root, 10000).trim()) continue;
|
|
996
|
+
|
|
997
|
+
const head = this._gitOutput(baseline.root, ['rev-parse', 'HEAD']);
|
|
998
|
+
if (!head || head === baseline.head) continue;
|
|
999
|
+
|
|
1000
|
+
const currentBranch = this._gitOutput(baseline.root, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
1001
|
+
if (baseline.branch && currentBranch && currentBranch !== baseline.branch && !this._allowsBranchDelivery(userMessage)) continue;
|
|
1002
|
+
|
|
1003
|
+
const upstream = this._gitOutput(baseline.root, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
|
|
1004
|
+
if (!upstream) continue;
|
|
1005
|
+
const unpushed = Number.parseInt(this._gitOutput(baseline.root, ['rev-list', '--count', `${upstream}..HEAD`]) || '0', 10);
|
|
1006
|
+
if (!Number.isFinite(unpushed) || unpushed > 0) continue;
|
|
1007
|
+
|
|
1008
|
+
const changed = String(this._gitOutput(baseline.root, ['diff', '--name-only', `${baseline.head}..HEAD`], 10000) || '')
|
|
1009
|
+
.split('\n')
|
|
1010
|
+
.map(line => line.trim())
|
|
1011
|
+
.filter(Boolean);
|
|
1012
|
+
if (changed.length === 0) continue;
|
|
1013
|
+
if (allowedSlugs.length > 0) {
|
|
1014
|
+
const outOfScope = changed.filter(rel => !this._scopeAllowsChangedPath(baseline, rel, allowedSlugs, pageOnly, userMessage));
|
|
1015
|
+
if (outOfScope.length > 0) continue;
|
|
1016
|
+
}
|
|
1017
|
+
return true;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
969
1023
|
_isBroadUiQualityTask(userMessage) {
|
|
970
1024
|
const text = String(userMessage || '');
|
|
971
1025
|
return /\b(professional|polish(?:ed)?|premium|quality|vibecoded|vibe-coded|looks? (?:bad|rough|unfinished|basic)|redesign|improve(?:d)? (?:the )?(?:ui|ux|design|page|pages|site|app|interface)|make (?:it|this|the .{0,50}) (?:look|feel)|landing pages?|listing pages?)\b/i.test(text);
|
|
@@ -2722,6 +2776,10 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
2722
2776
|
|
|
2723
2777
|
const initialIsVisualUiTask = this._isVisualUiTask(userMessage, platformGuides);
|
|
2724
2778
|
const initialIsPublishTask = this._isPublishTask(userMessage);
|
|
2779
|
+
const initialRequiresDeliverable =
|
|
2780
|
+
initialIsVisualUiTask ||
|
|
2781
|
+
initialIsPublishTask ||
|
|
2782
|
+
/\b(make|create|build|add|implement|update|edit|change|fix|improve|redesign|refactor|write|generate|scaffold|wire|ship|work on|delete|remove|drop|unpublish|decommission|take down|take offline)\b/i.test(String(userMessage || ''));
|
|
2725
2783
|
const TASK_TIMEOUT_MS = this._taskTimeoutMs({
|
|
2726
2784
|
message: userMessage,
|
|
2727
2785
|
platformGuides,
|
|
@@ -3526,6 +3584,7 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
3526
3584
|
let runtimeStallRetryCount = 0;
|
|
3527
3585
|
let runtimeModelSwitchCount = 0;
|
|
3528
3586
|
let uiVerificationRetryCount = 0;
|
|
3587
|
+
let taskAcceptedComplete = false;
|
|
3529
3588
|
const runtimeModelsTried = new Set();
|
|
3530
3589
|
const consumeUiRepairNudge = (reason, detail = '') => {
|
|
3531
3590
|
if (!isVisualUiTask) return '';
|
|
@@ -3709,7 +3768,8 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
3709
3768
|
providerKeys || null, effectiveImageModel || null,
|
|
3710
3769
|
taskId, iteration,
|
|
3711
3770
|
effectiveFallbackVisionModels,
|
|
3712
|
-
imageGenerationModel || null
|
|
3771
|
+
imageGenerationModel || null,
|
|
3772
|
+
scopeAwareUserMessage
|
|
3713
3773
|
);
|
|
3714
3774
|
runtimeStallRetryCount = 0;
|
|
3715
3775
|
} catch (runError) {
|
|
@@ -4184,6 +4244,7 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4184
4244
|
iterations: iteration
|
|
4185
4245
|
});
|
|
4186
4246
|
}
|
|
4247
|
+
taskAcceptedComplete = true;
|
|
4187
4248
|
break;
|
|
4188
4249
|
}
|
|
4189
4250
|
|
|
@@ -4265,10 +4326,12 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4265
4326
|
cleanForCheck.match(/\?[^?]*$/m);
|
|
4266
4327
|
if (isShortCompleteReply && !isVisualUiTask) {
|
|
4267
4328
|
console.log(`[${taskId}] Agent gave short complete reply (${cleanForCheck.length} chars) — accepting as done (no nudge)`);
|
|
4329
|
+
taskAcceptedComplete = true;
|
|
4268
4330
|
break;
|
|
4269
4331
|
}
|
|
4270
4332
|
if (isConversational && !isVisualUiTask) {
|
|
4271
4333
|
console.log(`[${taskId}] Agent gave conversational reply — accepting as done (no nudge)`);
|
|
4334
|
+
taskAcceptedComplete = true;
|
|
4272
4335
|
break;
|
|
4273
4336
|
}
|
|
4274
4337
|
const hasPublishEvidence = isPublishTask && this._hasPublishEvidence(output);
|
|
@@ -4415,6 +4478,7 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4415
4478
|
continue;
|
|
4416
4479
|
}
|
|
4417
4480
|
console.log(`[${taskId}] Publish evidence detected without explicit completion signal — accepting as done`);
|
|
4481
|
+
taskAcceptedComplete = true;
|
|
4418
4482
|
break;
|
|
4419
4483
|
}
|
|
4420
4484
|
// After genuine tool activity, check if the final paragraph reads like a conclusion.
|
|
@@ -4437,11 +4501,13 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4437
4501
|
console.log(`[${taskId}] UI conclusion detected without visual verification — continuing`);
|
|
4438
4502
|
} else {
|
|
4439
4503
|
console.log(`[${taskId}] Conclusion detected after tool activity (${lastPara.length} chars) — accepting as done`);
|
|
4504
|
+
taskAcceptedComplete = true;
|
|
4440
4505
|
break;
|
|
4441
4506
|
}
|
|
4442
4507
|
}
|
|
4443
4508
|
}
|
|
4444
4509
|
const madeRealProgress = hasSubstantiveProgress;
|
|
4510
|
+
const hasExplicitIncompleteResult = /\bThis is not complete\b|\bnot complete delivery\b|\brepo still has uncommitted changes\b|\bhas not been delivered yet\b|\bDelivery blocker\b/i.test(output);
|
|
4445
4511
|
if (visualVerificationFailureNudge) {
|
|
4446
4512
|
uiVerificationRetryCount++;
|
|
4447
4513
|
if (uiVerificationRetryCount >= UI_REPAIR_NUDGE_LIMIT) {
|
|
@@ -4469,6 +4535,22 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4469
4535
|
nudgeCount = 0;
|
|
4470
4536
|
console.log(`[${taskId}] Incomplete turn after tool call on iteration ${iteration} — resetting nudge count and retrying`);
|
|
4471
4537
|
iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\nYour last tool call hit a timeout and the response was cut short. Please retry the same tool call now. When finished, end your response with ✓ TASK_COMPLETE.`);
|
|
4538
|
+
} else if (hasExplicitIncompleteResult) {
|
|
4539
|
+
if (this._repoBaselinesHaveDeliveredScopedChanges(repoBaselines, scopeAwareUserMessage)) {
|
|
4540
|
+
console.log(`[${taskId}] Incomplete runner text ignored because scoped repo changes are already clean and pushed`);
|
|
4541
|
+
taskAcceptedComplete = true;
|
|
4542
|
+
taskResult.result = {
|
|
4543
|
+
...(taskResult.result || {}),
|
|
4544
|
+
output: `Verified scoped changes are committed, pushed, and clean.\n✓ TASK_COMPLETE`,
|
|
4545
|
+
};
|
|
4546
|
+
break;
|
|
4547
|
+
}
|
|
4548
|
+
nudgeCount++;
|
|
4549
|
+
if (nudgeCount >= MAX_NUDGES) {
|
|
4550
|
+
throw new Error(`Task remained incomplete after repeated retries: ${output.trim().slice(0, 600)}`);
|
|
4551
|
+
}
|
|
4552
|
+
console.log(`[${taskId}] Incomplete task result detected — continuing (${nudgeCount}/${MAX_NUDGES})`);
|
|
4553
|
+
iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\nAgentForge determined the task is not complete yet:\n\n${output.trim().slice(0, 1200)}\n\nContinue from the current state and finish the missing work or delivery. Only end with ✓ TASK_COMPLETE after the task is actually complete.`);
|
|
4472
4554
|
} else if (madeRealProgress) {
|
|
4473
4555
|
nudgeCount = 0;
|
|
4474
4556
|
console.log(`[${taskId}] Agent made substantive progress on iteration ${iteration} — continuing without nudge`);
|
|
@@ -4478,7 +4560,11 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4478
4560
|
} else {
|
|
4479
4561
|
nudgeCount++;
|
|
4480
4562
|
if (nudgeCount >= MAX_NUDGES) {
|
|
4563
|
+
if (initialRequiresDeliverable) {
|
|
4564
|
+
throw new Error(`Agent did not produce a completion signal after ${nudgeCount} retries for a task that requires a deliverable.`);
|
|
4565
|
+
}
|
|
4481
4566
|
console.log(`[${taskId}] Agent gave ${nudgeCount} consecutive narration-only responses — stopping`);
|
|
4567
|
+
taskAcceptedComplete = true;
|
|
4482
4568
|
break;
|
|
4483
4569
|
}
|
|
4484
4570
|
// Agent only narrated without acting — nudge it to use tools.
|
|
@@ -4509,6 +4595,13 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4509
4595
|
}
|
|
4510
4596
|
}
|
|
4511
4597
|
|
|
4598
|
+
if (initialRequiresDeliverable && taskResult?.success !== false && !taskAcceptedComplete) {
|
|
4599
|
+
const rawLoopOutput = taskResult?.result?.output || '';
|
|
4600
|
+
if (!/✓\s*TASK_COMPLETE/i.test(rawLoopOutput) && !this._repoBaselinesHaveDeliveredScopedChanges(repoBaselines, scopeAwareUserMessage)) {
|
|
4601
|
+
throw new Error(`Task ended without a verified completion signal for a deliverable task. Last output: ${String(rawLoopOutput).trim().slice(0, 600)}`);
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
|
|
4512
4605
|
// Clean up listeners and timer
|
|
4513
4606
|
if (_cleanup) { _cleanup(); _cleanup = null; }
|
|
4514
4607
|
|
|
@@ -5476,6 +5569,9 @@ end tell`.trim().replace(/\n/g, '\n');
|
|
|
5476
5569
|
|
|
5477
5570
|
_isVisualUiTask(message, platformGuides = []) {
|
|
5478
5571
|
const text = String(message || '').toLowerCase();
|
|
5572
|
+
if (this._allowsScopedPageSourcesToRemainDeleted(text)) {
|
|
5573
|
+
return false;
|
|
5574
|
+
}
|
|
5479
5575
|
if (Array.isArray(platformGuides) && platformGuides.some(g => String(g?.taskType || g?.task_type || '').toLowerCase() === 'ui')) {
|
|
5480
5576
|
return true;
|
|
5481
5577
|
}
|