@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 +1 -1
- package/scripts/check-task-semantics.js +34 -2
- package/src/OpenClawCLI.js +135 -4
- package/src/worker.js +129 -8
package/package.json
CHANGED
|
@@ -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
|
|
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(
|
package/src/OpenClawCLI.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
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
|
|
814
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|