@hamp10/agentforge 0.2.30 → 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 +50 -2
- package/src/OpenClawCLI.js +139 -5
- 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,34 @@ 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
|
+
);
|
|
576
|
+
const nestedDuplicateGamma = path.join(fixture.repo, path.basename(fixture.repo), 'public_html', 'domains', 'gamma.html');
|
|
577
|
+
mkdirSync(path.dirname(nestedDuplicateGamma), { recursive: true });
|
|
578
|
+
writeFileSync(nestedDuplicateGamma, '<!doctype html><html><body><h1>Gamma</h1></body></html>');
|
|
579
|
+
assert.doesNotThrow(
|
|
580
|
+
() => cli._guardDirectFileWritePath(
|
|
581
|
+
path.join(fixture.repo, 'public_html', 'domains', 'gamma.html'),
|
|
582
|
+
fixture.repo,
|
|
583
|
+
{ task: 'Can you make listing pages for Gamma?' }
|
|
584
|
+
),
|
|
585
|
+
'nested duplicate project copies should not block recreating the canonical page path'
|
|
586
|
+
);
|
|
587
|
+
assert.equal(
|
|
588
|
+
cli._directScopeFileCandidates(fixture.repo, ['gamma']).some(candidate => candidate.startsWith(`${path.basename(fixture.repo)}/`)),
|
|
589
|
+
false,
|
|
590
|
+
'scope candidates should not steer agents toward nested project-copy paths when the canonical parent exists'
|
|
591
|
+
);
|
|
558
592
|
const extractedOldCss = Array.from({ length: 28 }, (_, i) =>
|
|
559
593
|
`.hero-polish-${i} { color: #${String(100000 + i).slice(0, 6)}; padding: ${i + 1}px; margin: ${i}px; box-shadow: 0 0 ${i + 2}px rgba(0,0,0,.1); }`
|
|
560
594
|
).join('\n');
|
|
@@ -647,14 +681,28 @@ try {
|
|
|
647
681
|
),
|
|
648
682
|
'scoped shell sed should ignore the substitution script and allow canonical target page edits'
|
|
649
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
|
+
);
|
|
650
697
|
assert.doesNotThrow(
|
|
651
698
|
() => cli._guardDirectBashCommand(
|
|
652
699
|
'touch public_html/css/domain-detail.css',
|
|
653
700
|
fixture.repo,
|
|
654
701
|
{ task: message }
|
|
655
702
|
),
|
|
656
|
-
'direct scoped work should allow
|
|
703
|
+
'direct scoped work should allow generic CSS assets after they are linked only by scoped pages'
|
|
657
704
|
);
|
|
705
|
+
writeFileSync(alphaFixturePath, alphaFixtureCurrent);
|
|
658
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>';
|
|
659
707
|
assert.throws(
|
|
660
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 = [];
|
|
@@ -694,7 +807,10 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
694
807
|
walk(workDir, 0);
|
|
695
808
|
const resultSet = new Set(results);
|
|
696
809
|
return results.filter(rel => {
|
|
697
|
-
|
|
810
|
+
const absolute = path.resolve(workDir, rel);
|
|
811
|
+
const nestedCanonical = this._nestedProjectCopyCanonicalPath(absolute, workDir);
|
|
812
|
+
if (nestedCanonical && existsSync(path.dirname(nestedCanonical))) return false;
|
|
813
|
+
const canonical = this._nestedProjectDuplicateCanonicalPath(absolute, workDir);
|
|
698
814
|
if (!canonical) return true;
|
|
699
815
|
const canonicalRel = path.relative(workDir, canonical) || canonical;
|
|
700
816
|
return !resultSet.has(canonicalRel);
|
|
@@ -849,6 +965,24 @@ export class OpenClawCLI extends EventEmitter {
|
|
|
849
965
|
if (!this._directTaskScopeAllowsPath(filePath, workDir, options.task)) {
|
|
850
966
|
throw this._directScopeViolationError(this._formatDirectScopeError(filePath, workDir, options.task));
|
|
851
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
|
+
}
|
|
852
986
|
if (pageOnly && slugs.length > 0 && this._isDirectHtmlPagePath(relativePath)) {
|
|
853
987
|
const matchedSlugs = this._directScopeSlugsMatchingText(lowerRelativePath, slugs);
|
|
854
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
|
}
|