@hamp10/agentforge 0.2.31 → 0.2.33

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hamp10/agentforge",
3
- "version": "0.2.31",
3
+ "version": "0.2.33",
4
4
  "description": "AgentForge worker — connect your machine to agentforge.ai",
5
5
  "type": "module",
6
6
  "bin": {
@@ -437,6 +437,15 @@ try {
437
437
  );
438
438
  rmSync(sharedCssFile, { force: true });
439
439
  writeFileSync(alphaFixturePath, alphaFixtureCurrent);
440
+ const wrongRouteDir = path.join(fixture.repo, 'public_html', 'alpha');
441
+ mkdirSync(wrongRouteDir, { recursive: true });
442
+ writeFileSync(path.join(wrongRouteDir, 'index.html'), '<!doctype html><html><body><h1>Alpha</h1></body></html>');
443
+ assert.match(
444
+ worker._formatScopeDriftNudge(worker._findScopeDriftRepoChanges(baseline, message)),
445
+ /public_html\/alpha\/index\.html/i,
446
+ 'new same-scope route folders should be reported as out-of-scope when the project already has a page collection'
447
+ );
448
+ rmSync(wrongRouteDir, { recursive: true, force: true });
440
449
  const directOldHtml = '<!doctype html><html><body><main><section class="hero"><h1>Alpha</h1><p>Existing body copy.</p></section></main></body></html>';
441
450
  const directInlineStyleHtml = '<!doctype html><html><body><main><section class="hero" style="padding: 4rem; color: white;"><h1>Alpha</h1><p>Existing body copy.</p></section></main></body></html>';
442
451
  assert.throws(
@@ -552,9 +561,18 @@ try {
552
561
  fixture.repo,
553
562
  { task: 'Work on the listing page for Alpha. Only change that listing page.' }
554
563
  ),
555
- /existing scoped page target/i,
564
+ /existing (?:scoped page target|page collection)/i,
556
565
  'scoped page work should not create a new same-slug page when an existing scoped target page is present'
557
566
  );
567
+ assert.throws(
568
+ () => cli._guardDirectFileWritePath(
569
+ path.join(fixture.repo, 'public_html', 'alpha', 'index.html'),
570
+ fixture.repo,
571
+ { task: message }
572
+ ),
573
+ /existing page collection/i,
574
+ 'scoped page work should not create a route folder when the project has an existing page collection pattern'
575
+ );
558
576
  const nestedDuplicateGamma = path.join(fixture.repo, path.basename(fixture.repo), 'public_html', 'domains', 'gamma.html');
559
577
  mkdirSync(path.dirname(nestedDuplicateGamma), { recursive: true });
560
578
  writeFileSync(nestedDuplicateGamma, '<!doctype html><html><body><h1>Gamma</h1></body></html>');
@@ -663,14 +681,28 @@ try {
663
681
  ),
664
682
  'scoped shell sed should ignore the substitution script and allow canonical target page edits'
665
683
  );
684
+ assert.throws(
685
+ () => cli._guardDirectBashCommand(
686
+ 'touch public_html/css/domain-detail.css',
687
+ fixture.repo,
688
+ { task: message }
689
+ ),
690
+ /outside the requested task scope/i,
691
+ 'direct scoped work should reject new generic CSS assets before the target page owns or links them'
692
+ );
693
+ writeFileSync(
694
+ alphaFixturePath,
695
+ alphaFixtureCurrent.replace('</head>', '<link rel="stylesheet" href="../css/domain-detail.css">\n</head>')
696
+ );
666
697
  assert.doesNotThrow(
667
698
  () => cli._guardDirectBashCommand(
668
699
  'touch public_html/css/domain-detail.css',
669
700
  fixture.repo,
670
701
  { task: message }
671
702
  ),
672
- 'direct scoped work should allow creating a new page-owned CSS asset before the target page links it'
703
+ 'direct scoped work should allow generic CSS assets after they are linked only by scoped pages'
673
704
  );
