@hamp10/agentforge 0.2.39 → 0.2.41

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.39",
3
+ "version": "0.2.41",
4
4
  "description": "AgentForge worker — connect your machine to agentforge.ai",
5
5
  "type": "module",
6
6
  "bin": {
@@ -296,6 +296,93 @@ try {
296
296
  'new untracked scoped page files should count as touched UI targets'
297
297
  );
298
298
  rmSync(path.join(fixture.repo, untrackedGammaRel), { force: true });
299
+ const untrackedShallowGammaHtml = [
300
+ '<!doctype html>',
301
+ '<html>',
302
+ '<head>',
303
+ '<style>',
304
+ ...Array.from({ length: 36 }, (_, i) =>
305
+ `.hero-gloss-${i} { color: #fff; background: #${String(120000 + i).slice(0, 6)}; padding: ${i + 4}px; margin: ${i}px; box-shadow: 0 0 ${i + 1}px rgba(0,0,0,.12); }`
306
+ ),
307
+ '</style>',
308
+ '</head>',
309
+ '<body>',
310
+ '<main>',
311
+ '<section class="property-hero hero-gloss-1">',
312
+ '<h1 class="property-title">Gamma.ai</h1>',
313
+ '<p class="property-subtitle">Short generated hero copy.</p>',
314
+ '</section>',
315
+ '</main>',
316
+ '</body>',
317
+ '</html>',
318
+ ].join('\n');
319
+ writeFileSync(path.join(fixture.repo, untrackedGammaRel), untrackedShallowGammaHtml);
320
+ const generatedQualityMessage = 'Work on the Example.com listing page for Gamma.ai. Make it visually polished.';
321
+ assert.match(
322
+ worker._buildShallowUiSurfaceNudge(
323
+ [{ root: fixture.repo, head: fixture.head, initialDirtyPaths: [] }],
324
+ generatedQualityMessage
325
+ ),
326
+ /gamma\.html/i,
327
+ 'untracked generated scoped page files should participate in shallow UI quality checks'
328
+ );
329
+ const generatedReset = worker._restoreGeneratedScopedUiTargets(
330
+ [{ root: fixture.repo, head: fixture.head, initialDirtyPaths: [] }],
331
+ generatedQualityMessage
332
+ );
333
+ assert.equal(
334
+ existsSync(path.join(fixture.repo, untrackedGammaRel)),
335
+ false,
336
+ 'UI quality retries should remove rejected generated scoped pages before the next attempt'
337
+ );
338
+ assert.match(
339
+ worker._formatGeneratedScopedUiResetNudge(generatedReset),
340
+ /removed 1 rejected generated scoped file/i,
341
+ 'retry feedback should tell the agent that generated scoped pages were removed as failed output'
342
+ );
343
+ const generatedDeltaRel = 'public_html/domains/delta.html';
344
+ const generatedEpsilonRel = 'public_html/domains/epsilon.html';
345
+ writeFileSync(
346
+ path.join(fixture.repo, generatedDeltaRel),
347
+ '<!doctype html><html><body><main><section><h1>Delta</h1><p>New scoped page.</p></section></main></body></html>'
348
+ );
349
+ writeFileSync(
350
+ path.join(fixture.repo, generatedEpsilonRel),
351
+ '<!doctype html><html><body><main><section><h1>Epsilon</h1><p>New scoped page.</p></section></main></body></html>'
352
+ );
353
+ const multiGeneratedQualityMessage = 'Work on the Example.com listing pages for Delta.ai and Epsilon.ai. Make both pages visually polished.';
354
+ assert.deepEqual(
355
+ worker._extractUiVerificationFailureSlugs(
356
+ 'delta.html: Visual warning: hero text fails local contrast verification.',
357
+ multiGeneratedQualityMessage
358
+ ),
359
+ ['delta-ai'],
360
+ 'visual verification parsing should identify the scoped page named by a page-prefixed warning'
361
+ );
362
+ assert.deepEqual(
363
+ worker._extractUiVerificationFailureSlugs(
364
+ 'Visual warning: hero text fails local contrast verification.',
365
+ multiGeneratedQualityMessage
366
+ ),
367
+ [],
368
+ 'generic visual warnings should not be treated as evidence to reset every generated scoped page'
369
+ );
370
+ worker._restoreGeneratedScopedUiTargets(
371
+ [{ root: fixture.repo, head: fixture.head, initialDirtyPaths: [] }],
372
+ multiGeneratedQualityMessage,
373
+ { onlySlugs: ['delta-ai'] }
374
+ );
375
+ assert.equal(
376
+ existsSync(path.join(fixture.repo, generatedDeltaRel)),
377
+ false,
378
+ 'page-aware generated resets should remove the rejected generated page'
379
+ );
380
+ assert.equal(
381
+ existsSync(path.join(fixture.repo, generatedEpsilonRel)),
382
+ true,
383
+ 'page-aware generated resets should preserve unrelated generated scoped pages'
384
+ );
385
+ rmSync(path.join(fixture.repo, generatedEpsilonRel), { force: true });
299
386
  const projectsRoot = mkdtempSync(path.join(tmpdir(), 'agentforge-project-list-'));
300
387
  let agentWorkspace = null;
301
388
  try {
package/src/worker.js CHANGED
@@ -530,6 +530,24 @@ export class AgentForgeWorker extends EventEmitter {
530
530
  return stats;
531
531
  }
532
532
 
533
+ _listUntrackedUiFiles(baseline, userMessage, { scopedOnly = false } = {}) {
534
+ if (!baseline?.root) return [];
535
+ const uiFileRe = /\.(html?|css|s[ac]ss|jsx?|tsx?|vue|svelte|astro|mdx?)$/i;
536
+ const initialDirty = new Set(baseline.initialDirtyPaths || []);
537
+ const { slugs: allowedSlugs, pageOnly } = this._extractExplicitScope(userMessage);
538
+ return String(this._gitOutput(baseline.root, ['ls-files', '--others', '--exclude-standard'], 10000) || '')
539
+ .split('\n')
540
+ .map(line => line.trim())
541
+ .filter(Boolean)
542
+ .filter(rel => uiFileRe.test(rel))
543
+ .filter(rel => !initialDirty.has(rel))
544
+ .filter(rel => !this._isNestedProjectCopyRel(baseline.root, rel, baseline.head || 'HEAD'))
545
+ .filter(rel => {
546
+ if (!scopedOnly || allowedSlugs.length === 0) return true;
547
+ return this._scopeAllowsChangedPath(baseline, rel, allowedSlugs, pageOnly, userMessage);
548
+ });
549
+ }
550
+
533
551
  _findDestructiveRepoChanges(repoBaselines, userMessage) {
534
552
  if (!Array.isArray(repoBaselines) || repoBaselines.length === 0) return [];
535
553
  if (this._allowsLargeDestructiveChange(userMessage)) return [];
@@ -927,11 +945,19 @@ export class AgentForgeWorker extends EventEmitter {
927
945
  return restored;
928
946
  }
929
947
 
930
- _restoreGeneratedScopedUiTargets(repoBaselines, userMessage) {
948
+ _scopedUiPathMatchesAnySlug(relativePath, slugs) {
949
+ const matches = this._scopeSlugsMatchingText(relativePath, slugs);
950
+ return matches.length > 0;
951
+ }
952
+
953
+ _restoreGeneratedScopedUiTargets(repoBaselines, userMessage, options = {}) {
931
954
  if (!this._isBroadUiQualityTask(userMessage)) return [];
932
955
  if (!Array.isArray(repoBaselines) || repoBaselines.length === 0) return [];
933
956
  const { slugs: allowedSlugs, pageOnly } = this._extractExplicitScope(userMessage);
934
957
  if (allowedSlugs.length === 0 || !pageOnly) return [];
958
+ const onlySlugs = Array.isArray(options.onlySlugs)
959
+ ? [...new Set(options.onlySlugs.map(slug => this._scopeSlug(slug)).filter(Boolean))]
960
+ : [];
935
961
 
936
962
  const restored = [];
937
963
  for (const baseline of repoBaselines) {
@@ -952,6 +978,7 @@ export class AgentForgeWorker extends EventEmitter {
952
978
  const files = [...names]
953
979
  .filter(rel => !initialDirty.has(rel))
954
980
  .filter(rel => this._scopeAllowsChangedPath(baseline, rel, allowedSlugs, pageOnly, userMessage))
981
+ .filter(rel => onlySlugs.length === 0 || this._scopedUiPathMatchesAnySlug(rel, onlySlugs))
955
982
  .filter(rel => !this._gitPathExistsAtRef(baseline.root, baseline.head || 'HEAD', rel))
956
983
  .sort();
957
984
  if (files.length === 0) continue;
@@ -1158,7 +1185,20 @@ export class AgentForgeWorker extends EventEmitter {
1158
1185
  this._gitOutput(baseline.root, ['diff', '--numstat'], 10000),
1159
1186
  'working tree'
1160
1187
  );
1161
- const combined = [...committed, ...working]
1188
+ const untracked = this._listUntrackedUiFiles(baseline, userMessage, { scopedOnly: true })
1189
+ .map(rel => {
1190
+ let additions = 0;
1191
+ try {
1192
+ additions = String(readFileSync(path.join(baseline.root, rel), 'utf-8') || '')
1193
+ .split('\n')
1194
+ .filter(line => line.trim())
1195
+ .length;
1196
+ } catch {
1197
+ additions = 1;
1198
+ }
1199
+ return { additions, deletions: 0, path: rel, source: 'untracked' };
1200
+ });
1201
+ const combined = [...committed, ...working, ...untracked]
1162
1202
  .filter(stat => /\.(html?|css|s[ac]ss|jsx?|tsx?|vue|svelte|astro|mdx?)$/i.test(stat.path));
1163
1203
  if (combined.length === 0) continue;
1164
1204
 
@@ -1272,10 +1312,45 @@ export class AgentForgeWorker extends EventEmitter {
1272
1312
  }
1273
1313
  };
1274
1314
 
1315
+ const scanUntracked = (baseline, byPath) => {
1316
+ for (const currentFile of this._listUntrackedUiFiles(baseline, userMessage, { scopedOnly: true })) {
1317
+ let content = '';
1318
+ try {
1319
+ content = readFileSync(path.join(baseline.root, currentFile), 'utf-8');
1320
+ } catch {
1321
+ continue;
1322
+ }
1323
+ for (const line of String(content || '').split('\n')) {
1324
+ const raw = line.trim();
1325
+ if (!raw || /^(?:\/\*|\*|\/\/|<!--|-->|})$/.test(raw)) continue;
1326
+ const stat = byPath.get(currentFile) || {
1327
+ path: currentFile,
1328
+ changed: 0,
1329
+ shallow: 0,
1330
+ semanticAdds: 0,
1331
+ inlineStyleAdds: 0,
1332
+ sources: new Set(),
1333
+ };
1334
+ stat.changed += 1;
1335
+ stat.sources.add('untracked');
1336
+ if (shallowLineRe.test(raw)) stat.shallow += 1;
1337
+ if (/\bstyle=["'][^"']+["']/i.test(raw)) stat.inlineStyleAdds += 1;
1338
+ const text = visibleText(raw);
1339
+ const hasStructuralMarkup = /<(?:section|article|aside|ul|ol|li|h2|h3|p|form|table|figure)\b/i.test(raw);
1340
+ const hasSubstantiveClassWithText = structuralClassRe.test(raw) && /<[^>]+>/i.test(raw) && text.length >= 24;
1341
+ if ((hasStructuralMarkup && text.length >= 35) || hasSubstantiveClassWithText) {
1342
+ stat.semanticAdds += 1;
1343
+ }
1344
+ byPath.set(currentFile, stat);
1345
+ }
1346
+ }
1347
+ };
1348
+
1275
1349
  for (const baseline of repoBaselines) {
1276
1350
  const byPath = new Map();
1277
1351
  scanDiff(baseline, ['diff', '--unified=0', `${baseline.head}..HEAD`], 'committed', byPath);
1278
1352
  scanDiff(baseline, ['diff', '--unified=0'], 'working tree', byPath);
1353
+ scanUntracked(baseline, byPath);
1279
1354
  const changedFiles = [...byPath.values()];
1280
1355
  if (changedFiles.length === 0) continue;
1281
1356
 
@@ -4160,12 +4235,15 @@ export class AgentForgeWorker extends EventEmitter {
4160
4235
  : '';
4161
4236
  if (interimUiImplementationArtifactNudge) {
4162
4237
  const restoreNudge = this._formatUiQualityRestoreNudge(this._restoreUiQualityWarningPaths(interimUiImplementationArtifactWarnings), nudgeCount + 1);
4238
+ const generatedResetNudge = this._formatGeneratedScopedUiResetNudge(
4239
+ this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage)
4240
+ );
4163
4241
  const interimScopedUiTargetSetNudge = this._buildScopedUiTargetSetNudge(repoBaselines, scopeAwareUserMessage);
4164
4242
  nudgeCount++;
4165
4243
  const repairBudget = consumeUiRepairNudge('patch artifacts', interimUiImplementationArtifactNudge);
4166
4244
  if (nudgeCount < UI_QUALITY_NUDGE_LIMIT) {
4167
4245
  console.log(`[${taskId}] Iteration had patch artifacts — correcting (${nudgeCount}/${UI_QUALITY_NUDGE_LIMIT}, total UI repairs ${repairBudget})`);
4168
- const combinedNudge = [interimUiImplementationArtifactNudge, restoreNudge, interimScopedUiTargetSetNudge].filter(Boolean).join('\n\n');
4246
+ const combinedNudge = [interimUiImplementationArtifactNudge, restoreNudge, generatedResetNudge, interimScopedUiTargetSetNudge].filter(Boolean).join('\n\n');
4169
4247
  iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${combinedNudge}\n\nContinue from the restored files and already-inspected context. Clean the implementation, address every requested scoped target page, and verify each edited target screen after the final edit.`);
4170
4248
  continue;
4171
4249
  }
@@ -4190,12 +4268,15 @@ export class AgentForgeWorker extends EventEmitter {
4190
4268
  : '';
4191
4269
  if (interimShallowUiSurfaceNudge) {
4192
4270
  const restoreNudge = this._formatUiQualityRestoreNudge(this._restoreUiQualityWarningPaths(interimShallowUiSurfaceWarnings), nudgeCount + 1);
4271
+ const generatedResetNudge = this._formatGeneratedScopedUiResetNudge(
4272
+ this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage)
4273
+ );
4193
4274
  const scopedTargetSetNudge = this._formatScopedUiTargetSetReminder(scopeAwareUserMessage);
4194
4275
  nudgeCount++;
4195
4276
  const repairBudget = consumeUiRepairNudge('shallow UI surface changes', interimShallowUiSurfaceNudge);
4196
4277
  if (nudgeCount < UI_QUALITY_NUDGE_LIMIT) {
4197
4278
  console.log(`[${taskId}] Iteration had shallow UI surface changes — continuing (${nudgeCount}/${UI_QUALITY_NUDGE_LIMIT}, total UI repairs ${repairBudget})`);
4198
- iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${[interimShallowUiSurfaceNudge, restoreNudge, scopedTargetSetNudge].filter(Boolean).join('\n\n')}\n\nContinue from the restored files and already-inspected context. Make the requested scoped UI materially better across the page-owned surface, then verify the edited target screens after the final edit.`);
4279
+ iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${[interimShallowUiSurfaceNudge, restoreNudge, generatedResetNudge, scopedTargetSetNudge].filter(Boolean).join('\n\n')}\n\nContinue from the restored files and already-inspected context. Make the requested scoped UI materially better across the page-owned surface, then verify the edited target screens after the final edit.`);
4199
4280
  continue;
4200
4281
  }
4201
4282
  throw new Error(`Task produced shallow UI surface changes after repeated retries:\n${interimShallowUiSurfaceNudge}`);
@@ -4207,12 +4288,15 @@ export class AgentForgeWorker extends EventEmitter {
4207
4288
  : '';
4208
4289
  if (interimThinUiChangeNudge) {
4209
4290
  const restoreNudge = this._formatUiQualityRestoreNudge(this._restoreUiQualityWarningPaths(interimThinUiChangeWarnings), nudgeCount + 1);
4291
+ const generatedResetNudge = this._formatGeneratedScopedUiResetNudge(
4292
+ this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage)
4293
+ );
4210
4294
  const scopedTargetSetNudge = this._formatScopedUiTargetSetReminder(scopeAwareUserMessage);
4211
4295
  nudgeCount++;
4212
4296
  const repairBudget = consumeUiRepairNudge('too-thin UI changes', interimThinUiChangeNudge);
4213
4297
  if (nudgeCount < UI_QUALITY_NUDGE_LIMIT) {
4214
4298
  console.log(`[${taskId}] Iteration had too-thin UI changes — continuing (${nudgeCount}/${UI_QUALITY_NUDGE_LIMIT}, total UI repairs ${repairBudget})`);
4215
- iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${[interimThinUiChangeNudge, restoreNudge, scopedTargetSetNudge].filter(Boolean).join('\n\n')}\n\nContinue from the restored files and already-inspected context. Make the requested scoped UI materially better, then verify the edited target screens after the final edit.`);
4299
+ iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${[interimThinUiChangeNudge, restoreNudge, generatedResetNudge, scopedTargetSetNudge].filter(Boolean).join('\n\n')}\n\nContinue from the restored files and already-inspected context. Make the requested scoped UI materially better, then verify the edited target screens after the final edit.`);
4216
4300
  continue;
4217
4301
  }
4218
4302
  throw new Error(`Task produced too-thin UI changes after repeated retries:\n${interimThinUiChangeNudge}`);
@@ -4314,12 +4398,15 @@ export class AgentForgeWorker extends EventEmitter {
4314
4398
  : '';
4315
4399
  if (uiImplementationArtifactNudge) {
4316
4400
  const restoreNudge = this._formatUiQualityRestoreNudge(this._restoreUiQualityWarningPaths(uiImplementationArtifactWarnings), nudgeCount + 1);
4401
+ const generatedResetNudge = this._formatGeneratedScopedUiResetNudge(
4402
+ this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage)
4403
+ );
4317
4404
  const scopedUiTargetSetNudge = this._buildScopedUiTargetSetNudge(repoBaselines, scopeAwareUserMessage);
4318
4405
  nudgeCount++;
4319
4406
  const repairBudget = consumeUiRepairNudge('completion patch artifacts', uiImplementationArtifactNudge);
4320
4407
  if (nudgeCount < UI_QUALITY_NUDGE_LIMIT) {
4321
4408
  console.log(`[${taskId}] UI completion had patch artifacts — nudging (${nudgeCount}/${UI_QUALITY_NUDGE_LIMIT}, total UI repairs ${repairBudget})`);
4322
- const combinedNudge = [uiImplementationArtifactNudge, restoreNudge, scopedUiTargetSetNudge].filter(Boolean).join('\n\n');
4409
+ const combinedNudge = [uiImplementationArtifactNudge, restoreNudge, generatedResetNudge, scopedUiTargetSetNudge].filter(Boolean).join('\n\n');
4323
4410
  iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${combinedNudge}\n\nContinue from the restored files and already-inspected context. Do not mark complete until the UI source reads like finished product code and every requested scoped target screen is visually verified.`);
4324
4411
  continue;
4325
4412
  }
@@ -4342,12 +4429,15 @@ export class AgentForgeWorker extends EventEmitter {
4342
4429
  : '';
4343
4430
  if (shallowUiSurfaceNudge) {
4344
4431
  const restoreNudge = this._formatUiQualityRestoreNudge(this._restoreUiQualityWarningPaths(shallowUiSurfaceWarnings), nudgeCount + 1);
4432
+ const generatedResetNudge = this._formatGeneratedScopedUiResetNudge(
4433
+ this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage)
4434
+ );
4345
4435
  const scopedTargetSetNudge = this._formatScopedUiTargetSetReminder(scopeAwareUserMessage);
4346
4436
  nudgeCount++;
4347
4437
  const repairBudget = consumeUiRepairNudge('completion shallow UI surface changes', shallowUiSurfaceNudge);
4348
4438
  if (nudgeCount < UI_QUALITY_NUDGE_LIMIT) {
4349
4439
  console.log(`[${taskId}] UI completion had shallow surface diff — nudging (${nudgeCount}/${UI_QUALITY_NUDGE_LIMIT}, total UI repairs ${repairBudget})`);
4350
- iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${[shallowUiSurfaceNudge, restoreNudge, scopedTargetSetNudge].filter(Boolean).join('\n\n')}\n\nDo not mark complete until the requested scoped UI is improved across the page-owned product surface and visually verified.`);
4440
+ iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${[shallowUiSurfaceNudge, restoreNudge, generatedResetNudge, scopedTargetSetNudge].filter(Boolean).join('\n\n')}\n\nDo not mark complete until the requested scoped UI is improved across the page-owned product surface and visually verified.`);
4351
4441
  continue;
4352
4442
  }
4353
4443
  throw new Error(`UI task claimed completion with shallow UI surface changes after repeated retries:\n${shallowUiSurfaceNudge}`);
@@ -4358,12 +4448,15 @@ export class AgentForgeWorker extends EventEmitter {
4358
4448
  : '';
4359
4449
  if (thinUiChangeNudge) {
4360
4450
  const restoreNudge = this._formatUiQualityRestoreNudge(this._restoreUiQualityWarningPaths(thinUiChangeWarnings), nudgeCount + 1);
4451
+ const generatedResetNudge = this._formatGeneratedScopedUiResetNudge(
4452
+ this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage)
4453
+ );
4361
4454
  const scopedTargetSetNudge = this._formatScopedUiTargetSetReminder(scopeAwareUserMessage);
4362
4455
  nudgeCount++;
4363
4456
  const repairBudget = consumeUiRepairNudge('completion too-thin UI changes', thinUiChangeNudge);
4364
4457
  if (nudgeCount < UI_QUALITY_NUDGE_LIMIT) {
4365
4458
  console.log(`[${taskId}] UI completion had too-thin diff — nudging (${nudgeCount}/${UI_QUALITY_NUDGE_LIMIT}, total UI repairs ${repairBudget})`);
4366
- iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${[thinUiChangeNudge, restoreNudge, scopedTargetSetNudge].filter(Boolean).join('\n\n')}\n\nDo not mark complete until the requested scoped UI is substantively improved and visually verified.`);
4459
+ iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${[thinUiChangeNudge, restoreNudge, generatedResetNudge, scopedTargetSetNudge].filter(Boolean).join('\n\n')}\n\nDo not mark complete until the requested scoped UI is substantively improved and visually verified.`);
4367
4460
  continue;
4368
4461
  }
4369
4462
  throw new Error(`UI task claimed completion with too-thin diff after repeated retries:\n${thinUiChangeNudge}`);
@@ -4572,12 +4665,15 @@ export class AgentForgeWorker extends EventEmitter {
4572
4665
  : '';
4573
4666
  if (uiImplementationArtifactNudge) {
4574
4667
  const restoreNudge = this._formatUiQualityRestoreNudge(this._restoreUiQualityWarningPaths(uiImplementationArtifactWarnings), nudgeCount + 1);
4668
+ const generatedResetNudge = this._formatGeneratedScopedUiResetNudge(
4669
+ this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage)
4670
+ );
4575
4671
  const scopedUiTargetSetNudge = this._buildScopedUiTargetSetNudge(repoBaselines, scopeAwareUserMessage);
4576
4672
  nudgeCount++;
4577
4673
  const repairBudget = consumeUiRepairNudge('publish patch artifacts', uiImplementationArtifactNudge);
4578
4674
  if (nudgeCount < UI_QUALITY_NUDGE_LIMIT) {
4579
4675
  console.log(`[${taskId}] Publish evidence had patch artifacts — continuing (${nudgeCount}/${UI_QUALITY_NUDGE_LIMIT}, total UI repairs ${repairBudget})`);
4580
- const combinedNudge = [uiImplementationArtifactNudge, restoreNudge, scopedUiTargetSetNudge].filter(Boolean).join('\n\n');
4676
+ const combinedNudge = [uiImplementationArtifactNudge, restoreNudge, generatedResetNudge, scopedUiTargetSetNudge].filter(Boolean).join('\n\n');
4581
4677
  iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${combinedNudge}\n\nContinue from the restored files and already-inspected context, clean up the UI implementation, address every requested scoped target page, verify each target screen visually, then commit/push any additional changes before reporting delivery complete.`);
4582
4678
  continue;
4583
4679
  }
@@ -4600,12 +4696,15 @@ export class AgentForgeWorker extends EventEmitter {
4600
4696
  : '';
4601
4697
  if (shallowUiSurfaceNudge) {
4602
4698
  const restoreNudge = this._formatUiQualityRestoreNudge(this._restoreUiQualityWarningPaths(shallowUiSurfaceWarnings), nudgeCount + 1);
4699
+ const generatedResetNudge = this._formatGeneratedScopedUiResetNudge(
4700
+ this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage)
4701
+ );
4603
4702
  const scopedTargetSetNudge = this._formatScopedUiTargetSetReminder(scopeAwareUserMessage);
4604
4703
  nudgeCount++;
4605
4704
  const repairBudget = consumeUiRepairNudge('publish shallow UI surface changes', shallowUiSurfaceNudge);
4606
4705
  if (nudgeCount < UI_QUALITY_NUDGE_LIMIT) {
4607
4706
  console.log(`[${taskId}] Publish evidence had shallow UI surface diff — continuing (${nudgeCount}/${UI_QUALITY_NUDGE_LIMIT}, total UI repairs ${repairBudget})`);
4608
- iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${[shallowUiSurfaceNudge, restoreNudge, scopedTargetSetNudge].filter(Boolean).join('\n\n')}\n\nContinue from the restored files and already-inspected context, improve the requested scoped UI across the page-owned product surface, verify it visually, then commit/push any additional changes before reporting delivery complete.`);
4707
+ iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${[shallowUiSurfaceNudge, restoreNudge, generatedResetNudge, scopedTargetSetNudge].filter(Boolean).join('\n\n')}\n\nContinue from the restored files and already-inspected context, improve the requested scoped UI across the page-owned product surface, verify it visually, then commit/push any additional changes before reporting delivery complete.`);
4609
4708
  continue;
4610
4709
  }
4611
4710
  throw new Error('Publish task had shallow UI surface changes after repeated retries');
@@ -4616,12 +4715,15 @@ export class AgentForgeWorker extends EventEmitter {
4616
4715
  : '';
4617
4716
  if (thinUiChangeNudge) {
4618
4717
  const restoreNudge = this._formatUiQualityRestoreNudge(this._restoreUiQualityWarningPaths(thinUiChangeWarnings), nudgeCount + 1);
4718
+ const generatedResetNudge = this._formatGeneratedScopedUiResetNudge(
4719
+ this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage)
4720
+ );
4619
4721
  const scopedTargetSetNudge = this._formatScopedUiTargetSetReminder(scopeAwareUserMessage);
4620
4722
  nudgeCount++;
4621
4723
  const repairBudget = consumeUiRepairNudge('publish too-thin UI changes', thinUiChangeNudge);
4622
4724
  if (nudgeCount < UI_QUALITY_NUDGE_LIMIT) {
4623
4725
  console.log(`[${taskId}] Publish evidence had too-thin UI diff — continuing (${nudgeCount}/${UI_QUALITY_NUDGE_LIMIT}, total UI repairs ${repairBudget})`);
4624
- iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${[thinUiChangeNudge, restoreNudge, scopedTargetSetNudge].filter(Boolean).join('\n\n')}\n\nContinue from the restored files and already-inspected context, make the requested scoped UI improvement substantive, verify it visually, then commit/push any additional changes before reporting delivery complete.`);
4726
+ iterationMessage = withTaskContext(`The task is: "${userMessage}"\n\n${[thinUiChangeNudge, restoreNudge, generatedResetNudge, scopedTargetSetNudge].filter(Boolean).join('\n\n')}\n\nContinue from the restored files and already-inspected context, make the requested scoped UI improvement substantive, verify it visually, then commit/push any additional changes before reporting delivery complete.`);
4625
4727
  continue;
4626
4728
  }
4627
4729
  throw new Error('Publish task had too-thin UI diff after repeated retries');
@@ -4672,8 +4774,11 @@ export class AgentForgeWorker extends EventEmitter {
4672
4774
  throw new Error('UI task failed visual verification after repeated repair attempts');
4673
4775
  }
4674
4776
  const repairBudget = consumeUiRepairNudge('visual verification warnings', visualVerificationFailureNudge);
4777
+ const visualFailureSlugs = this._extractUiVerificationFailureSlugs(output, scopeAwareUserMessage);
4675
4778
  const generatedResetNudge = this._formatGeneratedScopedUiResetNudge(
4676
- this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage)
4779
+ visualFailureSlugs.length > 0
4780
+ ? this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage, { onlySlugs: visualFailureSlugs })
4781
+ : []
4677
4782
  );
4678
4783
  nudgeCount = 0;
4679
4784
  console.log(`[${taskId}] UI task visual verification still reported visible issues — retrying (${uiVerificationRetryCount}/${UI_REPAIR_NUDGE_LIMIT}, total UI repairs ${repairBudget})`);
@@ -4689,8 +4794,11 @@ export class AgentForgeWorker extends EventEmitter {
4689
4794
  throw new Error('UI task failed visual verification after repeated repair attempts');
4690
4795
  }
4691
4796
  const repairBudget = consumeUiRepairNudge('missing local visual verification', uiVerificationFailureDetails);
4797
+ const visualFailureSlugs = this._extractUiVerificationFailureSlugs(output, scopeAwareUserMessage);
4692
4798
  const generatedResetNudge = this._formatGeneratedScopedUiResetNudge(
4693
- this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage)
4799
+ visualFailureSlugs.length > 0
4800
+ ? this._restoreGeneratedScopedUiTargets(repoBaselines, scopeAwareUserMessage, { onlySlugs: visualFailureSlugs })
4801
+ : []
4694
4802
  );
