@hamp10/agentforge 0.2.21 → 0.2.23
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/bin/agentforge.js +909 -115
- package/package.json +2 -1
- package/scripts/check-task-semantics.js +911 -0
- package/scripts/postinstall.js +20 -5
- package/src/OllamaAgent.js +1178 -246
- package/src/OpenClawCLI.js +5897 -748
- package/src/browser.js +392 -0
- package/src/default-task-guides.js +95 -0
- package/src/resolveOpenclaw.js +38 -7
- package/src/selfUpdate.js +31 -3
- package/src/supervisor.js +88 -20
- package/src/taskSemantics.js +141 -0
- package/src/worker.js +4257 -230
- package/templates/agent/AGENTFORGE.md +151 -53
- package/templates/hooks/agentforge-platform/handler.js +322 -0
- package/src/HampAgentCLI.js +0 -125
- package/src/hampagent/browser.js +0 -321
- package/src/hampagent/runner.js +0 -277
- package/src/hampagent/sessions.js +0 -62
- package/src/hampagent/tools.js +0 -298
package/src/supervisor.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* worker code path is unchanged — only the lifecycle wrapper is new.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { spawn } from 'child_process';
|
|
13
|
+
import { spawn, execFileSync } from 'child_process';
|
|
14
14
|
import { mkdirSync, writeFileSync, readFileSync, unlinkSync, existsSync } from 'fs';
|
|
15
15
|
import { homedir } from 'os';
|
|
16
16
|
import path from 'path';
|
|
@@ -32,25 +32,80 @@ function removePid(file) {
|
|
|
32
32
|
try { unlinkSync(file); } catch {}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Kill a PID read from a file (if it exists and is still alive).
|
|
37
|
+
* Swallows errors — the process may already be gone.
|
|
38
|
+
*/
|
|
39
|
+
function killPidFile(file) {
|
|
40
|
+
try {
|
|
41
|
+
if (!existsSync(file)) return;
|
|
42
|
+
const pid = parseInt(readFileSync(file, 'utf8'));
|
|
43
|
+
if (pid && pid !== process.pid) {
|
|
44
|
+
try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
45
|
+
}
|
|
46
|
+
removePid(file);
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function killStaleAgentForgeProcesses() {
|
|
51
|
+
try {
|
|
52
|
+
const thisScript = process.argv[1] || '';
|
|
53
|
+
if (!thisScript) return;
|
|
54
|
+
const keep = new Set([process.pid, process.ppid].filter(Boolean).map(String));
|
|
55
|
+
const output = execFileSync('ps', ['-axo', 'pid=,ppid=,command='], { encoding: 'utf8' });
|
|
56
|
+
for (const rawLine of output.split('\n')) {
|
|
57
|
+
const line = rawLine.trim();
|
|
58
|
+
if (!line) continue;
|
|
59
|
+
const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
60
|
+
if (!match) continue;
|
|
61
|
+
const [, pid, , command] = match;
|
|
62
|
+
if (keep.has(pid)) continue;
|
|
63
|
+
if (!command.includes(thisScript)) continue;
|
|
64
|
+
if (!/\bstart\b/.test(command)) continue;
|
|
65
|
+
try { process.kill(Number(pid), 'SIGTERM'); } catch {}
|
|
66
|
+
}
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if there is already a supervisor running. Kill it (and its worker)
|
|
72
|
+
* before starting a new one so multiple agentforge start calls never stack up.
|
|
73
|
+
*/
|
|
74
|
+
function evictExistingWorker() {
|
|
75
|
+
killPidFile(WORKER_PID_FILE);
|
|
76
|
+
killPidFile(PID_FILE);
|
|
77
|
+
killStaleAgentForgeProcesses();
|
|
78
|
+
}
|
|
79
|
+
|
|
35
80
|
/**
|
|
36
81
|
* Run the supervisor in the foreground. Blocks until a clean exit (code 0).
|
|
37
82
|
* @param {string[]} innerArgv argv to pass to the worker process (must include --no-daemon)
|
|
38
83
|
*/
|
|
39
84
|
export async function runSupervisor(innerArgv) {
|
|
85
|
+
// Kill any already-running supervisor+worker before taking over.
|
|
86
|
+
// This prevents multiple workers from connecting when the user runs
|
|
87
|
+
// `agentforge start` more than once without stopping first.
|
|
88
|
+
evictExistingWorker();
|
|
89
|
+
|
|
40
90
|
writePid(PID_FILE, process.pid);
|
|
41
91
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
92
|
+
let currentChild = null;
|
|
93
|
+
|
|
94
|
+
const shutdown = (signal) => {
|
|
95
|
+
console.log(`[supervisor] Received ${signal} — shutting down`);
|
|
96
|
+
// Kill the worker child so it doesn't outlive the supervisor (macOS orphan issue)
|
|
97
|
+
if (currentChild && !currentChild.killed) {
|
|
98
|
+
try { currentChild.kill('SIGTERM'); } catch {}
|
|
99
|
+
}
|
|
100
|
+
removePid(WORKER_PID_FILE);
|
|
45
101
|
removePid(PID_FILE);
|
|
46
102
|
process.exit(0);
|
|
47
|
-
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// SIGTERM on supervisor = intentional stop (from agentforge stop command)
|
|
106
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
48
107
|
// SIGINT = Ctrl+C in foreground terminal = intentional stop
|
|
49
|
-
process.on('SIGINT', () =>
|
|
50
|
-
console.log('[supervisor] Received SIGINT — shutting down');
|
|
51
|
-
removePid(PID_FILE);
|
|
52
|
-
process.exit(0);
|
|
53
|
-
});
|
|
108
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
54
109
|
|
|
55
110
|
let consecutiveCrashes = 0;
|
|
56
111
|
|
|
@@ -63,6 +118,7 @@ export async function runSupervisor(innerArgv) {
|
|
|
63
118
|
env: { ...process.env, AGENTFORGE_SKIP_UPDATE: '1' },
|
|
64
119
|
});
|
|
65
120
|
|
|
121
|
+
currentChild = child;
|
|
66
122
|
writePid(WORKER_PID_FILE, child.pid);
|
|
67
123
|
|
|
68
124
|
const exitCode = await new Promise(resolve => {
|
|
@@ -98,6 +154,9 @@ export async function runSupervisor(innerArgv) {
|
|
|
98
154
|
* @param {string[]} supervisorArgv argv for the background supervisor (without --detach)
|
|
99
155
|
*/
|
|
100
156
|
export function detachSupervisor(supervisorArgv) {
|
|
157
|
+
// Kill any already-running supervisor+worker before starting a new one.
|
|
158
|
+
evictExistingWorker();
|
|
159
|
+
|
|
101
160
|
const logFile = path.join(CONFIG_DIR, 'worker.log');
|
|
102
161
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
103
162
|
|
|
@@ -119,17 +178,26 @@ export function detachSupervisor(supervisorArgv) {
|
|
|
119
178
|
* Stop a running background supervisor by sending SIGTERM to the PID in the pid file.
|
|
120
179
|
*/
|
|
121
180
|
export function stopSupervisor() {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
181
|
+
let stopped = false;
|
|
182
|
+
// Kill worker first (so it doesn't get orphaned if supervisor dies first)
|
|
183
|
+
if (existsSync(WORKER_PID_FILE)) {
|
|
184
|
+
try {
|
|
185
|
+
const pid = parseInt(readFileSync(WORKER_PID_FILE, 'utf8'));
|
|
186
|
+
if (pid) { process.kill(pid, 'SIGTERM'); stopped = true; }
|
|
187
|
+
} catch {}
|
|
188
|
+
removePid(WORKER_PID_FILE);
|
|
125
189
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
console.log('Supervisor not running or already stopped.');
|
|
190
|
+
// Kill supervisor
|
|
191
|
+
if (existsSync(PID_FILE)) {
|
|
192
|
+
try {
|
|
193
|
+
const pid = parseInt(readFileSync(PID_FILE, 'utf8'));
|
|
194
|
+
if (pid) { process.kill(pid, 'SIGTERM'); stopped = true; }
|
|
195
|
+
} catch {}
|
|
133
196
|
removePid(PID_FILE);
|
|
134
197
|
}
|
|
198
|
+
if (stopped) {
|
|
199
|
+
console.log('✅ AgentForge worker stopped.');
|
|
200
|
+
} else {
|
|
201
|
+
console.log('No running worker found.');
|
|
202
|
+
}
|
|
135
203
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const PAGE_KIND_RE = /\b(?:pages?|screens?|routes?|views?|listings?)\b/i;
|
|
2
|
+
const PAGE_KIND_WORD_RE = /^(?:listing|listings|page|pages|screen|screens|route|routes|view|views|site|website|app)$/i;
|
|
3
|
+
const ACTION_WORD_RE = /^(?:add|build|change|create|edit|fix|implement|improve|make|redesign|update|work)$/i;
|
|
4
|
+
|
|
5
|
+
const GENERIC_SCOPE_WORDS = new Set([
|
|
6
|
+
'a', 'an', 'the', 'and', 'or', 'page', 'pages', 'screen', 'screens',
|
|
7
|
+
'route', 'routes', 'view', 'views', 'listing', 'listings', 'site',
|
|
8
|
+
'website', 'app', 'readability', 'readable', 'legibility', 'contrast',
|
|
9
|
+
'quality', 'visual', 'content', 'layout', 'styling', 'polish',
|
|
10
|
+
'current', 'target', 'targets', 'requested', 'same', 'rest', 'live',
|
|
11
|
+
'local', 'itself', 'only', 'those', 'these', 'this', 'that', 'two',
|
|
12
|
+
'both', 'change', 'edit', 'modify', 'touch', 'make', 'improve', 'fix',
|
|
13
|
+
'update', 'redesign', 'owned', 'source', 'style', 'styles', 'global',
|
|
14
|
+
'shared', 'reference', 'references', 'broaden', 'work', 'wrong', 'bad',
|
|
15
|
+
'broken',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const followedByNamedPageTargets = (text, match) => {
|
|
19
|
+
const after = text.slice(match.index + match[0].length);
|
|
20
|
+
return /^\s+(?:listing\s+)?(?:pages?|screens?|routes?|views?|listings?)\s+(?:for|of|called|named)\b/i.test(after);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function scopeSlug(value) {
|
|
24
|
+
return String(value || '')
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/^https?:\/\//, '')
|
|
27
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
28
|
+
.replace(/^-+|-+$/g, '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function scopeExtractionText(message) {
|
|
32
|
+
const raw = String(message || '');
|
|
33
|
+
const taskMatches = [...raw.matchAll(/\b(?:The task is|Continue with the task):\s*"([^"]{1,4000})"/gi)];
|
|
34
|
+
if (taskMatches.length > 0) {
|
|
35
|
+
const taskText = taskMatches.at(-1)[1];
|
|
36
|
+
const resolvedScopeBlocks = [...raw.matchAll(/\[Resolved (?:task|target) scope[^\]]*\]/gi)]
|
|
37
|
+
.map(match => match[0]);
|
|
38
|
+
return [taskText, ...resolvedScopeBlocks].join('\n');
|
|
39
|
+
}
|
|
40
|
+
return raw;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function hasExplicitScopeRestriction(message) {
|
|
44
|
+
return /\b(do not (?:modify|change|edit|touch) anything else|don't (?:modify|change|edit|touch) anything else|only (?:touch|modify|change|edit|work on)|work only on|nothing else)\b/i.test(scopeExtractionText(message));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function addScopeCandidate(candidates, value) {
|
|
48
|
+
const parts = String(value || '')
|
|
49
|
+
.replace(/\b(?:make|keep|commit|push|verify|test|do\s+not|don't|change|edit|modify|touch|improve|fix|update|redesign)\b[\s\S]*$/i, '')
|
|
50
|
+
.replace(/(.+?)\s+\b(?:the|a|an|this|that|these|those)\b(?=[\s\S]*\b(?:is|are|was|were|looks?|feels?|seems?|needs?|need|should|must|has|have)\b)[\s\S]*$/i, '$1')
|
|
51
|
+
.replace(/\s+\b(?:is|are|was|were|looks?|feels?|seems?|needs?|need|should|must|has|have)\b[\s\S]*$/i, '')
|
|
52
|
+
.split(/\s*(?:,|&|\band\b|\bor\b)\s*/i);
|
|
53
|
+
|
|
54
|
+
for (const part of parts) {
|
|
55
|
+
const cleaned = part
|
|
56
|
+
.replace(/(.+?)\s+\b(?:the|a|an|this|that|these|those)\b[\s\S]*$/i, '$1')
|
|
57
|
+
.replace(/\s+\b(?:is|are|was|were|looks?|feels?|seems?|needs?|need|should|must|has|have)\b[\s\S]*$/i, '')
|
|
58
|
+
.replace(/\b(?:a|an|the|page|pages|screen|screens|route|routes|view|views|listing|listings|site|website|app|readability|readable|legibility|contrast|quality|visual|content|layout|styling|polish|current|target|targets|requested|same|rest|live|local|itself|only|those|these|this|that|two|both|change|edit|modify|touch|make|improve|fix|update|redesign|owned|source|style|styles|global|shared|reference|references|broaden|work|off|wrong|bad|broken)\b/gi, ' ')
|
|
59
|
+
.trim();
|
|
60
|
+
const slug = scopeSlug(cleaned);
|
|
61
|
+
if (slug.length >= 3 && !GENERIC_SCOPE_WORDS.has(slug)) candidates.add(slug);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function extractNamedPageScopeSlugs(message) {
|
|
66
|
+
const text = scopeExtractionText(message);
|
|
67
|
+
if (!PAGE_KIND_RE.test(text)) return [];
|
|
68
|
+
|
|
69
|
+
const candidates = new Set();
|
|
70
|
+
for (const match of text.matchAll(/\b[a-z0-9]+(?:[.-][a-z0-9]+)+\b/gi)) {
|
|
71
|
+
if (followedByNamedPageTargets(text, match)) continue;
|
|
72
|
+
addScopeCandidate(candidates, match[0]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const beforePage = /\b(?:on|for|of|called|named)\s+(?:the\s+)?([a-z0-9][a-z0-9 _/-]{0,100}?)\s+(?:pages?|screens?|routes?|views?|listings?)\b/gi;
|
|
76
|
+
for (const match of text.matchAll(beforePage)) {
|
|
77
|
+
const after = text.slice(match.index + match[0].length);
|
|
78
|
+
if (/\blistings?\b/i.test(match[1]) && /^\s+(?:for|of)\b/i.test(after)) continue;
|
|
79
|
+
addScopeCandidate(candidates, match[1]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const directBeforePage = /\b([a-z0-9][a-z0-9._/-]{2,80})\s+(?:pages?|screens?|routes?|views?|listings?)\b/gi;
|
|
83
|
+
for (const match of text.matchAll(directBeforePage)) {
|
|
84
|
+
addScopeCandidate(candidates, match[1]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
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|with|without|that|which|where|when|because|so|but)\b)/gi;
|
|
88
|
+
for (const match of text.matchAll(afterPage)) {
|
|
89
|
+
const before = text.slice(Math.max(0, match.index - 120), match.index);
|
|
90
|
+
const precedingToken = before.match(/\b([a-z0-9][a-z0-9._/-]{2,})\s*$/i)?.[1] || '';
|
|
91
|
+
if (precedingToken && !PAGE_KIND_WORD_RE.test(precedingToken) && !ACTION_WORD_RE.test(precedingToken)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
addScopeCandidate(candidates, match[1]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const match of text.matchAll(/\b[a-z0-9]+(?:[.-][a-z0-9]+)+\b/gi)) {
|
|
98
|
+
const after = text.slice(match.index + match[0].length);
|
|
99
|
+
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|with|without|that|which|where|when|because|so|but)\b)/i);
|
|
100
|
+
if (pageTarget) {
|
|
101
|
+
const targetCandidates = new Set();
|
|
102
|
+
addScopeCandidate(targetCandidates, pageTarget[1]);
|
|
103
|
+
targetCandidates.delete(scopeSlug(match[0]));
|
|
104
|
+
if (targetCandidates.size === 0) continue;
|
|
105
|
+
candidates.delete(scopeSlug(match[0]));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return [...candidates];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function extractExplicitScopeSlugs(message) {
|
|
113
|
+
const text = scopeExtractionText(message);
|
|
114
|
+
if (!hasExplicitScopeRestriction(text)) {
|
|
115
|
+
return extractNamedPageScopeSlugs(text);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const candidates = new Set();
|
|
119
|
+
for (const match of text.matchAll(/\b[a-z0-9]+(?:[.-][a-z0-9]+)+\b/gi)) {
|
|
120
|
+
if (followedByNamedPageTargets(text, match)) continue;
|
|
121
|
+
addScopeCandidate(candidates, match[0]);
|
|
122
|
+
}
|
|
123
|
+
for (const slug of extractNamedPageScopeSlugs(text)) {
|
|
124
|
+
candidates.add(slug);
|
|
125
|
+
}
|
|
126
|
+
return [...candidates];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function extractExplicitScope(message) {
|
|
130
|
+
const text = scopeExtractionText(message);
|
|
131
|
+
const slugs = extractExplicitScopeSlugs(text);
|
|
132
|
+
const inferredNamedPageScope = !hasExplicitScopeRestriction(text) && slugs.length > 0;
|
|
133
|
+
const pageOnly =
|
|
134
|
+
slugs.length > 0 &&
|
|
135
|
+
(
|
|
136
|
+
inferredNamedPageScope ||
|
|
137
|
+
/\bonly\s+(?:touch|modify|change|edit|work on)\b[\s\S]{0,120}\b(?:pages?|screens?|routes?|views?)\b/i.test(text) ||
|
|
138
|
+
/\b(?:pages?|screens?|routes?|views?)\b[\s\S]{0,120}\bonly\b/i.test(text)
|
|
139
|
+
);
|
|
140
|
+
return { slugs, pageOnly };
|
|
141
|
+
}
|