705
+ writeFileSync(alphaFixturePath, alphaFixtureCurrent);
674
706
  const staleLinkedCssHtml = '<!doctype html><html><head><link rel="stylesheet" href="css/alpha.css"></head><body><main><section><h1>Alpha</h1><p>Fresh page body copy with rebuilt structure.</p></section></main></body></html>';
675
707
  assert.throws(
676
708
  () => cli._validateDirectUiFileContent(
@@ -617,12 +617,65 @@ export class OpenClawCLI extends EventEmitter {
617
617
  return /\.(?:html?|xhtml|astro|mdx?)$/i.test(String(relativePath || ''));
618
618
  }
619
619
 
620
- _isDirectNewPageOwnedAssetPath(filePath, workDir, relativePath, pageOnly) {
620
+ _directFileMentionsAsset(content, sourceRel, assetRel) {
621
+ const source = String(content || '');
622
+ const assetUnix = String(assetRel || '').replace(/\\/g, '/');
623
+ const assetBase = path.basename(assetUnix);
624
+ const relativeFromSource = path.relative(path.dirname(sourceRel || ''), assetRel || '').replace(/\\/g, '/');
625
+ const candidates = [
626
+ assetBase,
627
+ assetUnix,
628
+ `/${assetUnix}`,
629
+ relativeFromSource,
630
+ relativeFromSource && !relativeFromSource.startsWith('.') ? `./${relativeFromSource}` : '',
631
+ ].filter(Boolean);
632
+ return candidates.some(candidate => source.includes(candidate));
633
+ }
634
+
635
+ _directCurrentPageSourceFiles(workDir) {
636
+ const names = new Set();
637
+ const addLines = (output) => {
638
+ for (const rel of String(output || '').split('\n').map(line => line.trim()).filter(Boolean)) {
639
+ if (this._isDirectPageSourcePath(rel)) names.add(rel);
640
+ }
641
+ };
642
+ try {
643
+ addLines(execFileSync('git', ['-C', workDir, 'ls-files'], {
644
+ encoding: 'utf-8',
645
+ stdio: ['ignore', 'pipe', 'ignore'],
646
+ }));
647
+ addLines(execFileSync('git', ['-C', workDir, 'ls-files', '--others', '--exclude-standard'], {
648
+ encoding: 'utf-8',
649
+ stdio: ['ignore', 'pipe', 'ignore'],
650
+ }));
651
+ } catch {}
652
+ return [...names].sort();
653
+ }
654
+
655
+ _isDirectNewPageOwnedAssetPath(filePath, workDir, relativePath, pageOnly, slugs = []) {
621
656
  if (!pageOnly || !this._isDirectPageAssetPath(relativePath)) return false;
622
- if (existsSync(filePath)) return false;
623
657
  const relative = path.relative(workDir, filePath);
624
658
  if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return false;
625
- return true;
659
+ if (this._directScopeSlugsMatchingText(String(relativePath || '').toLowerCase(), slugs).length > 0) return true;
660
+
661
+ const sourceFiles = this._directCurrentPageSourceFiles(workDir).filter(file => file !== relativePath);
662
+ const scopedSources = [];
663
+ const outOfScopeMentions = [];
664
+ for (const sourceRel of sourceFiles) {
665
+ let content = '';
666
+ try {
667
+ content = readFileSync(path.join(workDir, sourceRel), 'utf-8');
668
+ } catch {
669
+ continue;
670
+ }
671
+ if (!this._directFileMentionsAsset(content, sourceRel, relativePath)) continue;
672
+ if (this._directScopeSlugsMatchingText(String(sourceRel || '').toLowerCase(), slugs).length > 0) {
673
+ scopedSources.push(sourceRel);
674
+ } else {
675
+ outOfScopeMentions.push(sourceRel);
676
+ }
677
+ }
678
+ return scopedSources.length > 0 && outOfScopeMentions.length === 0;
626
679
  }
627
680
 
628
681
  _isDirectScopedReferenceSourcePath(filePath, workDir, relativePath, slugs, pageOnly, task) {
@@ -659,11 +712,71 @@ export class OpenClawCLI extends EventEmitter {
659
712
  const slugAllowed = this._directScopeSlugsMatchingText(lower, slugs).length > 0;
660
713
  if (!slugAllowed) {
661
714
  if (this._isDirectScopedReferenceSourcePath(filePath, workDir, scopePath, slugs, pageOnly, task)) return true;
662
- return this._isDirectNewPageOwnedAssetPath(filePath, workDir, scopePath, pageOnly);
715
+ return this._isDirectNewPageOwnedAssetPath(filePath, workDir, scopePath, pageOnly, slugs);
663
716
  }
664
717
  return !pageOnly || this._isDirectPageSourcePath(scopePath);
665
718
  }
666
719
 
720
+ _directHtmlPageCollectionDirs(workDir) {
721
+ if (!workDir) return [];
722
+ const counts = new Map();
723
+ try {
724
+ const output = execFileSync('git', ['-C', workDir, 'ls-files'], {
725
+ encoding: 'utf-8',
726
+ stdio: ['ignore', 'pipe', 'ignore'],
727
+ });
728
+ for (const rel of String(output || '').split('\n').map(line => line.trim()).filter(Boolean)) {
729
+ if (!this._isDirectHtmlPagePath(rel)) continue;
730
+ const absolute = path.resolve(workDir, rel);
731
+ if (this._nestedProjectCopyCanonicalPath(absolute, workDir)) continue;
732
+ const dir = path.dirname(rel);
733
+ if (!dir || dir === '.') continue;
734
+ counts.set(dir, (counts.get(dir) || 0) + 1);
735
+ }
736
+ } catch {
737
+ return [];
738
+ }
739
+ const dirs = [...counts.entries()]
740
+ .filter(([, count]) => count >= 2)
741
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
742
+ .map(([dir]) => dir);
743
+ return dirs.filter(dir => !dirs.some(other => other !== dir && this._directPathInsideRelDir(other, dir)));
744
+ }
745
+
746
+ _directPathInsideRelDir(relativePath, dir) {
747
+ const rel = path.relative(path.normalize(dir), path.normalize(relativePath));
748
+ return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
749
+ }
750
+
751
+ _directNewHtmlPageCollectionViolation(filePath, workDir, slugs, pageOnly, relativePath) {
752
+ if (!pageOnly || !Array.isArray(slugs) || slugs.length === 0) return null;
753
+ if (!this._isDirectHtmlPagePath(relativePath)) return null;
754
+ const lowerRelativePath = String(relativePath || '').toLowerCase();
755
+ const matchedSlugs = this._directScopeSlugsMatchingText(lowerRelativePath, slugs);
756
+ if (matchedSlugs.length === 0) return null;
757
+ if (this._gitTracksExactPath(workDir, relativePath)) return null;
758
+
759
+ const collectionDirs = this._directHtmlPageCollectionDirs(workDir);
760
+ if (collectionDirs.length === 0) return null;
761
+ if (collectionDirs.some(dir => this._directPathInsideRelDir(relativePath, dir))) return null;
762
+
763
+ const candidatePaths = [];
764
+ for (const dir of collectionDirs.slice(0, 4)) {
765
+ for (const slug of matchedSlugs) {
766
+ const aliases = new Set([slug]);
767
+ const withoutSuffix = slug.replace(/-(?:ai|app|co|com|dev|gg|io|net|org|so|xyz)$/i, '');
768
+ if (withoutSuffix && withoutSuffix !== slug && withoutSuffix.length >= 3) aliases.add(withoutSuffix);
769
+ for (const alias of aliases) candidatePaths.push(path.join(dir, `${alias}.html`));
770
+ }
771
+ }
772
+
773
+ return {
774
+ requested: relativePath,
775
+ collectionDirs,
776
+ candidatePaths: [...new Set(candidatePaths)].slice(0, 8),
777
+ };
778
+ }
779
+
667
780
  _directScopeFileCandidates(workDir, slugs, maxResults = 8) {
668
781
  if (!workDir || !Array.isArray(slugs) || slugs.length === 0) return [];
669
782
  const results = [];
@@ -852,6 +965,24 @@ export class OpenClawCLI extends EventEmitter {
852
965
  if (!this._directTaskScopeAllowsPath(filePath, workDir, options.task)) {
853
966
  throw this._directScopeViolationError(this._formatDirectScopeError(filePath, workDir, options.task));
854
967
  }
968
+ const collectionViolation = this._directNewHtmlPageCollectionViolation(
969
+ filePath,
970
+ workDir,
971
+ slugs,
972
+ pageOnly,
973
+ relativePath
974
+ );
975
+ if (collectionViolation) {
976
+ throw this._directScopeViolationError([
977
+ 'Refusing to create a new route/page path outside the project\'s existing page collection for this scoped page task.',
978
+ `Requested new page: ${collectionViolation.requested}`,
979
+ `Existing page collection director${collectionViolation.collectionDirs.length === 1 ? 'y' : 'ies'}: ${collectionViolation.collectionDirs.slice(0, 6).join(', ')}`,
980
+ collectionViolation.candidatePaths.length > 0
981
+ ? `Likely target path(s): ${collectionViolation.candidatePaths.join(', ')}`
982
+ : '',
983
+ 'Use the project\'s existing page collection pattern instead of inventing a sibling URL or route folder.',
984
+ ].filter(Boolean).join(' '));
985
+ }
855
986
  if (pageOnly && slugs.length > 0 && this._isDirectHtmlPagePath(relativePath)) {
856
987
  const matchedSlugs = this._directScopeSlugsMatchingText(lowerRelativePath, slugs);
857
988
  const isTrackedTarget = this._gitTracksExactPath(workDir, relativePath);
package/src/worker.js CHANGED
@@ -646,6 +646,47 @@ export class AgentForgeWorker extends EventEmitter {
646
646
  return /\.(css|scss|sass|less|jsx?|tsx?)$/i.test(String(relativePath || ''));
647
647
  }
648
648
 
649
+ _isHtmlPagePath(relativePath) {
650
+ return /\.(?:html?|xhtml|astro|mdx?)$/i.test(String(relativePath || ''));
651
+ }
652
+
653
+ _htmlPageCollectionDirs(repo, ref = 'HEAD') {
654
+ if (!repo) return [];
655
+ const counts = new Map();
656
+ const output = this._gitOutput(repo, ['ls-tree', '-r', '--name-only', ref || 'HEAD'], 50000)
657
+ || this._gitOutput(repo, ['ls-files'], 50000);
658
+ for (const rel of String(output || '').split('\n').map(line => line.trim()).filter(Boolean)) {
659
+ if (!this._isHtmlPagePath(rel)) continue;
660
+ const parts = rel.split(/[\\/]+/).filter(Boolean);
661
+ if (parts.length >= 2 && parts[0].toLowerCase() === path.basename(repo).toLowerCase()) continue;
662
+ const dir = path.dirname(rel);
663
+ if (!dir || dir === '.') continue;
664
+ counts.set(dir, (counts.get(dir) || 0) + 1);
665
+ }
666
+ const dirs = [...counts.entries()]
667
+ .filter(([, count]) => count >= 2)
668
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
669
+ .map(([dir]) => dir);
670
+ return dirs.filter(dir => !dirs.some(other => other !== dir && this._pathInsideRelDir(other, dir)));
671
+ }
672
+
673
+ _pathInsideRelDir(relativePath, dir) {
674
+ const rel = path.relative(path.normalize(dir), path.normalize(relativePath));
675
+ return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
676
+ }
677
+
678
+ _isNewHtmlPageOutsideExistingCollection(baseline, rel, allowedSlugs, pageOnly) {
679
+ if (!baseline?.root || !pageOnly || !this._isHtmlPagePath(rel)) return false;
680
+ if (!Array.isArray(allowedSlugs) || allowedSlugs.length === 0) return false;
681
+ const lower = String(rel || '').toLowerCase();
682
+ if (this._scopeSlugsMatchingText(lower, allowedSlugs).length === 0) return false;
683
+ if (this._gitPathExistsAtRef(baseline.root, baseline.head || 'HEAD', rel)) return false;
684
+
685
+ const collectionDirs = this._htmlPageCollectionDirs(baseline.root, baseline.head || 'HEAD');
686
+ if (collectionDirs.length === 0) return false;
687
+ return !collectionDirs.some(dir => this._pathInsideRelDir(rel, dir));
688
+ }
689
+
649
690
  _fileMentionsAsset(content, sourceRel, assetRel) {
650
691
  const source = String(content || '');
651
692
  const assetUnix = String(assetRel || '').replace(/\\/g, '/');
@@ -723,7 +764,10 @@ export class AgentForgeWorker extends EventEmitter {
723
764
  _scopeAllowsChangedPath(baseline, rel, allowedSlugs, pageOnly, userMessage = '') {
724
765
  const lower = String(rel || '').toLowerCase();
725
766
  const slugAllowed = this._scopeSlugsMatchingText(lower, allowedSlugs).length > 0;
726
- if (slugAllowed) return !pageOnly || this._isPageSourcePath(rel);
767
+ if (slugAllowed) {
768
+ if (this._isNewHtmlPageOutsideExistingCollection(baseline, rel, allowedSlugs, pageOnly)) return false;
769
+ return !pageOnly || this._isPageSourcePath(rel);
770
+ }
727
771
  if (this._isScopedReferenceSourcePath(baseline, rel, allowedSlugs, pageOnly, userMessage)) return true;
728
772
  return this._isNewScopedPageOwnedAsset(baseline, rel, allowedSlugs, pageOnly);
729
773
  }
@@ -810,11 +854,8 @@ export class AgentForgeWorker extends EventEmitter {
810
854
  addLines(this._gitOutput(baseline.root, ['diff', '--name-only'], 10000));
811
855
  addLines(this._gitOutput(baseline.root, ['diff', '--name-only', '--cached'], 10000));
812
856
  addLines(this._gitOutput(baseline.root, ['ls-files', '--others', '--exclude-standard'], 10000));
813
- const status = this._gitStatusPorcelain(baseline.root, 10000);
814
- for (const rawLine of String(status || '').split('\n').filter(Boolean)) {
815
- const rel = rawLine.length >= 3 && rawLine[2] === ' ' ? rawLine.slice(3) : rawLine.trim().replace(/^..\s+/, '');
816
- const pathName = rel.includes(' -> ') ? rel.split(' -> ').pop() : rel;
817
- if (pathName) names.add(pathName);
857
+ for (const pathName of this._parseGitStatusPaths(this._gitStatusPorcelain(baseline.root, 10000))) {
858
+ names.add(pathName);
818
859
  }
819
860
 
820
861
  const initialDirty = new Set(baseline.initialDirtyPaths || []);
@@ -846,6 +887,45 @@ export class AgentForgeWorker extends EventEmitter {
846
887
  return restored;
847
888
  }
848
889
 
890
+ _restoreGeneratedScopedUiTargets(repoBaselines, userMessage) {
891
+ if (!this._isBroadUiQualityTask(userMessage)) return [];
892
+ if (!Array.isArray(repoBaselines) || repoBaselines.length === 0) return [];
893
+ const { slugs: allowedSlugs, pageOnly } = this._extractExplicitScope(userMessage);
894
+ if (allowedSlugs.length === 0 || !pageOnly) return [];
895
+
896
+ const restored = [];
897
+ for (const baseline of repoBaselines) {
898
+ const names = new Set();
899
+ const addLines = (output) => {
900
+ for (const rel of String(output || '').split('\n').map(line => line.trim()).filter(Boolean)) {
901
+ names.add(rel);
902
+ }
903
+ };
904
+ addLines(this._gitOutput(baseline.root, ['diff', '--name-only'], 10000));
905
+ addLines(this._gitOutput(baseline.root, ['diff', '--name-only', '--cached'], 10000));
906
+ addLines(this._gitOutput(baseline.root, ['ls-files', '--others', '--exclude-standard'], 10000));
907
+ for (const pathName of this._parseGitStatusPaths(this._gitStatusPorcelain(baseline.root, 10000))) {
908
+ names.add(pathName);
909
+ }
910
+
911
+ const initialDirty = new Set(baseline.initialDirtyPaths || []);
912
+ const files = [...names]
913
+ .filter(rel => !initialDirty.has(rel))
914
+ .filter(rel => this._scopeAllowsChangedPath(baseline, rel, allowedSlugs, pageOnly, userMessage))
915
+ .filter(rel => !this._gitPathExistsAtRef(baseline.root, baseline.head || 'HEAD', rel))
916
+ .sort();
917
+ if (files.length === 0) continue;
918
+
919
+ this._gitRun(baseline.root, ['restore', '--staged', '--', ...files], 10000);
920
+ const untracked = files.filter(file => this._gitOutput(baseline.root, ['ls-files', '--others', '--exclude-standard', '--', file], 5000));
921
+ if (untracked.length > 0) {
922
+ this._gitRun(baseline.root, ['clean', '-fd', '--', ...untracked], 10000);
923
+ }
924
+ restored.push({ repo: baseline.root, files: files.slice(0, 12), restoredCount: files.length });
925
+ }
926
+ return restored;
927
+ }
928
+
849
929
  _formatScopeDriftRestoreNudge(restored) {
850
930
  const lines = restored.slice(0, 5).flatMap(w => [
851
931
  `- ${w.repo}: restored ${w.restoredCount} out-of-scope file(s); allowed scope tokens: ${w.allowedSlugs.join(', ')}${w.pageOnly ? ' (page/source files only)' : ''}`,
@@ -1250,6 +1330,19 @@ export class AgentForgeWorker extends EventEmitter {
1250
1330
  ].join('\n');
1251
1331
  }
1252
1332
 
1333
+ _formatGeneratedScopedUiResetNudge(restored) {
1334
+ if (!Array.isArray(restored) || restored.length === 0) return '';
1335
+ const lines = restored.slice(0, 5).flatMap(item => [
1336
+ `- ${item.repo}: removed ${item.restoredCount} rejected generated scoped file(s) before retrying`,
1337
+ ...item.files.map(file => ` ${file}`),
1338
+ ]);
1339
+ return [
1340
+ 'AgentForge removed the rejected generated target file(s) before this retry.',
1341
+ ...lines,
1342
+ '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.',
1343
+ ].join('\n');
1344
+ }
1345
+
1253
1346
  _findUiImplementationArtifactChanges(repoBaselines, userMessage) {
1254
1347
  if (!this._isBroadUiQualityTask(userMessage)) return [];
1255
1348
  if (!Array.isArray(repoBaselines) || repoBaselines.length === 0) return [];
@@ -3425,6 +3518,20 @@ export class AgentForgeWorker extends EventEmitter {
3425
3518
  // generic discovery loop after the user has steered the active task.
3426
3519
  const withTaskContext = (message) => activeGuidePrefix() + retryContextPrefix + taskContextPrefix + message;
3427
3520
  finalMessage = withTaskContext(finalMessage);
3521
+ const applyGuideToSemanticScope = (guideText) => {
3522
+ const text = String(guideText || '').trim();
3523
+ if (!text) return;
3524
+ const before = this._extractExplicitScope(scopeAwareUserMessage);
3525
+ const merged = `${scopeAwareUserMessage}\n\n[Active user guide]\n${text}`;
3526
+ const after = this._extractExplicitScope(merged);
3527
+ if (after.slugs.length === 0) return;
3528
+ scopeAwareUserMessage = merged;
3529
+ const beforeKey = `${before.slugs.join(',')}|${before.pageOnly}`;
3530
+ const afterKey = `${after.slugs.join(',')}|${after.pageOnly}`;
3531
+ if (beforeKey !== afterKey) {
3532
+ agentLog(`[${taskId}] 🎯 Updated active scope from guide: ${after.slugs.join(', ')}${after.pageOnly ? ' (page/source files only)' : ''}`);
3533
+ }
3534
+ };
3428
3535
 
3429
3536
  // If conversation history was loaded from DB (e.g. session expired, worker restarted,
3430
3537
  // or user returning hours later), prepend it so the agent has full context.
@@ -3750,6 +3857,7 @@ export class AgentForgeWorker extends EventEmitter {
3750
3857
  nudgeCount = 0;
3751
3858
  uiRepairNudgeCount = 0;
3752
3859
  uiVerificationRetryCount = 0;
3860
+ applyGuideToSemanticScope(preRunGuide.text);
3753
3861
  agentLog(`[${taskId}] 🧭 Applying ${preRunGuide.count} guide note(s) before iteration ${iteration}`);
3754
3862
  this.send({ type: 'task_progress', taskId, agentId, roomId, output: 'Guide received; applying it to the current task...' });
3755
3863
  iterationMessage = `${preRunGuide.text}\n\n${iterationMessage}`;
@@ -4068,6 +4176,7 @@ export class AgentForgeWorker extends EventEmitter {
4068
4176
  nudgeCount = 0;
4069
4177
  uiRepairNudgeCount = 0;
4070
4178
  uiVerificationRetryCount = 0;
4179
+ applyGuideToSemanticScope(postRunGuide.text);
4071
4180
  agentLog(`[${taskId}] 🧭 Applying ${postRunGuide.count} guide note(s) after iteration ${iteration}`);
4072
4181
  this.send({ type: 'task_progress', taskId, agentId, roomId, output: 'Guide received; continuing with the updated direction...' });
4073
4182
  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.`);
@@ -4515,9 +4624,15 @@ export class AgentForgeWorker extends EventEmitter {
4515
4624
  throw new Error('UI task failed visual verification after repeated repair attempts');
4516
4625
  }
4517
4626
  const repairBudget = consumeUiRepairNudge('visual verification warnings', visualVerificationFailureNudge);
4627
+ const generatedResetNudge = this._formatGeneratedScopedUiResetNudge(
4628
+ this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage)
4629
+ );
4518
4630
  nudgeCount = 0;
4519
4631
  console.log(`[${taskId}] UI task visual verification still reported visible issues — retrying (${uiVerificationRetryCount}/${UI_REPAIR_NUDGE_LIMIT}, total UI repairs ${repairBudget})`);
4520
- iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${visualVerificationFailureNudge}\n\nContinue from the current changed files and latest browser evidence. Do not restart from scratch. 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.`);
4632
+ const retryInstruction = generatedResetNudge
4633
+ ? '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.'
4634
+ : '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.';
4635
+ iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${[visualVerificationFailureNudge, generatedResetNudge].filter(Boolean).join('\n\n')}\n\n${retryInstruction}`);
4521
4636
  } else if (hasMissingLocalUiVerification) {
4522
4637
  uiVerificationRetryCount++;
4523
4638
  const uiVerificationFailureDetails = this._extractUiVerificationFailureDetails(output);
@@ -4526,9 +4641,15 @@ export class AgentForgeWorker extends EventEmitter {
4526
4641
  throw new Error('UI task failed visual verification after repeated repair attempts');
4527
4642
  }
4528
4643
  const repairBudget = consumeUiRepairNudge('missing local visual verification', uiVerificationFailureDetails);
4644
+ const generatedResetNudge = this._formatGeneratedScopedUiResetNudge(
4645
+ this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage)
4646
+ );
4529
4647
  nudgeCount = 0;
4530
4648
  console.log(`[${taskId}] UI task missing local visual verification — retrying with local-app repair instruction (${uiVerificationRetryCount}/${UI_REPAIR_NUDGE_LIMIT}, total UI repairs ${repairBudget})`);
4531
- 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}` : ''}\n\nContinue 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. 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.`);
4649
+ const retryInstruction = generatedResetNudge
4650
+ ? '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.'
4651
+ : '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.';
4652
+ 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.`);
4532
4653
  } else if (hasIncompleteTurn) {
4533
4654
  // openclaw reported an incomplete turn (payloads=0) after the agent used a tool.
4534
4655
  // This is a tool timeout, not a narration. Reset nudgeCount and give a targeted retry hint.