4695
4803
  nudgeCount = 0;
4696
4804
  console.log(`[${taskId}] UI task missing local visual verification — retrying with local-app repair instruction (${uiVerificationRetryCount}/${UI_REPAIR_NUDGE_LIMIT}, total UI repairs ${repairBudget})`);
@@ -5783,7 +5891,7 @@ end tell`.trim().replace(/\n/g, '\n');
5783
5891
  .split('\n')
5784
5892
  .map(line => line.trim())
5785
5893
  .filter(line =>
5786
- /^(?:Style|Visual) warning:/i.test(line) ||
5894
+ /(?:^|[\s:])(?:Style|Visual) warning:/i.test(line) ||
5787
5895
  /Browser failed to load/i.test(line) ||
5788
5896
  /changed UI was not successfully loaded and inspected/i.test(line) ||
5789
5897
  /latest visual verification still reported/i.test(line) ||
@@ -5793,6 +5901,29 @@ end tell`.trim().replace(/\n/g, '\n');
5793
5901
  return unique.join('\n').slice(0, 1800);
5794
5902
  }
5795
5903
 
5904
+ _extractUiVerificationFailureSlugs(output, userMessage) {
5905
+ const { slugs: allowedSlugs } = this._extractExplicitScope(userMessage);
5906
+ if (allowedSlugs.length === 0) return [];
5907
+ const lines = String(output || '')
5908
+ .split('\n')
5909
+ .map(line => line.trim())
5910
+ .filter(line =>
5911
+ /(?:^|[\s:])(?:Style|Visual) warning:/i.test(line) ||
5912
+ /Browser failed to load/i.test(line) ||
5913
+ /changed UI was not successfully loaded and inspected/i.test(line) ||
5914
+ /latest visual verification still reported/i.test(line) ||
5915
+ /This is not complete/i.test(line)
5916
+ );
5917
+ if (lines.length === 0) return [];
5918
+ const matched = new Set();
5919
+ for (const line of lines) {
5920
+ for (const slug of this._scopeSlugsMatchingText(line, allowedSlugs)) {
5921
+ matched.add(slug);
5922
+ }
5923
+ }
5924
+ return [...matched];
5925
+ }
5926
+
5796
5927
  _buildUiVerificationFailureNudge(output) {
5797
5928
  const details = this._extractUiVerificationFailureDetails(output);
5798
5929
  const genericFailure =