@hamp10/agentforge 0.2.31 → 0.2.32
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 +45 -1
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
|
}
|