@hamp10/agentforge 0.2.32 → 0.2.34
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 +28 -0
- package/src/OpenClawCLI.js +22 -0
- package/src/taskSemantics.js +20 -3
- package/src/worker.js +106 -7
package/package.json
CHANGED
|
@@ -29,6 +29,12 @@ const cases = [
|
|
|
29
29
|
absent: ['example-com', 'visual', 'excellent'],
|
|
30
30
|
pageOnly: true,
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
text: 'Can you make Example.com listing pages for AlphaBoard.ai and BetaMatch.ai, and fix the GammaForge.ai listing page?',
|
|
34
|
+
slugs: ['alphaboard-ai', 'betamatch-ai', 'gammaforge-ai'],
|
|
35
|
+
absent: ['example-com', 'make', 'fix'],
|
|
36
|
+
pageOnly: true,
|
|
37
|
+
},
|
|
32
38
|
{
|
|
33
39
|
text: 'Work on the Example.com listing pages for AlphaBoard and BetaMatch. Delete and rebuild those two listing page implementations from a clean start, preserving the same URLs and site conventions. Fix the readability and design issues. Only change those two listing pages.',
|
|
34
40
|
slugs: ['alphaboard', 'betamatch'],
|
|
@@ -179,6 +185,10 @@ const makeShallowUiFixture = () => {
|
|
|
179
185
|
writeFileSync(path.join(domainsDir, `${name}.html`), baselineHtml);
|
|
180
186
|
writeFileSync(path.join(nestedDomainsDir, `${name}.html`), baselineHtml);
|
|
181
187
|
}
|
|
188
|
+
writeFileSync(
|
|
189
|
+
path.join(repo, 'public_html', 'domains.html'),
|
|
190
|
+
'<!doctype html><html><body><main><a href="/domains/alpha.html">Alpha</a><a href="/domains/beta.html">Beta</a></main></body></html>'
|
|
191
|
+
);
|
|
182
192
|
git(repo, ['init']);
|
|
183
193
|
git(repo, ['config', 'user.email', 'test@example.com']);
|
|
184
194
|
git(repo, ['config', 'user.name', 'AgentForge Test']);
|
|
@@ -337,6 +347,24 @@ try {
|
|
|
337
347
|
);
|
|
338
348
|
const message = 'Improve the listing pages for Alpha and Beta so they feel polished.';
|
|
339
349
|
const baseline = [{ root: fixture.repo, head: fixture.head }];
|
|
350
|
+
const domainsIndexPath = path.join(fixture.repo, 'public_html', 'domains.html');
|
|
351
|
+
const domainsIndexCurrent = readFileSync(domainsIndexPath, 'utf-8');
|
|
352
|
+
const deleteOnlyScopedPagesMessage = 'Delete the Example.com listing pages for Alpha and Beta. Only change those listing pages.';
|
|
353
|
+
writeFileSync(
|
|
354
|
+
domainsIndexPath,
|
|
355
|
+
domainsIndexCurrent.replace('</main>', '<p>Alpha and Beta are featured listing pages.</p></main>')
|
|
356
|
+
);
|
|
357
|
+
assert.match(
|
|
358
|
+
worker._formatScopeDriftNudge(worker._findScopeDriftRepoChanges(baseline, deleteOnlyScopedPagesMessage)),
|
|
359
|
+
/public_html\/domains\.html/i,
|
|
360
|
+
'broad listing indexes should not become editable targets just because they mention scoped page names'
|
|
361
|
+
);
|
|
362
|
+
assert.throws(
|
|
363
|
+
() => cli._guardDirectFileWritePath(domainsIndexPath, fixture.repo, { task: deleteOnlyScopedPagesMessage }),
|
|
364
|
+
/outside the requested task scope/i,
|
|
365
|
+
'direct scoped deletion work should reject broad listing indexes even when their content mentions scoped pages'
|
|
366
|
+
);
|
|
367
|
+
writeFileSync(domainsIndexPath, domainsIndexCurrent);
|
|
340
368
|
assert.match(
|
|
341
369
|
worker._formatScopedUiTargetSetReminder(message),
|
|
342
370
|
/Requested scoped UI targets: alpha, beta/i,
|
package/src/OpenClawCLI.js
CHANGED
|
@@ -617,6 +617,27 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
617
617
|
return /\.(?:html?|xhtml|astro|mdx?)$/i.test(String(relativePath || ''));
|
|
618
618
|
}
|
|
619
619
|
|
|
620
|
+
_isDirectBroadReferenceSourcePath(relativePath) {
|
|
621
|
+
const normalized = String(relativePath || '').replace(/\\/g, '/').toLowerCase();
|
|
622
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
623
|
+
const base = path.basename(normalized).replace(/\.[^.]+$/, '');
|
|
624
|
+
const broadBase = new Set([
|
|
625
|
+
'index', 'home', 'main', 'app', 'root',
|
|
626
|
+
'domain', 'domains', 'listing', 'listings', 'catalog', 'directory', 'archive',
|
|
627
|
+
'portfolio', 'about', 'contact', 'support',
|
|
628
|
+
'header', 'footer', 'nav', 'navbar', 'navigation',
|
|
629
|
+
'layout', 'layouts', 'template', 'templates',
|
|
630
|
+
'base', 'shared', 'global', 'common', 'universal',
|
|
631
|
+
'theme', 'themes', 'tokens', 'components', 'component',
|
|
632
|
+
'reset', 'site', 'website',
|
|
633
|
+
]);
|
|
634
|
+
if (broadBase.has(base)) return true;
|
|
635
|
+
if (/(^|[-_.])(domain|domains|listing|listings|catalog|directory|portfolio|layout|template|base|shared|global|common|universal|theme|tokens?|components?|reset|site|website)([-_.]|$)/i.test(base)) {
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
return parts.slice(0, -1).some(part => /^(components?|layouts?|partials?|includes?|shared|common|global|styles?|theme|tokens?|templates?)$/i.test(part));
|
|
639
|
+
}
|
|
640
|
+
|
|
620
641
|
_directFileMentionsAsset(content, sourceRel, assetRel) {
|
|
621
642
|
const source = String(content || '');
|
|
622
643
|
const assetUnix = String(assetRel || '').replace(/\\/g, '/');
|
|
@@ -685,6 +706,7 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
685
706
|
|
|
686
707
|
const relative = path.relative(workDir, filePath);
|
|
687
708
|
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return false;
|
|
709
|
+
if (this._isDirectBroadReferenceSourcePath(relativePath)) return false;
|
|
688
710
|
|
|
689
711
|
const contents = [];
|
|
690
712
|
try {
|
package/src/taskSemantics.js
CHANGED
|
@@ -20,7 +20,19 @@ const GENERIC_SCOPE_WORDS = new Set([
|
|
|
20
20
|
|
|
21
21
|
const followedByNamedPageTargets = (text, match) => {
|
|
22
22
|
const after = text.slice(match.index + match[0].length);
|
|
23
|
-
|
|
23
|
+
const target = 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);
|
|
24
|
+
if (!target) return false;
|
|
25
|
+
const targetCandidates = new Set();
|
|
26
|
+
addScopeCandidate(targetCandidates, target[1]);
|
|
27
|
+
targetCandidates.delete(scopeSlug(match[0]));
|
|
28
|
+
return targetCandidates.size > 0;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const matchFromCapture = (match, captureIndex = 1) => {
|
|
32
|
+
const value = match?.[captureIndex] || '';
|
|
33
|
+
const whole = match?.[0] || '';
|
|
34
|
+
const offset = whole.indexOf(value);
|
|
35
|
+
return { 0: value, index: (match?.index || 0) + (offset >= 0 ? offset : 0) };
|
|
24
36
|
};
|
|
25
37
|
|
|
26
38
|
export function scopeSlug(value) {
|
|
@@ -109,10 +121,11 @@ export function extractNamedPageScopeSlugs(message) {
|
|
|
109
121
|
|
|
110
122
|
const directBeforePage = /\b([a-z0-9][a-z0-9._/-]{2,80})\s+(?:pages?|screens?|routes?|views?|listings?)\b/gi;
|
|
111
123
|
for (const match of text.matchAll(directBeforePage)) {
|
|
124
|
+
if (followedByNamedPageTargets(text, matchFromCapture(match))) continue;
|
|
112
125
|
addScopeCandidate(candidates, match[1]);
|
|
113
126
|
}
|
|
114
127
|
|
|
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}?)(?=$|[
|
|
128
|
+
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
129
|
for (const match of text.matchAll(afterPage)) {
|
|
117
130
|
const before = text.slice(Math.max(0, match.index - 120), match.index);
|
|
118
131
|
const precedingToken = before.match(/\b([a-z0-9][a-z0-9._/-]{2,})\s*$/i)?.[1] || '';
|
|
@@ -124,7 +137,7 @@ export function extractNamedPageScopeSlugs(message) {
|
|
|
124
137
|
|
|
125
138
|
for (const match of text.matchAll(/\b[a-z0-9]+(?:[.-][a-z0-9]+)+\b/gi)) {
|
|
126
139
|
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}?)(?=$|[
|
|
140
|
+
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
141
|
if (pageTarget) {
|
|
129
142
|
const targetCandidates = new Set();
|
|
130
143
|
addScopeCandidate(targetCandidates, pageTarget[1]);
|
|
@@ -134,6 +147,10 @@ export function extractNamedPageScopeSlugs(message) {
|
|
|
134
147
|
}
|
|
135
148
|
}
|
|
136
149
|
|
|
150
|
+
for (const match of text.matchAll(/\b[a-z0-9]+(?:[.-][a-z0-9]+)+\b/gi)) {
|
|
151
|
+
if (followedByNamedPageTargets(text, match)) candidates.delete(scopeSlug(match[0]));
|
|
152
|
+
}
|
|
153
|
+
|
|
137
154
|
return [...candidates];
|
|
138
155
|
}
|
|
139
156
|
|
package/src/worker.js
CHANGED
|
@@ -650,6 +650,27 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
650
650
|
return /\.(?:html?|xhtml|astro|mdx?)$/i.test(String(relativePath || ''));
|
|
651
651
|
}
|
|
652
652
|
|
|
653
|
+
_isBroadReferenceSourcePath(relativePath) {
|
|
654
|
+
const normalized = String(relativePath || '').replace(/\\/g, '/').toLowerCase();
|
|
655
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
656
|
+
const base = path.basename(normalized).replace(/\.[^.]+$/, '');
|
|
657
|
+
const broadBase = new Set([
|
|
658
|
+
'index', 'home', 'main', 'app', 'root',
|
|
659
|
+
'domain', 'domains', 'listing', 'listings', 'catalog', 'directory', 'archive',
|
|
660
|
+
'portfolio', 'about', 'contact', 'support',
|
|
661
|
+
'header', 'footer', 'nav', 'navbar', 'navigation',
|
|
662
|
+
'layout', 'layouts', 'template', 'templates',
|
|
663
|
+
'base', 'shared', 'global', 'common', 'universal',
|
|
664
|
+
'theme', 'themes', 'tokens', 'components', 'component',
|
|
665
|
+
'reset', 'site', 'website',
|
|
666
|
+
]);
|
|
667
|
+
if (broadBase.has(base)) return true;
|
|
668
|
+
if (/(^|[-_.])(domain|domains|listing|listings|catalog|directory|portfolio|layout|template|base|shared|global|common|universal|theme|tokens?|components?|reset|site|website)([-_.]|$)/i.test(base)) {
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
return parts.slice(0, -1).some(part => /^(components?|layouts?|partials?|includes?|shared|common|global|styles?|theme|tokens?|templates?)$/i.test(part));
|
|
672
|
+
}
|
|
673
|
+
|
|
653
674
|
_htmlPageCollectionDirs(repo, ref = 'HEAD') {
|
|
654
675
|
if (!repo) return [];
|
|
655
676
|
const counts = new Map();
|
|
@@ -748,6 +769,7 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
748
769
|
if (!baseline?.root || !pageOnly || !this._allowsScopedPageSourcesToRemainDeleted(userMessage)) return false;
|
|
749
770
|
if (!Array.isArray(allowedSlugs) || allowedSlugs.length === 0) return false;
|
|
750
771
|
if (!this._isPageSourcePath(rel)) return false;
|
|
772
|
+
if (this._isBroadReferenceSourcePath(rel)) return false;
|
|
751
773
|
|
|
752
774
|
let content = '';
|
|
753
775
|
try {
|
|
@@ -854,11 +876,8 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
854
876
|
addLines(this._gitOutput(baseline.root, ['diff', '--name-only'], 10000));
|
|
855
877
|
addLines(this._gitOutput(baseline.root, ['diff', '--name-only', '--cached'], 10000));
|
|
856
878
|
addLines(this._gitOutput(baseline.root, ['ls-files', '--others', '--exclude-standard'], 10000));
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
const rel = rawLine.length >= 3 && rawLine[2] === ' ' ? rawLine.slice(3) : rawLine.trim().replace(/^..\s+/, '');
|
|
860
|
-
const pathName = rel.includes(' -> ') ? rel.split(' -> ').pop() : rel;
|
|
861
|
-
if (pathName) names.add(pathName);
|
|
879
|
+
for (const pathName of this._parseGitStatusPaths(this._gitStatusPorcelain(baseline.root, 10000))) {
|
|
880
|
+
names.add(pathName);
|
|
862
881
|
}
|
|
863
882
|
|
|
864
883
|
const initialDirty = new Set(baseline.initialDirtyPaths || []);
|
|
@@ -890,6 +909,45 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
890
909
|
return restored;
|
|
891
910
|
}
|
|
892
911
|
|
|
912
|
+
_restoreGeneratedScopedUiTargets(repoBaselines, userMessage) {
|
|
913
|
+
if (!this._isBroadUiQualityTask(userMessage)) return [];
|
|
914
|
+
if (!Array.isArray(repoBaselines) || repoBaselines.length === 0) return [];
|
|
915
|
+
const { slugs: allowedSlugs, pageOnly } = this._extractExplicitScope(userMessage);
|
|
916
|
+
if (allowedSlugs.length === 0 || !pageOnly) return [];
|
|
917
|
+
|
|
918
|
+
const restored = [];
|
|
919
|
+
for (const baseline of repoBaselines) {
|
|
920
|
+
const names = new Set();
|
|
921
|
+
const addLines = (output) => {
|
|
922
|
+
for (const rel of String(output || '').split('\n').map(line => line.trim()).filter(Boolean)) {
|
|
923
|
+
names.add(rel);
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
addLines(this._gitOutput(baseline.root, ['diff', '--name-only'], 10000));
|
|
927
|
+
addLines(this._gitOutput(baseline.root, ['diff', '--name-only', '--cached'], 10000));
|
|
928
|
+
addLines(this._gitOutput(baseline.root, ['ls-files', '--others', '--exclude-standard'], 10000));
|
|
929
|
+
for (const pathName of this._parseGitStatusPaths(this._gitStatusPorcelain(baseline.root, 10000))) {
|
|
930
|
+
names.add(pathName);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const initialDirty = new Set(baseline.initialDirtyPaths || []);
|
|
934
|
+
const files = [...names]
|
|
935
|
+
.filter(rel => !initialDirty.has(rel))
|
|
936
|
+
.filter(rel => this._scopeAllowsChangedPath(baseline, rel, allowedSlugs, pageOnly, userMessage))
|
|
937
|
+
.filter(rel => !this._gitPathExistsAtRef(baseline.root, baseline.head || 'HEAD', rel))
|
|
938
|
+
.sort();
|
|
939
|
+
if (files.length === 0) continue;
|
|
940
|
+
|
|
941
|
+
this._gitRun(baseline.root, ['restore', '--staged', '--', ...files], 10000);
|
|
942
|
+
const untracked = files.filter(file => this._gitOutput(baseline.root, ['ls-files', '--others', '--exclude-standard', '--', file], 5000));
|
|
943
|
+
if (untracked.length > 0) {
|
|
944
|
+
this._gitRun(baseline.root, ['clean', '-fd', '--', ...untracked], 10000);
|
|
945
|
+
}
|
|
946
|
+
restored.push({ repo: baseline.root, files: files.slice(0, 12), restoredCount: files.length });
|
|
947
|
+
}
|
|
948
|
+
return restored;
|
|
949
|
+
}
|
|
950
|
+
|
|
893
951
|
_formatScopeDriftRestoreNudge(restored) {
|
|
894
952
|
const lines = restored.slice(0, 5).flatMap(w => [
|
|
895
953
|
`- ${w.repo}: restored ${w.restoredCount} out-of-scope file(s); allowed scope tokens: ${w.allowedSlugs.join(', ')}${w.pageOnly ? ' (page/source files only)' : ''}`,
|
|
@@ -1294,6 +1352,19 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
1294
1352
|
].join('\n');
|
|
1295
1353
|
}
|
|
1296
1354
|
|
|
1355
|
+
_formatGeneratedScopedUiResetNudge(restored) {
|
|
1356
|
+
if (!Array.isArray(restored) || restored.length === 0) return '';
|
|
1357
|
+
const lines = restored.slice(0, 5).flatMap(item => [
|
|
1358
|
+
`- ${item.repo}: removed ${item.restoredCount} rejected generated scoped file(s) before retrying`,
|
|
1359
|
+
...item.files.map(file => ` ${file}`),
|
|
1360
|
+
]);
|
|
1361
|
+
return [
|
|
1362
|
+
'AgentForge removed the rejected generated target file(s) before this retry.',
|
|
1363
|
+
...lines,
|
|
1364
|
+
'Treat those removed files as failed output, not source material. Rebuild the requested target page-owned file(s) from inspected project context and comparable existing pages, then verify the rebuilt pages visually.',
|
|
1365
|
+
].join('\n');
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1297
1368
|
_findUiImplementationArtifactChanges(repoBaselines, userMessage) {
|
|
1298
1369
|
if (!this._isBroadUiQualityTask(userMessage)) return [];
|
|
1299
1370
|
if (!Array.isArray(repoBaselines) || repoBaselines.length === 0) return [];
|
|
@@ -3469,6 +3540,20 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
3469
3540
|
// generic discovery loop after the user has steered the active task.
|
|
3470
3541
|
const withTaskContext = (message) => activeGuidePrefix() + retryContextPrefix + taskContextPrefix + message;
|
|
3471
3542
|
finalMessage = withTaskContext(finalMessage);
|
|
3543
|
+
const applyGuideToSemanticScope = (guideText) => {
|
|
3544
|
+
const text = String(guideText || '').trim();
|
|
3545
|
+
if (!text) return;
|
|
3546
|
+
const before = this._extractExplicitScope(scopeAwareUserMessage);
|
|
3547
|
+
const merged = `${scopeAwareUserMessage}\n\n[Active user guide]\n${text}`;
|
|
3548
|
+
const after = this._extractExplicitScope(merged);
|
|
3549
|
+
if (after.slugs.length === 0) return;
|
|
3550
|
+
scopeAwareUserMessage = merged;
|
|
3551
|
+
const beforeKey = `${before.slugs.join(',')}|${before.pageOnly}`;
|
|
3552
|
+
const afterKey = `${after.slugs.join(',')}|${after.pageOnly}`;
|
|
3553
|
+
if (beforeKey !== afterKey) {
|
|
3554
|
+
agentLog(`[${taskId}] 🎯 Updated active scope from guide: ${after.slugs.join(', ')}${after.pageOnly ? ' (page/source files only)' : ''}`);
|
|
3555
|
+
}
|
|
3556
|
+
};
|
|
3472
3557
|
|
|
3473
3558
|
// If conversation history was loaded from DB (e.g. session expired, worker restarted,
|
|
3474
3559
|
// or user returning hours later), prepend it so the agent has full context.
|
|
@@ -3794,6 +3879,7 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
3794
3879
|
nudgeCount = 0;
|
|
3795
3880
|
uiRepairNudgeCount = 0;
|
|
3796
3881
|
uiVerificationRetryCount = 0;
|
|
3882
|
+
applyGuideToSemanticScope(preRunGuide.text);
|
|
3797
3883
|
agentLog(`[${taskId}] 🧭 Applying ${preRunGuide.count} guide note(s) before iteration ${iteration}`);
|
|
3798
3884
|
this.send({ type: 'task_progress', taskId, agentId, roomId, output: 'Guide received; applying it to the current task...' });
|
|
3799
3885
|
iterationMessage = `${preRunGuide.text}\n\n${iterationMessage}`;
|
|
@@ -4112,6 +4198,7 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4112
4198
|
nudgeCount = 0;
|
|
4113
4199
|
uiRepairNudgeCount = 0;
|
|
4114
4200
|
uiVerificationRetryCount = 0;
|
|
4201
|
+
applyGuideToSemanticScope(postRunGuide.text);
|
|
4115
4202
|
agentLog(`[${taskId}] 🧭 Applying ${postRunGuide.count} guide note(s) after iteration ${iteration}`);
|
|
4116
4203
|
this.send({ type: 'task_progress', taskId, agentId, roomId, output: 'Guide received; continuing with the updated direction...' });
|
|
4117
4204
|
iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${postRunGuide.text}\n\nContinue with the active task using the guide as the newest user direction. Preserve useful work and context only when it does not conflict with the guide.`);
|
|
@@ -4559,9 +4646,15 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4559
4646
|
throw new Error('UI task failed visual verification after repeated repair attempts');
|
|
4560
4647
|
}
|
|
4561
4648
|
const repairBudget = consumeUiRepairNudge('visual verification warnings', visualVerificationFailureNudge);
|
|
4649
|
+
const generatedResetNudge = this._formatGeneratedScopedUiResetNudge(
|
|
4650
|
+
this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage)
|
|
4651
|
+
);
|
|
4562
4652
|
nudgeCount = 0;
|
|
4563
4653
|
console.log(`[${taskId}] UI task visual verification still reported visible issues — retrying (${uiVerificationRetryCount}/${UI_REPAIR_NUDGE_LIMIT}, total UI repairs ${repairBudget})`);
|
|
4564
|
-
|
|
4654
|
+
const retryInstruction = generatedResetNudge
|
|
4655
|
+
? 'Rebuild the removed target page file(s) from already-inspected project context and comparable existing pages. Do not reuse the rejected generated page as the basis for the next attempt. Fix the visible issues reported by local browser verification, reopen each edited target screen locally after the final edit, and only then end with ✓ TASK_COMPLETE.'
|
|
4656
|
+
: 'Continue from the current changed files and latest browser evidence. Fix the visible issues reported by the local browser verification, reopen each edited target screen locally after the final edit, and only then end with ✓ TASK_COMPLETE.';
|
|
4657
|
+
iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${[visualVerificationFailureNudge, generatedResetNudge].filter(Boolean).join('\n\n')}\n\n${retryInstruction}`);
|
|
4565
4658
|
} else if (hasMissingLocalUiVerification) {
|
|
4566
4659
|
uiVerificationRetryCount++;
|
|
4567
4660
|
const uiVerificationFailureDetails = this._extractUiVerificationFailureDetails(output);
|
|
@@ -4570,9 +4663,15 @@ export class AgentForgeWorker extends EventEmitter {
|
|
|
4570
4663
|
throw new Error('UI task failed visual verification after repeated repair attempts');
|
|
4571
4664
|
}
|
|
4572
4665
|
const repairBudget = consumeUiRepairNudge('missing local visual verification', uiVerificationFailureDetails);
|
|
4666
|
+
const generatedResetNudge = this._formatGeneratedScopedUiResetNudge(
|
|
4667
|
+
this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage)
|
|
4668
|
+
);
|
|
4573
4669
|
nudgeCount = 0;
|
|
4574
4670
|
console.log(`[${taskId}] UI task missing local visual verification — retrying with local-app repair instruction (${uiVerificationRetryCount}/${UI_REPAIR_NUDGE_LIMIT}, total UI repairs ${repairBudget})`);
|
|
4575
|
-
|
|
4671
|
+
const retryInstruction = generatedResetNudge
|
|
4672
|
+
? 'Rebuild the removed target page file(s) from already-inspected project context and comparable existing pages. Do not reuse the rejected generated page as the basis for the next attempt.'
|
|
4673
|
+
: 'Continue from the current changed files and already-inspected project context. Do not restart from scratch, repeat initial delete/reset/setup steps, discard existing progress, or reread site indexes, shared/global CSS, header/footer partials, or unrelated reference pages unless a specific verification issue requires it.';
|
|
4674
|
+
iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\nYour edits are not complete because the changed UI was not successfully loaded and inspected cleanly in its actual local app URL.${uiVerificationFailureDetails ? `\n\nSpecific verification issue(s):\n${uiVerificationFailureDetails}` : ''}${generatedResetNudge ? `\n\n${generatedResetNudge}` : ''}\n\n${retryInstruction} Do not use external sites, site indexes, listing indexes, shared style files, or reference pages as final verification for scoped UI edits. Start or repair the project's local dev/static server if needed, use the port the project actually declares, open the real localhost/127.0.0.1 URL for each edited target screen, inspect the changed screen, fix visible issues, and only then end with ✓ TASK_COMPLETE.`);
|
|
4576
4675
|
} else if (hasIncompleteTurn) {
|
|
4577
4676
|
// openclaw reported an incomplete turn (payloads=0) after the agent used a tool.
|
|
4578
4677
|
// This is a tool timeout, not a narration. Reset nudgeCount and give a targeted retry hint.
|