@hamp10/agentforge 0.2.33 → 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 +22 -0
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 {
|