@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hamp10/agentforge",
3
- "version": "0.2.33",
3
+ "version": "0.2.34",
4
4
  "description": "AgentForge worker — connect your machine to agentforge.ai",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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,
@@ -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 {
@@ -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
- return /^\s+(?:listing\s+)?(?:pages?|screens?|routes?|views?|listings?)\s+(?:for|of|called|named)\b/i.test(after);
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}?)(?=$|[?!;]|\.(?:\s|$)|\s+(?:on|at|in|from|with|without|while|using|preserv(?:e|ing)|that|which|where|when|because|so|but)\b)/gi;
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}?)(?=$|[?!;]|\.(?:\s|$)|\s+(?:on|at|in|from|with|without|while|using|preserv(?:e|ing)|that|which|where|when|because|so|but)\b)/i);
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 {