@hamp10/agentforge 0.2.24 → 0.2.26

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.24",
3
+ "version": "0.2.26",
4
4
  "description": "AgentForge worker — connect your machine to agentforge.ai",
5
5
  "type": "module",
6
6
  "bin": {
@@ -648,8 +648,8 @@ try {
648
648
  ));
649
649
  assert.match(
650
650
  cli._detectScreenshotVisualDiscontinuities(seamPng).join('\n'),
651
- /hard-edged/i,
652
- 'screenshot discontinuity scan should catch large hard-edged rectangular seams'
651
+ /screenshot continuity break/i,
652
+ 'screenshot discontinuity scan should catch large rectangular visual discontinuities'
653
653
  );
654
654
  const subtlePartialSurfacePng = path.join(fixture.repo, 'subtle-partial-surface.png');
655
655
  writeRgbPng(subtlePartialSurfacePng, 1000, 700, (x, y) => {
@@ -658,8 +658,8 @@ try {
658
658
  });
659
659
  assert.match(
660
660
  cli._detectScreenshotVisualDiscontinuities(subtlePartialSurfacePng).join('\n'),
661
- /partial-surface hard-edged background\/tint block edge/i,
662
- 'screenshot discontinuity scan should catch large subtle partial-surface tint blocks'
661
+ /partial-surface screenshot continuity break/i,
662
+ 'screenshot discontinuity scan should catch large subtle partial-surface visual discontinuities'
663
663
  );
664
664
  } finally {
665
665
  rmSync(fixture.repo, { recursive: true, force: true });
@@ -668,6 +668,7 @@ try {
668
668
  const openClawSource = readFileSync(new URL('../src/OpenClawCLI.js', import.meta.url), 'utf-8');
669
669
  const workerSource = readFileSync(new URL('../src/worker.js', import.meta.url), 'utf-8');
670
670
  const workerBinSource = readFileSync(new URL('../bin/agentforge.js', import.meta.url), 'utf-8');
671
+ const selfUpdateSource = readFileSync(new URL('../src/selfUpdate.js', import.meta.url), 'utf-8');
671
672
  const defaultGuidesSource = readFileSync(new URL('../src/default-task-guides.js', import.meta.url), 'utf-8');
672
673
  const browserSource = readFileSync(new URL('../src/browser.js', import.meta.url), 'utf-8');
673
674
  const previewServerSource = readFileSync(new URL('../src/preview-server.js', import.meta.url), 'utf-8');
@@ -690,13 +691,13 @@ assert.match(
690
691
  );
691
692
  assert.match(
692
693
  openClawSource,
693
- /large hard-edged rectangular background\/overlay boundary across a dominant UI surface/i,
694
- 'screenshot discontinuity detection should fail large hard-edged visual breaks'
694
+ /visible screenshot continuity break across a dominant UI surface/i,
695
+ 'screenshot discontinuity detection should fail large visual continuity breaks'
695
696
  );
696
697
  assert.match(
697
698
  openClawSource,
698
- /partial-surface hard-edged background\/tint block edge/i,
699
- 'screenshot discontinuity detection should fail subtle partial-surface tint blocks'
699
+ /partial-surface screenshot continuity break/i,
700
+ 'screenshot discontinuity detection should fail subtle partial-surface visual continuity breaks'
700
701
  );
701
702
  assert.match(
702
703
  openClawSource,
@@ -838,16 +839,41 @@ assert.match(
838
839
  /app\.delete\('\/api\/agents\/delete\/cleanup-empty'[\s\S]*activeTasks[\s\S]*type: 'agents_pruned'/i,
839
840
  'empty-agent cleanup should preserve live tasks and broadcast removed agents'
840
841
  );
842
+ assert.match(
843
+ serverSource,
844
+ /CREATE TABLE IF NOT EXISTS agent_flows[\s\S]*config JSONB DEFAULT NULL/i,
845
+ 'agent flow emergency schema patch should include config so model routing survives skipped migrations'
846
+ );
847
+ assert.match(
848
+ serverSource,
849
+ /ALTER TABLE agent_flows ADD COLUMN IF NOT EXISTS config JSONB DEFAULT NULL/i,
850
+ 'agent flow startup should self-heal missing config column'
851
+ );
852
+ assert.match(
853
+ serverSource,
854
+ /ON CONFLICT \(id\)[\s\S]*WHERE agent_flows\.user_id = EXCLUDED\.user_id[\s\S]*RETURNING id/i,
855
+ 'flow upserts should not update another user flow with the same id'
856
+ );
841
857
  assert.match(
842
858
  dashboardSource,
843
859
  /confirmDeleteAgent[\s\S]*await fetch[\s\S]*removeAgentFromLocalState/i,
844
860
  'single-agent delete should only remove local UI after the server confirms deletion'
845
861
  );
862
+ assert.match(
863
+ dashboardSource,
864
+ /confirmPruneAgents[\s\S]*\/api\/agents\/delete\/prune\?keep=15[\s\S]*removeAgentFromLocalState/i,
865
+ 'dashboard should expose old-agent pruning through the safe keep-latest endpoint'
866
+ );
846
867
  assert.match(
847
868
  dashboardSource,
848
869
  /function finalizeLiveFeedBeforeGuide[\s\S]*commitChunkBuffer[\s\S]*hideTypingIndicator/i,
849
870
  'live Guide messages should close the current visible agent feed before rendering the user bubble'
850
871
  );
872
+ assert.match(
873
+ selfUpdateSource,
874
+ /ensureWritableGlobalInstallEnv[\s\S]*canWriteNpmRoot[\s\S]*configureUserOwnedNpmGlobalEnv/i,
875
+ 'self-update should preflight npm global permissions before printing a failed system-prefix install'
876
+ );
851
877
  assert.match(
852
878
  dashboardSource,
853
879
  /async function sendGuideMessage[\s\S]*agent\._guideInflight[\s\S]*finalizeLiveFeedBeforeGuide\(agentId\)[\s\S]*addMessage/i,
@@ -953,11 +979,31 @@ assert.doesNotMatch(
953
979
  /taskRequiresVisualVerification\s*&&\s*directIteration\s*<=\s*1\s*&&\s*explicitScopeForTask\.pageOnly/s,
954
980
  'comparable UI context gate should apply on retries, not only first iteration'
955
981
  );
982
+ assert.match(
983
+ openClawSource,
984
+ /read-only project context, not as a target page/,
985
+ 'read-only comparable page inspection should not be narrated as target-page work'
986
+ );
987
+ assert.match(
988
+ openClawSource,
989
+ /verifiedLocalUrl = isLocalUiUrl\(resultUrl \|\| url\)/,
990
+ 'scoped auto-verification should count a clean inferred local target URL even when browser output formatting is imperfect'
991
+ );
956
992
  assert.match(
957
993
  openClawSource,
958
994
  /const minRatio = isLargeText \? 3 : 4\.5;/,
959
995
  'control/link contrast checks should use text-size-aware WCAG thresholds'
960
996
  );
997
+ assert.match(
998
+ dashboardSource,
999
+ /typingEl\.dataset\.startedAt\s*=\s*startedAt/,
1000
+ 'live guide insertion should preserve the active feed start time for chronological reload ordering'
1001
+ );
1002
+ assert.match(
1003
+ dashboardSource,
1004
+ /time:\s*startedAt/,
1005
+ 'completed live feeds should save with their start time so guide bubbles appear after the feed they interrupted'
1006
+ );
961
1007
  assert.match(
962
1008
  defaultGuidesSource,
963
1009
  /anything that reads as rendering damage rather than deliberate design/i,
@@ -3128,7 +3128,7 @@ export class OpenClawCLI extends EventEmitter {
3128
3128
  horizontal.y < height * 0.94;
3129
3129
  if (!awayFromViewportEdge) continue;
3130
3130
  warnings.push([
3131
- `large hard-edged rectangular background/overlay boundary across a dominant UI surface near x=${vertical.x}, y=${horizontal.y}`,
3131
+ `visible screenshot continuity break across a dominant UI surface near x=${vertical.x}, y=${horizontal.y}`,
3132
3132
  `(vertical span y=${vertical.minY}-${vertical.maxY}, horizontal span x=${horizontal.minX}-${horizontal.maxX})`,
3133
3133
  ].join(' '));
3134
3134
  break;
@@ -3144,7 +3144,7 @@ export class OpenClawCLI extends EventEmitter {
3144
3144
  edge.average >= 38
3145
3145
  );
3146
3146
  if (dominantVertical) {
3147
- warnings.push(`large hard-edged vertical background/overlay seam across a dominant UI surface near x=${dominantVertical.x} spanning y=${dominantVertical.minY}-${dominantVertical.maxY}`);
3147
+ warnings.push(`visible vertical screenshot continuity break across a dominant UI surface near x=${dominantVertical.x} spanning y=${dominantVertical.minY}-${dominantVertical.maxY}`);
3148
3148
  }
3149
3149
  }
3150
3150
 
@@ -3197,7 +3197,7 @@ export class OpenClawCLI extends EventEmitter {
3197
3197
  }
3198
3198
 
3199
3199
  if (partialHorizontal) {
3200
- warnings.push(`large partial-surface hard-edged background/tint block edge near y=${partialHorizontal.y}, running from x=${partialHorizontal.start} to x=${partialHorizontal.end}. This often indicates an unintended rectangular overlay, cropped layer, or mismatched background plane.`);
3200
+ warnings.push(`visible partial-surface screenshot continuity break near y=${partialHorizontal.y}, running from x=${partialHorizontal.start} to x=${partialHorizontal.end}`);
3201
3201
  }
3202
3202
  }
3203
3203
 
@@ -3249,7 +3249,7 @@ export class OpenClawCLI extends EventEmitter {
3249
3249
  }
3250
3250
 
3251
3251
  if (partialVertical) {
3252
- warnings.push(`large partial-surface hard-edged background/tint block edge near x=${partialVertical.x}, running from y=${partialVertical.start} to y=${partialVertical.end}. This often indicates an unintended rectangular overlay, cropped layer, or mismatched background plane.`);
3252
+ warnings.push(`visible partial-surface screenshot continuity break near x=${partialVertical.x}, running from y=${partialVertical.start} to y=${partialVertical.end}`);
3253
3253
  }
3254
3254
  }
3255
3255
  return warnings;
@@ -3850,7 +3850,7 @@ export class OpenClawCLI extends EventEmitter {
3850
3850
  ? `${visualWarningPrefix}: ${summary.brokenImages.length} visible image(s) failed to load: ${summary.brokenImages.map(img => img.alt || img.src || '(unlabeled image)').join(', ')}. ${visualWarningTail}`
3851
3851
  : '';
3852
3852
  const screenshotDiscontinuityWarning = screenshotDiscontinuityWarnings.length > 0
3853
- ? `${visualWarningPrefix}: screenshot shows ${screenshotDiscontinuityWarnings.slice(0, 2).join('; ')}. Rework the affected layout/background/layering so the changed screen reads as intentional, then verify the page again. ${visualWarningTail}`
3853
+ ? `${visualWarningPrefix}: screenshot pixel scan flagged visible continuity issue(s): ${screenshotDiscontinuityWarnings.slice(0, 2).join('; ')}. Inspect the screenshot and source to identify the actual rendering cause, then rework the affected layout/background/layering so the changed screen reads as intentional. Verify the page again. ${visualWarningTail}`
3854
3854
  : '';
3855
3855
  const reportLowContrastControls = options?.afterMutation && isScopedBrowserTarget && explicitScopeSlugs.length > 0
3856
3856
  ? (summary.lowContrastControls || []).filter(c => !c.chrome)
@@ -4629,8 +4629,10 @@ export class OpenClawCLI extends EventEmitter {
4629
4629
  recordDirectToolSummary('browser', result);
4630
4630
  continue;
4631
4631
  }
4632
- rememberLocalScopedUrl(browserResultUrl(result));
4633
- if (isLocalUiVerificationResult(result)) {
4632
+ const resultUrl = browserResultUrl(result);
4633
+ rememberLocalScopedUrl(resultUrl);
4634
+ const verifiedLocalUrl = isLocalUiUrl(resultUrl || url);
4635
+ if (isLocalUiVerificationResult(result) || verifiedLocalUrl) {
4634
4636
  recordCleanLocalVerification(result, [slug]);
4635
4637
  verifiedAny = true;
4636
4638
  }
@@ -4766,6 +4768,13 @@ export class OpenClawCLI extends EventEmitter {
4766
4768
  return `I am checking the live browser${url} to judge the actual product surface before deciding the next change.`;
4767
4769
  }
4768
4770
  if (name === 'read_file' || name === 'read') {
4771
+ const scope = extractExplicitScope(task);
4772
+ const targetText = String(targetPath || base || '').toLowerCase();
4773
+ const looksLikePageSource = /\.(html?|css|s[ac]ss|jsx?|tsx?|vue|svelte|astro|mdx?)$/i.test(targetText);
4774
+ const isNamedTarget = scope.slugs.length > 0 && scopeSlugsMatchingText(targetText, scope.slugs).length > 0;
4775
+ if (base && looksLikePageSource && scope.slugs.length > 0 && !isNamedTarget) {
4776
+ return `I am reading ${base} as read-only project context, not as a target page.`;
4777
+ }
4769
4778
  return base
4770
4779
  ? `I am reading ${base} to work from the current implementation instead of guessing.`
4771
4780
  : `I am reading the current code before making the next change.`;
@@ -5097,6 +5106,10 @@ export class OpenClawCLI extends EventEmitter {
5097
5106
  lastDirectVisualWarning = visualWarning;
5098
5107
  } else if (!taskRequiresVisualVerification || isLocalUiVerificationResult(result)) {
5099
5108
  recordCleanLocalVerification(result);
5109
+ } else {
5110
+ const verifiedUrl = resultUrl || args?.url || '';
5111
+ const verifiedSlugs = isLocalUiUrl(verifiedUrl) ? scopeSlugsInText(verifiedUrl) : [];
5112
+ if (verifiedSlugs.length > 0) recordCleanLocalVerification(result, verifiedSlugs);
5100
5113
  }
5101
5114
  }
5102
5115
  }
package/src/selfUpdate.js CHANGED
@@ -6,8 +6,8 @@
6
6
  * prevent infinite re-exec loops).
7
7
  */
8
8
 
9
- import { execSync, spawn } from 'child_process';
10
- import { mkdirSync } from 'fs';
9
+ import { execFileSync, execSync, spawn } from 'child_process';
10
+ import { mkdirSync, rmSync } from 'fs';
11
11
  import { homedir } from 'os';
12
12
  import path from 'path';
13
13
 
@@ -75,22 +75,58 @@ export async function checkAndUpdate(packageName, currentVersion) {
75
75
 
76
76
  function installGlobalPackage(pkg, { force = false } = {}) {
77
77
  const forceFlag = force ? ' --force' : '';
78
+ const preflightEnv = ensureWritableGlobalInstallEnv();
78
79
  try {
79
- execSync(`npm install -g${forceFlag} ${pkg}`, { stdio: 'inherit' });
80
+ execSync(`npm install -g${forceFlag} ${pkg}`, { stdio: 'inherit', env: preflightEnv });
80
81
  return;
81
82
  } catch (error) {
82
83
  if (!looksLikePermissionFailure(error)) throw error;
83
84
  }
84
85
 
86
+ const env = configureUserOwnedNpmGlobalEnv();
87
+ execSync(`npm install -g${forceFlag} ${pkg}`, { stdio: 'inherit', env });
88
+ }
89
+
90
+ function npmConfigGet(args) {
91
+ try {
92
+ return execFileSync('npm', args, {
93
+ encoding: 'utf8',
94
+ stdio: ['ignore', 'pipe', 'ignore'],
95
+ }).trim();
96
+ } catch {
97
+ return '';
98
+ }
99
+ }
100
+
101
+ function canWriteNpmRoot(rootDir) {
102
+ if (!rootDir) return false;
103
+ const testDir = path.join(rootDir, `.agentforge-write-test-${process.pid}`);
104
+ try {
105
+ mkdirSync(testDir, { recursive: true });
106
+ rmSync(testDir, { recursive: true, force: true });
107
+ return true;
108
+ } catch {
109
+ try { rmSync(testDir, { recursive: true, force: true }); } catch {}
110
+ return false;
111
+ }
112
+ }
113
+
114
+ function ensureWritableGlobalInstallEnv() {
115
+ const root = npmConfigGet(['root', '-g']);
116
+ if (canWriteNpmRoot(root)) return process.env;
117
+ return configureUserOwnedNpmGlobalEnv();
118
+ }
119
+
120
+ function configureUserOwnedNpmGlobalEnv() {
85
121
  const prefix = path.join(homedir(), '.npm-global');
86
122
  mkdirSync(path.join(prefix, 'bin'), { recursive: true });
87
123
  mkdirSync(path.join(prefix, 'lib', 'node_modules'), { recursive: true });
88
- execSync(`npm config set prefix "${prefix}"`, { stdio: 'inherit' });
124
+ execFileSync('npm', ['config', 'set', 'prefix', prefix], { stdio: ['ignore', 'ignore', 'inherit'] });
89
125
  const env = {
90
126
  ...process.env,
91
127
  PATH: `${path.join(prefix, 'bin')}${path.delimiter}${process.env.PATH || ''}`,
92
128
  };
93
- execSync(`npm install -g${forceFlag} ${pkg}`, { stdio: 'inherit', env });
129
+ return env;
94
130
  }
95
131
 
96
132
  function looksLikePermissionFailure(error) {