@hamp10/agentforge 0.2.36 → 0.2.38

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.36",
3
+ "version": "0.2.38",
4
4
  "description": "AgentForge worker — connect your machine to agentforge.ai",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,6 +35,12 @@ const cases = [
35
35
  absent: ['example-com', 'make', 'fix'],
36
36
  pageOnly: true,
37
37
  },
38
+ {
39
+ text: 'Work on the Example.com listing pages for AlphaBoard.ai, BetaMatch.ai, and GammaForge.ai. Make all three pages visually polished and consistent with the rest of the site.',
40
+ slugs: ['alphaboard-ai', 'betamatch-ai', 'gammaforge-ai'],
41
+ absent: ['example-com', 'three', 'all-three', 'visual', 'polished'],
42
+ pageOnly: true,
43
+ },
38
44
  {
39
45
  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.',
40
46
  slugs: ['alphaboard', 'betamatch'],
@@ -249,6 +255,33 @@ try {
249
255
  'clean-start rebuild tasks should still require the scoped target page to be recreated'
250
256
  );
251
257
  git(fixture.repo, ['restore', '--', 'public_html/domains/alpha.html']);
258
+ const nestedAlphaRel = `${path.basename(fixture.repo)}/public_html/domains/alpha.html`;
259
+ rmSync(path.join(fixture.repo, nestedAlphaRel), { force: true });
260
+ assert.equal(
261
+ worker._buildDeletedScopedPageNudge(
262
+ [{ root: fixture.repo, head: fixture.head }],
263
+ 'Delete and rebuild the Alpha listing page from a clean start, preserving the same URL. Only change that listing page.'
264
+ ),
265
+ '',
266
+ 'tracked same-name nested project copies should not count as missing scoped page sources'
267
+ );
268
+ assert.equal(
269
+ worker._scopeAllowsChangedPath(
270
+ { root: fixture.repo, head: fixture.head },
271
+ nestedAlphaRel,
272
+ ['alpha'],
273
+ true,
274
+ 'Delete and rebuild the Alpha listing page from a clean start. Only change that listing page.'
275
+ ),
276
+ false,
277
+ 'tracked same-name nested project copies should be treated as out-of-scope changes'
278
+ );
279
+ assert.equal(
280
+ worker._allCurrentPageSourceFiles(fixture.repo).includes(nestedAlphaRel),
281
+ false,
282
+ 'tracked same-name nested project copies should not pollute current page source discovery'
283
+ );
284
+ git(fixture.repo, ['restore', '--', nestedAlphaRel]);
252
285
  const projectsRoot = mkdtempSync(path.join(tmpdir(), 'agentforge-project-list-'));
253
286
  let agentWorkspace = null;
254
287
  try {
@@ -540,6 +573,38 @@ try {
540
573
  ),
541
574
  'direct UI validation should allow standard tooling/license source comments'
542
575
  );
576
+ const richExistingPageHtml = [
577
+ '<!doctype html><html><body><main>',
578
+ '<section><h1>AgentForge.ai</h1><p>' + 'Detailed product positioning and buyer rationale. '.repeat(12) + '</p></section>',
579
+ '<section><h2>Market Context</h2><p>' + 'Category analysis, comparable sales, and audience fit. '.repeat(10) + '</p></section>',
580
+ '<section><h2>Use Cases</h2><p>' + 'Platform applications, operator workflows, and strategic value. '.repeat(10) + '</p></section>',
581
+ '<section><h2>Acquisition</h2><p>' + 'Inquiry process, domain transfer notes, and buyer confidence. '.repeat(8) + '</p></section>',
582
+ '</main></body></html>',
583
+ ].join('\n');
584
+ const thinReplacementPageHtml = '<!doctype html><html><body><main><section><h1>AgentForge.ai</h1><p>Short replacement.</p></section></main></body></html>';
585
+ assert.throws(
586
+ () => cli._validateDirectUiFileContent(
587
+ path.join(fixture.repo, 'public_html', 'domains', 'agentforge-ai.html'),
588
+ thinReplacementPageHtml,
589
+ {
590
+ task: 'Work on the Example.com listing pages for Alpha.ai, Beta.ai, and AgentForge.ai. Delete and rebuild Alpha.ai and Beta.ai from a clean start, and fix AgentForge.ai so all three are visually polished.',
591
+ oldContent: richExistingPageHtml,
592
+ }
593
+ ),
594
+ /substantial target-page content was removed/i,
595
+ 'clean-start permission for some scoped targets should not allow destructive content loss on a different fix-only target'
596
+ );
597
+ assert.doesNotThrow(
598
+ () => cli._validateDirectUiFileContent(
599
+ path.join(fixture.repo, 'public_html', 'domains', 'alpha.html'),
600
+ thinReplacementPageHtml.replace(/AgentForge\.ai/g, 'Alpha.ai'),
601
+ {
602
+ task: 'Work on the Example.com listing pages for Alpha.ai, Beta.ai, and AgentForge.ai. Delete and rebuild Alpha.ai and Beta.ai from a clean start, and fix AgentForge.ai so all three are visually polished.',
603
+ oldContent: richExistingPageHtml.replace(/AgentForge\.ai/g, 'Alpha.ai'),
604
+ }
605
+ ),
606
+ 'clean-start permission should still allow substantial replacement on the specific target named in the rebuild clause'
607
+ );
543
608
  const protectedReplacementFile = path.join(fixture.repo, 'public_html', 'domains', 'alpha.html');
544
609
  const protectedReplacementOriginal = readFileSync(protectedReplacementFile, 'utf-8');
545
610
  await assert.rejects(
@@ -1193,7 +1193,7 @@ export class OpenClawCLI extends EventEmitter {
1193
1193
  if (!this._isDirectBroadUiQualityTask(options?.task)) return;
1194
1194
  if (!/\.css$/i.test(String(filePath || ''))) return;
1195
1195
  const taskText = String(options?.task || '');
1196
- const isCleanStartTask = /\b(?:delete|rebuild|clean start|from scratch|start over|fresh|remake)\b/i.test(taskText);
1196
+ const isCleanStartTask = this._directTaskAllowsContentReductionForPath(filePath, taskText, /\b(?:delete|rebuild|clean start|from scratch|start over|fresh|remake)\b/i);
1197
1197
  if (!isCleanStartTask) return;
1198
1198
  const { slugs, pageOnly } = this._extractDirectExplicitScope(options?.task);
1199
1199
  if (!pageOnly || slugs.length === 0) return;
@@ -1289,7 +1289,7 @@ export class OpenClawCLI extends EventEmitter {
1289
1289
  if (!this._isDirectBroadUiQualityTask(options?.task)) return;
1290
1290
  if (!/\.(?:html?|xhtml|astro|mdx?)$/i.test(String(filePath || ''))) return;
1291
1291
  const taskText = String(options?.task || '');
1292
- const isCleanStartTask = /\b(?:delete|rebuild|clean start|from scratch|start over|fresh|remake)\b/i.test(taskText);
1292
+ const isCleanStartTask = this._directTaskAllowsContentReductionForPath(filePath, taskText, /\b(?:delete|rebuild|clean start|from scratch|start over|fresh|remake)\b/i);
1293
1293
  if (!isCleanStartTask) return;
1294
1294
  const { slugs, pageOnly } = this._extractDirectExplicitScope(options?.task);
1295
1295
  if (!pageOnly || slugs.length === 0) return;
@@ -1351,6 +1351,27 @@ export class OpenClawCLI extends EventEmitter {
1351
1351
  throw err;
1352
1352
  }
1353
1353
 
1354
+ _directTaskActionClauses(taskText) {
1355
+ return String(taskText || '')
1356
+ .split(/(?:[.!?;]\s+|,\s*(?:but|then)\s+|,\s*and\s+(?=(?:fix|repair|update|improve|polish|address|adjust|correct)\b)|\band\s+(?=(?:fix|repair|update|improve|polish|address|adjust|correct)\b))/i)
1357
+ .map(clause => clause.trim())
1358
+ .filter(Boolean);
1359
+ }
1360
+
1361
+ _directTaskAllowsContentReductionForPath(filePath, taskText, actionRe = /\b(?:remove|delete|strip|simplif(?:y|ied|ication)|shorten|condense|reduce|minimal|less content|cleanup|clean up|prune)\b/i) {
1362
+ const text = String(taskText || '');
1363
+ if (!actionRe.test(text)) return false;
1364
+ const { slugs, pageOnly } = this._extractDirectExplicitScope(text);
1365
+ if (!pageOnly || slugs.length <= 1) return true;
1366
+ const matchingSlugs = this._directScopeSlugsMatchingText(String(filePath || '').toLowerCase(), slugs);
1367
+ if (matchingSlugs.length === 0) return false;
1368
+ const clauses = this._directTaskActionClauses(text);
1369
+ return matchingSlugs.some(slug => clauses.some(clause =>
1370
+ actionRe.test(clause) &&
1371
+ this._directScopeSlugsMatchingText(clause.toLowerCase(), [slug]).length > 0
1372
+ ));
1373
+ }
1374
+
1354
1375
  _validateDirectUiImplementationArtifacts(filePath, content, options = {}) {
1355
1376
  if (!this._isDirectBroadUiQualityTask(options?.task)) return;
1356
1377
  if (!/\.(?:html?|xhtml|css|s[ac]ss|jsx?|tsx?|vue|svelte|astro|mdx?)$/i.test(String(filePath || ''))) return;
@@ -1427,7 +1448,7 @@ export class OpenClawCLI extends EventEmitter {
1427
1448
  }
1428
1449
  }
1429
1450
 
1430
- const taskAllowsContentReduction = /\b(?:remove|delete|strip|simplif(?:y|ied|ication)|shorten|condense|reduce|minimal|less content|cleanup|clean up|prune)\b/i.test(taskText);
1451
+ const taskAllowsContentReduction = this._directTaskAllowsContentReductionForPath(filePath, taskText);
1431
1452
  if (!taskAllowsContentReduction && (oldText.trim() || headText.trim()) && /\.(?:html?|xhtml|jsx?|tsx?|vue|svelte|astro|mdx?)$/i.test(String(filePath || ''))) {
1432
1453
  const visibleTextApprox = (value) => String(value || '')
1433
1454
  .replace(/<script\b[\s\S]*?<\/script>/gi, ' ')
@@ -11,8 +11,9 @@ const GENERIC_SCOPE_WORDS = new Set([
11
11
  'website', 'app', 'readability', 'readable', 'legibility', 'contrast',
12
12
  'quality', 'visual', 'content', 'layout', 'styling', 'polish',
13
13
  'current', 'target', 'targets', 'requested', 'same', 'rest', 'live',
14
- 'local', 'itself', 'only', 'those', 'these', 'this', 'that', 'two',
15
- 'both', 'change', 'edit', 'modify', 'touch', 'make', 'improve', 'fix',
14
+ 'local', 'itself', 'only', 'all', 'those', 'these', 'this', 'that',
15
+ 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine',
16
+ 'ten', 'both', 'change', 'edit', 'modify', 'touch', 'make', 'improve', 'fix',
16
17
  'update', 'redesign', 'owned', 'source', 'style', 'styles', 'global',
17
18
  'shared', 'reference', 'references', 'broaden', 'work', 'wrong', 'bad',
18
19
  'broken',
package/src/worker.js CHANGED
@@ -650,6 +650,22 @@ export class AgentForgeWorker extends EventEmitter {
650
650
  return /\.(?:html?|xhtml|astro|mdx?)$/i.test(String(relativePath || ''));
651
651
  }
652
652
 
653
+ _nestedProjectCopyCanonicalRel(repo, rel) {
654
+ const normalized = String(rel || '').replace(/^[/\\]+/, '');
655
+ if (!repo || !normalized) return '';
656
+ const parts = normalized.split(/[\\/]+/).filter(Boolean);
657
+ if (parts.length < 2 || parts[0].toLowerCase() !== path.basename(repo).toLowerCase()) return '';
658
+ return parts.slice(1).join('/');
659
+ }
660
+
661
+ _isNestedProjectCopyRel(repo, rel, ref = 'HEAD') {
662
+ const canonicalRel = this._nestedProjectCopyCanonicalRel(repo, rel);
663
+ if (!canonicalRel) return false;
664
+ const canonicalAbs = path.join(repo, ...canonicalRel.split('/'));
665
+ if (existsSync(canonicalAbs) || existsSync(path.dirname(canonicalAbs))) return true;
666
+ return this._gitPathExistsAtRef(repo, ref || 'HEAD', canonicalRel);
667
+ }
668
+
653
669
  _isBroadReferenceSourcePath(relativePath) {
654
670
  const normalized = String(relativePath || '').replace(/\\/g, '/').toLowerCase();
655
671
  const parts = normalized.split('/').filter(Boolean);
@@ -727,6 +743,7 @@ export class AgentForgeWorker extends EventEmitter {
727
743
  const names = new Set();
728
744
  const add = (output) => {
729
745
  for (const rel of String(output || '').split('\n').map(line => line.trim()).filter(Boolean)) {
746
+ if (this._isNestedProjectCopyRel(repo, rel)) continue;
730
747
  if (this._isPageSourcePath(rel)) names.add(rel);
731
748
  }
732
749
  };
@@ -785,6 +802,7 @@ export class AgentForgeWorker extends EventEmitter {
785
802
 
786
803
  _scopeAllowsChangedPath(baseline, rel, allowedSlugs, pageOnly, userMessage = '') {
787
804
  const lower = String(rel || '').toLowerCase();
805
+ if (this._isNestedProjectCopyRel(baseline?.root, rel, baseline?.head || 'HEAD')) return false;
788
806
  const slugAllowed = this._scopeSlugsMatchingText(lower, allowedSlugs).length > 0;
789
807
  if (slugAllowed) {
790
808
  if (this._isNewHtmlPageOutsideExistingCollection(baseline, rel, allowedSlugs, pageOnly)) return false;
@@ -988,13 +1006,12 @@ export class AgentForgeWorker extends EventEmitter {
988
1006
  const deleted = [];
989
1007
  const status = this._gitStatusPorcelain(baseline.root, 10000);
990
1008
  for (const rawLine of String(status || '').split('\n').filter(Boolean)) {
991
- const x = rawLine[0] || ' ';
992
- const y = rawLine[1] || ' ';
993
- if (x !== 'D' && y !== 'D') continue;
994
- const rel = rawLine.length >= 3 && rawLine[2] === ' ' ? rawLine.slice(3) : rawLine.trim().replace(/^..\s+/, '');
995
- const pathName = rel.includes(' -> ') ? rel.split(' -> ').pop() : rel;
1009
+ const statusCode = rawLine.slice(0, 2);
1010
+ if (!statusCode.includes('D')) continue;
1011
+ const pathName = this._parseGitStatusPaths(rawLine)[0] || '';
996
1012
  const lower = String(pathName || '').toLowerCase();
997
1013
  if (!this._isPageSourcePath(lower)) continue;
1014
+ if (this._isNestedProjectCopyRel(baseline.root, pathName, baseline.head || 'HEAD')) continue;
998
1015
  if (this._scopeSlugsMatchingText(lower, allowedSlugs).length === 0) continue;
999
1016
  deleted.push(pathName);
1000
1017
  }