@hamp10/agentforge 0.2.27 → 0.2.29
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 +25 -0
- package/src/OpenClawCLI.js +194 -7
- package/src/default-task-guides.js +2 -1
- package/src/taskSemantics.js +2 -2
- package/src/worker.js +108 -3
package/package.json
CHANGED
|
@@ -35,6 +35,12 @@ const cases = [
|
|
|
35
35
|
absent: ['example-com', 'delete', 'rebuild', 'readability', 'design', 'urls', 'site'],
|
|
36
36
|
pageOnly: true,
|
|
37
37
|
},
|
|
38
|
+
{
|
|
39
|
+
text: 'Delete and rebuild the listing pages for Alpha and Beta from a clean start. Only change those listing pages.',
|
|
40
|
+
slugs: ['alpha', 'beta'],
|
|
41
|
+
absent: ['beta-from', 'clean-start', 'from'],
|
|
42
|
+
pageOnly: true,
|
|
43
|
+
},
|
|
38
44
|
{
|
|
39
45
|
text: 'Work on Alpha pages for readability',
|
|
40
46
|
slugs: ['alpha'],
|
|
@@ -214,6 +220,25 @@ try {
|
|
|
214
220
|
const worker = Object.create(AgentForgeWorker.prototype);
|
|
215
221
|
worker.cli = { isAnthropicApiKey: (key) => /^sk-ant-/i.test(String(key || '')) };
|
|
216
222
|
const cli = Object.create(OpenClawCLI.prototype);
|
|
223
|
+
const deletedScopedPagePath = path.join(fixture.repo, 'public_html', 'domains', 'alpha.html');
|
|
224
|
+
rmSync(deletedScopedPagePath, { force: true });
|
|
225
|
+
assert.equal(
|
|
226
|
+
worker._buildDeletedScopedPageNudge(
|
|
227
|
+
[{ root: fixture.repo, head: fixture.head }],
|
|
228
|
+
'Delete the Example.com listing page for Alpha. Only change that listing page.'
|
|
229
|
+
),
|
|
230
|
+
'',
|
|
231
|
+
'explicit page deletion tasks should be allowed to leave the scoped target page deleted'
|
|
232
|
+
);
|
|
233
|
+
assert.match(
|
|
234
|
+
worker._buildDeletedScopedPageNudge(
|
|
235
|
+
[{ root: fixture.repo, head: fixture.head }],
|
|
236
|
+
'Delete and rebuild the Alpha listing page from a clean start, preserving the same URL. Only change that listing page.'
|
|
237
|
+
),
|
|
238
|
+
/deleted target page source\(s\) still missing/i,
|
|
239
|
+
'clean-start rebuild tasks should still require the scoped target page to be recreated'
|
|
240
|
+
);
|
|
241
|
+
git(fixture.repo, ['restore', '--', 'public_html/domains/alpha.html']);
|
|
217
242
|
const projectsRoot = mkdtempSync(path.join(tmpdir(), 'agentforge-project-list-'));
|
|
218
243
|
let agentWorkspace = null;
|
|
219
244
|
try {
|
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,11 @@ 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 taskRequiresFileChange = /\b(make|create|build|add|implement|update|edit|change|fix|improve|redesign|refactor|write|generate|scaffold|wire|ship|work on)\b/i.test(task);
|
|
4188
|
-
const
|
|
4355
|
+
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(task);
|
|
4356
|
+
const taskAllowsDeletedScopedPages = this._allowsDirectScopedPageSourcesToRemainDeleted(task);
|
|
4357
|
+
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(task);
|
|
4189
4358
|
const directRequestTimeoutMs = Math.max(
|
|
4190
4359
|
30_000,
|
|
4191
4360
|
Number(process.env.AGENTFORGE_DIRECT_LLM_REQUEST_TIMEOUT_MS || (taskRequiresVisualVerification ? 120_000 : 90_000))
|
|
@@ -4699,12 +4868,13 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
4699
4868
|
return warnings.join('\n').slice(0, 1200);
|
|
4700
4869
|
};
|
|
4701
4870
|
const directStopResponse = (reason, forceIncomplete = false) => {
|
|
4702
|
-
const
|
|
4871
|
+
const pendingGitDelivery = this._directGitPendingDeliveryMessage(workDir, task, { directMutationCount, taskRequiresFileChange });
|
|
4872
|
+
const noDeliverable = taskRequiresFileChange && directMutationCount === 0 && !pendingGitDelivery;
|
|
4703
4873
|
const missingScopedMutation = missingScopedMutationSlugs();
|
|
4704
4874
|
const missingVisualVerification = taskRequiresVisualVerification && directMutationCount > 0 && !hasCleanLocalVerificationForLatestMutation();
|
|
4705
4875
|
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;
|
|
4876
|
+
const stoppedBeforeSummary = forceIncomplete && !noDeliverable && missingScopedMutation.length === 0 && !missingVisualVerification && !visualQualityBlocked && !pendingGitDelivery;
|
|
4877
|
+
const incomplete = forceIncomplete || noDeliverable || missingScopedMutation.length > 0 || missingVisualVerification || visualQualityBlocked || !!pendingGitDelivery;
|
|
4708
4878
|
return {
|
|
4709
4879
|
text: [
|
|
4710
4880
|
noDeliverable
|
|
@@ -4715,6 +4885,8 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
4715
4885
|
? `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
4886
|
: missingVisualVerification
|
|
4717
4887
|
? `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.`
|
|
4888
|
+
: pendingGitDelivery
|
|
4889
|
+
? `Native AgentForge tools changed files, but the repo has not been delivered yet. ${reason}\n${pendingGitDelivery}`
|
|
4718
4890
|
: stoppedBeforeSummary
|
|
4719
4891
|
? `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
4892
|
: `Native AgentForge tools completed ${directToolCount} action(s), including any edits or browser checks listed below. ${reason}`,
|
|
@@ -4783,6 +4955,20 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
4783
4955
|
usedTools: directToolCount > 0,
|
|
4784
4956
|
};
|
|
4785
4957
|
}
|
|
4958
|
+
const delivery = this._directMaybeAutoDeliverGitChanges(workDir, task, { directMutationCount, taskRequiresFileChange });
|
|
4959
|
+
if (delivery.delivered) {
|
|
4960
|
+
recordDirectToolSummary('git', delivery.message);
|
|
4961
|
+
} else if (delivery.pending) {
|
|
4962
|
+
return {
|
|
4963
|
+
text: [
|
|
4964
|
+
summary || fallbackSummary,
|
|
4965
|
+
delivery.pending,
|
|
4966
|
+
delivery.reason ? `Delivery blocker: ${delivery.reason}` : null,
|
|
4967
|
+
].filter(Boolean).join('\n'),
|
|
4968
|
+
hasToolCalls: false,
|
|
4969
|
+
usedTools: directToolCount > 0,
|
|
4970
|
+
};
|
|
4971
|
+
}
|
|
4786
4972
|
return {
|
|
4787
4973
|
text: [
|
|
4788
4974
|
summary || fallbackSummary,
|
|
@@ -4887,7 +5073,8 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
4887
5073
|
const text = parts.filter(p => p.text).map(p => p.text).join('');
|
|
4888
5074
|
if (!functionCalls.length) {
|
|
4889
5075
|
if (step > 0) {
|
|
4890
|
-
|
|
5076
|
+
const pendingGitDelivery = this._directGitPendingDeliveryMessage(workDir, task, { directMutationCount, taskRequiresFileChange });
|
|
5077
|
+
if (taskRequiresFileChange && directMutationCount === 0 && !pendingGitDelivery && directInternalNudgeCount < 2) {
|
|
4891
5078
|
directInternalNudgeCount += 1;
|
|
4892
5079
|
contents.push(content);
|
|
4893
5080
|
contents.push({
|
|
@@ -4902,7 +5089,7 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
4902
5089
|
});
|
|
4903
5090
|
continue;
|
|
4904
5091
|
}
|
|
4905
|
-
if (taskRequiresFileChange && directMutationCount === 0) {
|
|
5092
|
+
if (taskRequiresFileChange && directMutationCount === 0 && !pendingGitDelivery) {
|
|
4906
5093
|
const inspectedText = stripCompletionMarker(text);
|
|
4907
5094
|
return {
|
|
4908
5095
|
text: [
|
|
@@ -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/taskSemantics.js
CHANGED
|
@@ -112,7 +112,7 @@ export function extractNamedPageScopeSlugs(message) {
|
|
|
112
112
|
addScopeCandidate(candidates, match[1]);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
const afterPage = /\b(?:pages?|screens?|routes?|views?|listings?)\s+(?:for|of|called|named)\s+(?:the\s+)?([a-z0-9][a-z0-9 ._/-]{0,160}?)(?=$|[?!;]|\.(?:\s|$)|\s+(?:on|at|in|with|without|that|which|where|when|because|so|but)\b)/gi;
|
|
115
|
+
const afterPage = /\b(?:pages?|screens?|routes?|views?|listings?)\s+(?:for|of|called|named)\s+(?:the\s+)?([a-z0-9][a-z0-9 ._/-]{0,160}?)(?=$|[?!;]|\.(?:\s|$)|\s+(?:on|at|in|from|with|without|while|using|preserv(?:e|ing)|that|which|where|when|because|so|but)\b)/gi;
|
|
116
116
|
for (const match of text.matchAll(afterPage)) {
|
|
117
117
|
const before = text.slice(Math.max(0, match.index - 120), match.index);
|
|
118
118
|
const precedingToken = before.match(/\b([a-z0-9][a-z0-9._/-]{2,})\s*$/i)?.[1] || '';
|
|
@@ -124,7 +124,7 @@ export function extractNamedPageScopeSlugs(message) {
|
|
|
124
124
|
|
|
125
125
|
for (const match of text.matchAll(/\b[a-z0-9]+(?:[.-][a-z0-9]+)+\b/gi)) {
|
|
126
126
|
const after = text.slice(match.index + match[0].length);
|
|
127
|
-
const pageTarget = after.match(/^\s+(?:listing\s+)?(?:pages?|screens?|routes?|views?|listings?)\s+(?:for|of|called|named)\s+(?:the\s+)?([a-z0-9][a-z0-9 ._/-]{0,160}?)(?=$|[?!;]|\.(?:\s|$)|\s+(?:on|at|in|with|without|that|which|where|when|because|so|but)\b)/i);
|
|
127
|
+
const pageTarget = after.match(/^\s+(?:listing\s+)?(?:pages?|screens?|routes?|views?|listings?)\s+(?:for|of|called|named)\s+(?:the\s+)?([a-z0-9][a-z0-9 ._/-]{0,160}?)(?=$|[?!;]|\.(?:\s|$)|\s+(?:on|at|in|from|with|without|while|using|preserv(?:e|ing)|that|which|where|when|because|so|but)\b)/i);
|
|
128
128
|
if (pageTarget) {
|
|
129
129
|
const targetCandidates = new Set();
|
|
130
130
|
addScopeCandidate(targetCandidates, pageTarget[1]);
|
package/src/worker.js
CHANGED
|
@@ -501,6 +501,17 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
501
501
|
return /\b(delete|remove|drop|strip|simplify|replace|rewrite|rebuild|from scratch|start over|nuke|wipe|clear out|throw away)\b/i.test(String(userMessage || ''));
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
+
_allowsScopedPageSourcesToRemainDeleted(userMessage) {
|
|
505
|
+
const text = String(userMessage || '');
|
|
506
|
+
const asksForDeletion = /\b(delete|remove|drop|unpublish|decommission|take down|take offline)\b/i.test(text);
|
|
507
|
+
if (!asksForDeletion) return false;
|
|
508
|
+
|
|
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;
|
|
513
|
+
}
|
|
514
|
+
|
|
504
515
|
_parseNumstat(output, source) {
|
|
505
516
|
const stats = [];
|
|
506
517
|
for (const line of String(output || '').split('\n')) {
|
|
@@ -692,10 +703,28 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
692
703
|
return scopedSources.length > 0 && outOfScopeMentions.length === 0;
|
|
693
704
|
}
|
|
694
705
|
|
|
695
|
-
|
|
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 = '') {
|
|
696
724
|
const lower = String(rel || '').toLowerCase();
|
|
697
725
|
const slugAllowed = this._scopeSlugsMatchingText(lower, allowedSlugs).length > 0;
|
|
698
726
|
if (slugAllowed) return !pageOnly || this._isPageSourcePath(rel);
|
|
727
|
+
if (this._isScopedReferenceSourcePath(baseline, rel, allowedSlugs, pageOnly, userMessage)) return true;
|
|
699
728
|
return this._isNewScopedPageOwnedAsset(baseline, rel, allowedSlugs, pageOnly);
|
|
700
729
|
}
|
|
701
730
|
|
|
@@ -725,7 +754,7 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
725
754
|
const initialDirty = new Set(baseline.initialDirtyPaths || []);
|
|
726
755
|
const outOfScope = [...names]
|
|
727
756
|
.filter(rel => !initialDirty.has(rel))
|
|
728
|
-
.filter(rel => !this._scopeAllowsChangedPath(baseline, rel, allowedSlugs, pageOnly))
|
|
757
|
+
.filter(rel => !this._scopeAllowsChangedPath(baseline, rel, allowedSlugs, pageOnly, userMessage))
|
|
729
758
|
.sort();
|
|
730
759
|
if (outOfScope.length > 0) {
|
|
731
760
|
warnings.push({ repo: baseline.root, head: baseline.head, allowedSlugs, pageOnly, files: outOfScope });
|
|
@@ -791,7 +820,7 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
791
820
|
const initialDirty = new Set(baseline.initialDirtyPaths || []);
|
|
792
821
|
const files = [...names]
|
|
793
822
|
.filter(rel => resetPreexistingScopedTargets || !initialDirty.has(rel))
|
|
794
|
-
.filter(rel => this._scopeAllowsChangedPath(baseline, rel, allowedSlugs, pageOnly))
|
|
823
|
+
.filter(rel => this._scopeAllowsChangedPath(baseline, rel, allowedSlugs, pageOnly, userMessage))
|
|
795
824
|
.sort();
|
|
796
825
|
if (files.length === 0) continue;
|
|
797
826
|
|
|
@@ -848,6 +877,7 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
848
877
|
|
|
849
878
|
_findDeletedScopedPageSources(repoBaselines, userMessage) {
|
|
850
879
|
if (!Array.isArray(repoBaselines) || repoBaselines.length === 0) return [];
|
|
880
|
+
if (this._allowsScopedPageSourcesToRemainDeleted(userMessage)) return [];
|
|
851
881
|
const { slugs: allowedSlugs, pageOnly } = this._extractExplicitScope(userMessage);
|
|
852
882
|
if (allowedSlugs.length === 0 || !pageOnly) return [];
|
|
853
883
|
|
|
@@ -956,6 +986,40 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
956
986
|
return warnings.length > 0 ? this._formatDeliveryStateNudge(warnings) : '';
|
|
957
987
|
}
|
|
958
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
|
+
|
|
959
1023
|
_isBroadUiQualityTask(userMessage) {
|
|
960
1024
|
const text = String(userMessage || '');
|
|
961
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);
|
|
@@ -2712,6 +2776,10 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
2712
2776
|
|
|
2713
2777
|
const initialIsVisualUiTask = this._isVisualUiTask(userMessage, platformGuides);
|
|
2714
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 || ''));
|
|
2715
2783
|
const TASK_TIMEOUT_MS = this._taskTimeoutMs({
|
|
2716
2784
|
message: userMessage,
|
|
2717
2785
|
platformGuides,
|
|
@@ -3516,6 +3584,7 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
3516
3584
|
let runtimeStallRetryCount = 0;
|
|
3517
3585
|
let runtimeModelSwitchCount = 0;
|
|
3518
3586
|
let uiVerificationRetryCount = 0;
|
|
3587
|
+
let taskAcceptedComplete = false;
|
|
3519
3588
|
const runtimeModelsTried = new Set();
|
|
3520
3589
|
const consumeUiRepairNudge = (reason, detail = '') => {
|
|
3521
3590
|
if (!isVisualUiTask) return '';
|
|
@@ -4174,6 +4243,7 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4174
4243
|
iterations: iteration
|
|
4175
4244
|
});
|
|
4176
4245
|
}
|
|
4246
|
+
taskAcceptedComplete = true;
|
|
4177
4247
|
break;
|
|
4178
4248
|
}
|
|
4179
4249
|
|
|
@@ -4255,10 +4325,12 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4255
4325
|
cleanForCheck.match(/\?[^?]*$/m);
|
|
4256
4326
|
if (isShortCompleteReply && !isVisualUiTask) {
|
|
4257
4327
|
console.log(`[${taskId}] Agent gave short complete reply (${cleanForCheck.length} chars) — accepting as done (no nudge)`);
|
|
4328
|
+
taskAcceptedComplete = true;
|
|
4258
4329
|
break;
|
|
4259
4330
|
}
|
|
4260
4331
|
if (isConversational && !isVisualUiTask) {
|
|
4261
4332
|
console.log(`[${taskId}] Agent gave conversational reply — accepting as done (no nudge)`);
|
|
4333
|
+
taskAcceptedComplete = true;
|
|
4262
4334
|
break;
|
|
4263
4335
|
}
|
|
4264
4336
|
const hasPublishEvidence = isPublishTask && this._hasPublishEvidence(output);
|
|
@@ -4405,6 +4477,7 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4405
4477
|
continue;
|
|
4406
4478
|
}
|
|
4407
4479
|
console.log(`[${taskId}] Publish evidence detected without explicit completion signal — accepting as done`);
|
|
4480
|
+
taskAcceptedComplete = true;
|
|
4408
4481
|
break;
|
|
4409
4482
|
}
|
|
4410
4483
|
// After genuine tool activity, check if the final paragraph reads like a conclusion.
|
|
@@ -4427,11 +4500,13 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4427
4500
|
console.log(`[${taskId}] UI conclusion detected without visual verification — continuing`);
|
|
4428
4501
|
} else {
|
|
4429
4502
|
console.log(`[${taskId}] Conclusion detected after tool activity (${lastPara.length} chars) — accepting as done`);
|
|
4503
|
+
taskAcceptedComplete = true;
|
|
4430
4504
|
break;
|
|
4431
4505
|
}
|
|
4432
4506
|
}
|
|
4433
4507
|
}
|
|
4434
4508
|
const madeRealProgress = hasSubstantiveProgress;
|
|
4509
|
+
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);
|
|
4435
4510
|
if (visualVerificationFailureNudge) {
|
|
4436
4511
|
uiVerificationRetryCount++;
|
|
4437
4512
|
if (uiVerificationRetryCount >= UI_REPAIR_NUDGE_LIMIT) {
|
|
@@ -4459,6 +4534,22 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4459
4534
|
nudgeCount = 0;
|
|
4460
4535
|
console.log(`[${taskId}] Incomplete turn after tool call on iteration ${iteration} — resetting nudge count and retrying`);
|
|
4461
4536
|
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.`);
|
|
4537
|
+
} else if (hasExplicitIncompleteResult) {
|
|
4538
|
+
if (this._repoBaselinesHaveDeliveredScopedChanges(repoBaselines, scopeAwareUserMessage)) {
|
|
4539
|
+
console.log(`[${taskId}] Incomplete runner text ignored because scoped repo changes are already clean and pushed`);
|
|
4540
|
+
taskAcceptedComplete = true;
|
|
4541
|
+
taskResult.result = {
|
|
4542
|
+
...(taskResult.result || {}),
|
|
4543
|
+
output: `Verified scoped changes are committed, pushed, and clean.\n✓ TASK_COMPLETE`,
|
|
4544
|
+
};
|
|
4545
|
+
break;
|
|
4546
|
+
}
|
|
4547
|
+
nudgeCount++;
|
|
4548
|
+
if (nudgeCount >= MAX_NUDGES) {
|
|
4549
|
+
throw new Error(`Task remained incomplete after repeated retries: ${output.trim().slice(0, 600)}`);
|
|
4550
|
+
}
|
|
4551
|
+
console.log(`[${taskId}] Incomplete task result detected — continuing (${nudgeCount}/${MAX_NUDGES})`);
|
|
4552
|
+
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.`);
|
|
4462
4553
|
} else if (madeRealProgress) {
|
|
4463
4554
|
nudgeCount = 0;
|
|
4464
4555
|
console.log(`[${taskId}] Agent made substantive progress on iteration ${iteration} — continuing without nudge`);
|
|
@@ -4468,7 +4559,11 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4468
4559
|
} else {
|
|
4469
4560
|
nudgeCount++;
|
|
4470
4561
|
if (nudgeCount >= MAX_NUDGES) {
|
|
4562
|
+
if (initialRequiresDeliverable) {
|
|
4563
|
+
throw new Error(`Agent did not produce a completion signal after ${nudgeCount} retries for a task that requires a deliverable.`);
|
|
4564
|
+
}
|
|
4471
4565
|
console.log(`[${taskId}] Agent gave ${nudgeCount} consecutive narration-only responses — stopping`);
|
|
4566
|
+
taskAcceptedComplete = true;
|
|
4472
4567
|
break;
|
|
4473
4568
|
}
|
|
4474
4569
|
// Agent only narrated without acting — nudge it to use tools.
|
|
@@ -4499,6 +4594,13 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4499
4594
|
}
|
|
4500
4595
|
}
|
|
4501
4596
|
|
|
4597
|
+
if (initialRequiresDeliverable && taskResult?.success !== false && !taskAcceptedComplete) {
|
|
4598
|
+
const rawLoopOutput = taskResult?.result?.output || '';
|
|
4599
|
+
if (!/✓\s*TASK_COMPLETE/i.test(rawLoopOutput) && !this._repoBaselinesHaveDeliveredScopedChanges(repoBaselines, scopeAwareUserMessage)) {
|
|
4600
|
+
throw new Error(`Task ended without a verified completion signal for a deliverable task. Last output: ${String(rawLoopOutput).trim().slice(0, 600)}`);
|
|
4601
|
+
}
|
|
4602
|
+
}
|
|
4603
|
+
|
|
4502
4604
|
// Clean up listeners and timer
|
|
4503
4605
|
if (_cleanup) { _cleanup(); _cleanup = null; }
|
|
4504
4606
|
|
|
@@ -5466,6 +5568,9 @@ end tell`.trim().replace(/\n/g, '\n');
|
|
|
5466
5568
|
|
|
5467
5569
|
_isVisualUiTask(message, platformGuides = []) {
|
|
5468
5570
|
const text = String(message || '').toLowerCase();
|
|
5571
|
+
if (this._allowsScopedPageSourcesToRemainDeleted(text)) {
|
|
5572
|
+
return false;
|
|
5573
|
+
}
|
|
5469
5574
|
if (Array.isArray(platformGuides) && platformGuides.some(g => String(g?.taskType || g?.task_type || '').toLowerCase() === 'ui')) {
|
|
5470
5575
|
return true;
|
|
5471
5576
|
}
|