rentabots-sdk 1.7.17 ā 1.7.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/dist/index.js +1 -1
- package/init.js +313 -39
- package/init_templates.js +313 -39
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -71,7 +71,7 @@ exports.MessageSchema = zod_1.z.object({
|
|
|
71
71
|
})
|
|
72
72
|
});
|
|
73
73
|
// --- CORE SDK ENGINE ---
|
|
74
|
-
let SDK_VERSION = '1.7.
|
|
74
|
+
let SDK_VERSION = '1.7.26'; // fallback when package.json is unavailable
|
|
75
75
|
try {
|
|
76
76
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
77
77
|
SDK_VERSION = pkg.version;
|
package/init.js
CHANGED
|
@@ -71,6 +71,20 @@ async function main() {
|
|
|
71
71
|
const agentApiKey = process.env.RENTABOTS_API_KEY || process.env.RENTABOTS_SECRET_KEY;
|
|
72
72
|
const apiBase = (process.env.RENTABOTS_API_URL || 'https://rentabots.com/api').replace(/\/$/, '');
|
|
73
73
|
|
|
74
|
+
const notifyOwner = async (message, data = null) => {
|
|
75
|
+
try {
|
|
76
|
+
if (!agentId || !agentApiKey) return;
|
|
77
|
+
await fetch(apiBase + '/agents/' + agentId + '/notify', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
Authorization: 'Bearer ' + agentApiKey
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify({ message, data })
|
|
84
|
+
});
|
|
85
|
+
} catch (_) {}
|
|
86
|
+
};
|
|
87
|
+
|
|
74
88
|
const pushLog = async (level, message) => {
|
|
75
89
|
try {
|
|
76
90
|
if (!agentId || !agentApiKey) return;
|
|
@@ -82,10 +96,26 @@ async function main() {
|
|
|
82
96
|
},
|
|
83
97
|
body: JSON.stringify({ level, message })
|
|
84
98
|
});
|
|
99
|
+
|
|
100
|
+
if (level === 'ERROR' || String(message).includes('INCIDENT')) {
|
|
101
|
+
await notifyOwner(message);
|
|
102
|
+
}
|
|
85
103
|
} catch (_) {}
|
|
86
104
|
};
|
|
87
105
|
|
|
88
|
-
const
|
|
106
|
+
const getRepoFileCount = async (jobId) => {
|
|
107
|
+
try {
|
|
108
|
+
const repoRes = await queen.getRepo(jobId);
|
|
109
|
+
if (!repoRes.success || !repoRes.exists || !repoRes.repo?.id) return 0;
|
|
110
|
+
const res = await fetch(apiBase + '/repos/' + repoRes.repo.id + '/files', {
|
|
111
|
+
headers: { Authorization: 'Bearer ' + agentApiKey }
|
|
112
|
+
});
|
|
113
|
+
const data = await res.json();
|
|
114
|
+
return Array.isArray(data.files) ? data.files.length : 0;
|
|
115
|
+
} catch (_) { return 0; }
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const spawnMissionWorker = async (job, source = 'assignment', opts = {}) => {
|
|
89
119
|
try {
|
|
90
120
|
const jobDataPath = path.join(__dirname, 'workspace', '_job_' + job.id + '.json');
|
|
91
121
|
fs.mkdirSync(path.dirname(jobDataPath), { recursive: true });
|
|
@@ -124,6 +154,25 @@ async function main() {
|
|
|
124
154
|
await pushLog('ERROR', 'Worker process error for mission ' + job.id + ': ' + err.message);
|
|
125
155
|
});
|
|
126
156
|
|
|
157
|
+
// Hard fail-safe: after human clarification, auto-restart if no new repo files appear.
|
|
158
|
+
if (opts.watchAfterClarification) {
|
|
159
|
+
const baseline = typeof opts.baselineFiles === 'number' ? opts.baselineFiles : await getRepoFileCount(job.id);
|
|
160
|
+
const minutes = Number(process.env.RENTABOTS_CLARIFICATION_TIMEOUT_MIN || 3);
|
|
161
|
+
setTimeout(async () => {
|
|
162
|
+
try {
|
|
163
|
+
if (!queen.workers.has(job.id)) return;
|
|
164
|
+
const nowFiles = await getRepoFileCount(job.id);
|
|
165
|
+
if (nowFiles <= baseline) {
|
|
166
|
+
await pushLog('ERROR', 'INCIDENT: No new repo files after clarification window (' + minutes + 'm) for ' + job.id + '. Auto-restarting worker.');
|
|
167
|
+
await queen.sendMessage(job.id, 'š Incident detected: no new deliverables within ' + minutes + ' minutes after clarification. Restarting worker now.');
|
|
168
|
+
try { queen.workers.get(job.id)?.kill(); } catch (_) {}
|
|
169
|
+
queen.workers.delete(job.id);
|
|
170
|
+
await spawnMissionWorker(job, 'clarification-watchdog-restart');
|
|
171
|
+
}
|
|
172
|
+
} catch (_) {}
|
|
173
|
+
}, minutes * 60 * 1000);
|
|
174
|
+
}
|
|
175
|
+
|
|
127
176
|
await queen.sendMessage(job.id, 'š· Worker dispatched by Queen. Streaming execution logs...');
|
|
128
177
|
} catch (err) {
|
|
129
178
|
const msg = err?.message || String(err);
|
|
@@ -152,16 +201,30 @@ async function main() {
|
|
|
152
201
|
|
|
153
202
|
queen.on('message', async (msg) => {
|
|
154
203
|
if (msg.sender.type === 'agent') return;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
204
|
+
|
|
205
|
+
const job = queen.activeMissions.get(msg.jobId);
|
|
206
|
+
if (!job) {
|
|
207
|
+
await queen.sendMessage(msg.jobId, "š¤ Supervisor online. Mission sync in progress.");
|
|
208
|
+
await pushLog('WARN', 'Message received for unknown mission ' + msg.jobId);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Human clarification should immediately re-trigger implementation.
|
|
213
|
+
if (queen.workers.has(msg.jobId)) {
|
|
214
|
+
await pushLog('INFO', 'Human update received for ' + msg.jobId + '; restarting worker for immediate implementation.');
|
|
215
|
+
try { queen.workers.get(msg.jobId)?.kill(); } catch (_) {}
|
|
216
|
+
queen.workers.delete(msg.jobId);
|
|
164
217
|
}
|
|
218
|
+
|
|
219
|
+
const baselineFiles = await getRepoFileCount(msg.jobId);
|
|
220
|
+
try {
|
|
221
|
+
const markerPath = path.join(__dirname, 'workspace', '_clarification_' + msg.jobId + '.json');
|
|
222
|
+
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
|
223
|
+
fs.writeFileSync(markerPath, JSON.stringify({ at: Date.now(), content: msg.content || '' }));
|
|
224
|
+
} catch (_) {}
|
|
225
|
+
|
|
226
|
+
await queen.sendMessage(msg.jobId, "ā
Clarification received. Worker is resuming implementation now.");
|
|
227
|
+
await spawnMissionWorker(job, 'human-followup', { watchAfterClarification: true, baselineFiles });
|
|
165
228
|
});
|
|
166
229
|
|
|
167
230
|
queen.startAutopilot({ scoutingInterval: 60000, minBudget: 5 });
|
|
@@ -176,16 +239,80 @@ const { Agent } = require('rentabots-sdk');
|
|
|
176
239
|
const fs = require('fs');
|
|
177
240
|
const path = require('path');
|
|
178
241
|
|
|
179
|
-
// Read job from temp file (not CLI arg
|
|
242
|
+
// Read job from temp file (not CLI arg - avoids OS length limits)
|
|
180
243
|
const jobDataPath = process.argv[2];
|
|
181
244
|
const job = JSON.parse(fs.readFileSync(jobDataPath, 'utf8'));
|
|
182
245
|
|
|
246
|
+
function inferContract(job) {
|
|
247
|
+
const text = ((job.title || '') + ' ' + (job.description || '')).toLowerCase();
|
|
248
|
+
const adapter = text.includes('api') ? 'api' : text.includes('csv') || text.includes('dataset') || text.includes('etl') ? 'data' : text.includes('landing page') || text.includes('frontend') || text.includes('ui') || text.includes('web') ? 'web' : text.includes('docs') || text.includes('documentation') ? 'docs' : 'script';
|
|
249
|
+
const contract = {
|
|
250
|
+
adapter,
|
|
251
|
+
taskType: adapter,
|
|
252
|
+
language: text.includes('python') ? 'python' : (text.includes('javascript') || text.includes('node') ? 'javascript' : 'unspecified'),
|
|
253
|
+
requires: [],
|
|
254
|
+
deliverables: ['README.md'],
|
|
255
|
+
acceptance: []
|
|
256
|
+
};
|
|
257
|
+
if (text.includes('csv')) contract.requires.push('csv');
|
|
258
|
+
if (text.includes('blank') || text.includes('whitespace') || text.includes('trim')) contract.requires.push('trim');
|
|
259
|
+
if (contract.language === 'python') contract.deliverables.push('solution.py');
|
|
260
|
+
if (adapter === 'api') contract.deliverables.push('openapi-or-endpoint-docs.md');
|
|
261
|
+
if (adapter === 'web') contract.deliverables.push('index.html');
|
|
262
|
+
if (adapter === 'data') contract.deliverables.push('data_dictionary.md');
|
|
263
|
+
if (adapter === 'docs') contract.deliverables.push('DOCUMENTATION.md');
|
|
264
|
+
contract.acceptance.push('output reflects mission title and description');
|
|
265
|
+
contract.acceptance.push('at least one non-template implementation file');
|
|
266
|
+
return contract;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function generateAdapterFallbackFiles(workDir, job, contract) {
|
|
270
|
+
const brief = (job.description || '').trim();
|
|
271
|
+
const readme = [
|
|
272
|
+
'# Mission Recovery Draft',
|
|
273
|
+
'',
|
|
274
|
+
'This draft was auto-generated because OpenClaw execution failed at runtime.',
|
|
275
|
+
'',
|
|
276
|
+
'## Mission',
|
|
277
|
+
'- Title: ' + (job.title || ''),
|
|
278
|
+
'- Adapter: ' + contract.adapter,
|
|
279
|
+
'- Language: ' + contract.language,
|
|
280
|
+
'',
|
|
281
|
+
'## Brief',
|
|
282
|
+
brief || '(none provided)',
|
|
283
|
+
'',
|
|
284
|
+
'## Next step',
|
|
285
|
+
'Review this draft and send clarification in mission chat for refinement.'
|
|
286
|
+
].join('\n');
|
|
287
|
+
fs.writeFileSync(path.join(workDir, 'README.md'), readme);
|
|
288
|
+
|
|
289
|
+
if (contract.adapter === 'api') {
|
|
290
|
+
fs.writeFileSync(path.join(workDir, 'openapi-or-endpoint-docs.md'), '# API Draft\n\n- GET /health\n- POST /process\n\nDescribe request/response schema and validations.');
|
|
291
|
+
} else if (contract.adapter === 'web') {
|
|
292
|
+
fs.writeFileSync(path.join(workDir, 'index.html'), '<!doctype html><html><body><h1>Recovery Draft</h1><p>Mission: ' + (job.title || '') + '</p></body></html>');
|
|
293
|
+
} else if (contract.adapter === 'data') {
|
|
294
|
+
fs.writeFileSync(path.join(workDir, 'data_dictionary.md'), '# Data Dictionary Draft\n\n| Field | Type | Notes |\n|---|---|---|\n| id | string | unique id |');
|
|
295
|
+
} else if (contract.adapter === 'docs') {
|
|
296
|
+
fs.writeFileSync(path.join(workDir, 'DOCUMENTATION.md'), '# Documentation Draft\n\n## Overview\n\n## Setup\n\n## Usage\n');
|
|
297
|
+
} else if (contract.language === 'python') {
|
|
298
|
+
fs.writeFileSync(path.join(workDir, 'solution.py'), 'def main():\n print("recovery draft")\n\nif __name__ == "__main__":\n main()\n');
|
|
299
|
+
} else {
|
|
300
|
+
fs.writeFileSync(path.join(workDir, 'solution.js'), 'console.log("recovery draft");\n');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
183
304
|
async function main() {
|
|
184
305
|
const agent = new Agent({ persistState: false, debug: true });
|
|
185
306
|
await agent.connect();
|
|
186
307
|
await agent.sendMessage(job.id, "š· Worker Unit [PID " + process.pid + "] deployed. Analyzing requirements...");
|
|
187
308
|
|
|
188
|
-
const
|
|
309
|
+
const contract = inferContract(job);
|
|
310
|
+
const workDir = path.join(agent.workspaceRoot, job.id);
|
|
311
|
+
fs.mkdirSync(workDir, { recursive: true });
|
|
312
|
+
fs.writeFileSync(path.join(workDir, 'ACCEPTANCE.json'), JSON.stringify(contract, null, 2));
|
|
313
|
+
await agent.sendMessage(job.id, 'š Contract parsed: ' + contract.taskType + ' / ' + contract.language + '. Deliverables: ' + contract.deliverables.join(', '));
|
|
314
|
+
|
|
315
|
+
const task = 'PROJECT: ' + job.title + '. BRIEF: ' + job.description + '. CONTRACT: ' + JSON.stringify(contract) + '. INSTRUCTION: Execute this task in the current directory. Do not leave the workspace. You MUST produce tangible deliverables (code + README + run/test notes) in local files.';
|
|
189
316
|
|
|
190
317
|
// Use shell command string (not array args)
|
|
191
318
|
const taskArg = JSON.stringify(task);
|
|
@@ -260,7 +387,7 @@ async function main() {
|
|
|
260
387
|
|
|
261
388
|
if (overallSuccess) {
|
|
262
389
|
// Verify actual deliverables exist before claiming success
|
|
263
|
-
|
|
390
|
+
// reuse workDir initialized earlier
|
|
264
391
|
const files = [];
|
|
265
392
|
const walk = (dir, rel = '') => {
|
|
266
393
|
if (!fs.existsSync(dir)) return;
|
|
@@ -274,21 +401,11 @@ async function main() {
|
|
|
274
401
|
};
|
|
275
402
|
walk(workDir);
|
|
276
403
|
|
|
277
|
-
|
|
404
|
+
const fallbackGenerated = false;
|
|
278
405
|
if (files.length === 0) {
|
|
279
406
|
await agent.setProgress(job.id, 40);
|
|
280
|
-
await agent.sendMessage(job.id, "ā ļø No
|
|
281
|
-
|
|
282
|
-
const readme = '# Mission Output\n\nTitle: ' + job.title + '\n\nDescription: ' + job.description + '\n\n## Status\n- Auto-generated fallback deliverable because runtime produced no files.\n- Worker is continuing execution and awaiting optional clarifications.';
|
|
283
|
-
const questions = '# Clarifications Needed\n\n1. Exact output file name(s)?\n2. Provide sample input data?\n3. Any strict acceptance criteria/tests?';
|
|
284
|
-
const script = "# TODO: Implement mission logic\n# Mission: " + job.title + "\n\nprint('Placeholder fallback script generated. Update with final implementation.')\n";
|
|
285
|
-
|
|
286
|
-
fs.writeFileSync(path.join(workDir, 'README.md'), readme);
|
|
287
|
-
fs.writeFileSync(path.join(workDir, 'CLARIFICATIONS.md'), questions);
|
|
288
|
-
fs.writeFileSync(path.join(workDir, 'solution.py'), script);
|
|
289
|
-
|
|
290
|
-
files.push('README.md', 'CLARIFICATIONS.md', 'solution.py');
|
|
291
|
-
fallbackGenerated = true;
|
|
407
|
+
await agent.sendMessage(job.id, "ā ļø No usable code files were generated from the mission brief. I need 3 clarifications to continue: 1) expected output file names, 2) sample input CSV row(s), 3) exact clean-up rules (trim spaces only or additional normalization).");
|
|
408
|
+
return;
|
|
292
409
|
}
|
|
293
410
|
|
|
294
411
|
const repoRes = await agent.getRepo(job.id);
|
|
@@ -314,13 +431,7 @@ async function main() {
|
|
|
314
431
|
return;
|
|
315
432
|
}
|
|
316
433
|
|
|
317
|
-
//
|
|
318
|
-
const missionText = (job.title + ' ' + job.description).toLowerCase();
|
|
319
|
-
const required = [];
|
|
320
|
-
if (missionText.includes('python')) required.push('python');
|
|
321
|
-
if (missionText.includes('csv')) required.push('csv');
|
|
322
|
-
if (missionText.includes('blank') || missionText.includes('whitespace')) required.push('strip');
|
|
323
|
-
|
|
434
|
+
// Dynamic contract QA: deterministic checks based on inferred contract
|
|
324
435
|
let combinedText = '';
|
|
325
436
|
for (const rel of files) {
|
|
326
437
|
const ext = path.extname(rel).toLowerCase();
|
|
@@ -328,10 +439,65 @@ async function main() {
|
|
|
328
439
|
try { combinedText += '\n' + fs.readFileSync(path.join(workDir, rel), 'utf8').toLowerCase(); } catch (_) {}
|
|
329
440
|
}
|
|
330
441
|
}
|
|
331
|
-
|
|
332
|
-
|
|
442
|
+
|
|
443
|
+
const issues = [];
|
|
444
|
+
const hasFile = (predicate) => files.some(predicate);
|
|
445
|
+
|
|
446
|
+
if (contract.language === 'python' && !hasFile((f) => f.toLowerCase().endsWith('.py'))) {
|
|
447
|
+
issues.push('expected at least one .py implementation file');
|
|
448
|
+
}
|
|
449
|
+
if (contract.language === 'javascript' && !hasFile((f) => ['.js','.ts','.tsx','.jsx'].some(e => f.toLowerCase().endsWith(e)))) {
|
|
450
|
+
issues.push('expected at least one JS/TS implementation file');
|
|
451
|
+
}
|
|
452
|
+
if (contract.requires.includes('csv') && !(combinedText.includes('csv') || hasFile((f) => f.toLowerCase().endsWith('.csv')))) {
|
|
453
|
+
issues.push('csv requirement not reflected in output');
|
|
454
|
+
}
|
|
455
|
+
if (contract.requires.includes('trim') && !(combinedText.includes('strip') || combinedText.includes('trim'))) {
|
|
456
|
+
issues.push('whitespace cleanup requirement not reflected in output');
|
|
457
|
+
}
|
|
458
|
+
for (const d of contract.deliverables || []) {
|
|
459
|
+
if (!hasFile((f) => f.toLowerCase() === String(d).toLowerCase())) {
|
|
460
|
+
issues.push('missing contract deliverable: ' + d);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Adapter-specific deterministic validators
|
|
465
|
+
if (contract.adapter === 'api') {
|
|
466
|
+
const hasApiSource = hasFile((f) => ['.js','.ts','.py','.go','.java','.rb','.php'].some(e => f.toLowerCase().endsWith(e)));
|
|
467
|
+
const hasMethod = /(get|post|put|delete|patch)\s*\//i.test(combinedText) || /(app\.|router\.|fastapi|flask|express|endpoint|route)/i.test(combinedText);
|
|
468
|
+
const hasSchema = /(request|response|schema|json|payload|status\s*code)/i.test(combinedText);
|
|
469
|
+
if (!hasApiSource) issues.push('api adapter: missing implementation source file');
|
|
470
|
+
if (!hasMethod) issues.push('api adapter: missing endpoint/method definitions');
|
|
471
|
+
if (!hasSchema) issues.push('api adapter: missing request/response contract details');
|
|
472
|
+
} else if (contract.adapter === 'web') {
|
|
473
|
+
const hasHtml = hasFile((f) => f.toLowerCase().endsWith('.html') || f.toLowerCase().endsWith('.tsx') || f.toLowerCase().endsWith('.jsx'));
|
|
474
|
+
const hasUiSignals = /(button|form|input|nav|header|section|main|aria-|onclick|onsubmit)/i.test(combinedText);
|
|
475
|
+
const hasStyleSignals = /(css|class=|tailwind|style=|flex|grid|responsive)/i.test(combinedText);
|
|
476
|
+
if (!hasHtml) issues.push('web adapter: missing page/component file');
|
|
477
|
+
if (!hasUiSignals) issues.push('web adapter: missing interactive/ui structure signals');
|
|
478
|
+
if (!hasStyleSignals) issues.push('web adapter: missing styling/responsive signals');
|
|
479
|
+
} else if (contract.adapter === 'data') {
|
|
480
|
+
const hasDataScript = hasFile((f) => ['.py','.ipynb','.sql','.js','.ts'].some(e => f.toLowerCase().endsWith(e)));
|
|
481
|
+
const hasTransform = /(transform|clean|normalize|dedup|aggregate|groupby|pandas|sql)/i.test(combinedText);
|
|
482
|
+
const hasOutputSpec = /(output|export|write|save|table|csv)/i.test(combinedText);
|
|
483
|
+
if (!hasDataScript) issues.push('data adapter: missing data-processing script/query');
|
|
484
|
+
if (!hasTransform) issues.push('data adapter: missing deterministic transform logic');
|
|
485
|
+
if (!hasOutputSpec) issues.push('data adapter: missing output definition');
|
|
486
|
+
} else if (contract.adapter === 'docs') {
|
|
487
|
+
const hasDocFile = hasFile((f) => f.toLowerCase().endsWith('.md'));
|
|
488
|
+
const hasSections = /(##\s+overview|##\s+setup|##\s+usage|table of contents|getting started)/i.test(combinedText);
|
|
489
|
+
const hasExamples = /(example|sample|command|curl|code block|```)/i.test(combinedText);
|
|
490
|
+
if (!hasDocFile) issues.push('docs adapter: missing markdown documentation');
|
|
491
|
+
if (!hasSections) issues.push('docs adapter: missing core documentation sections');
|
|
492
|
+
if (!hasExamples) issues.push('docs adapter: missing usage examples');
|
|
493
|
+
} else {
|
|
494
|
+
const hasRunnable = /(main\(|if __name__ ==|module\.exports|export default|function\s+\w+)/i.test(combinedText);
|
|
495
|
+
if (!hasRunnable) issues.push('script adapter: missing runnable implementation entry');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (issues.length > 0) {
|
|
333
499
|
await agent.setProgress(job.id, 70);
|
|
334
|
-
await agent.sendMessage(job.id, 'ā ļø QA
|
|
500
|
+
await agent.sendMessage(job.id, 'ā ļø Contract QA failed: ' + issues.slice(0, 4).join('; ') + '. Revising now.');
|
|
335
501
|
return;
|
|
336
502
|
}
|
|
337
503
|
|
|
@@ -341,9 +507,105 @@ async function main() {
|
|
|
341
507
|
return;
|
|
342
508
|
}
|
|
343
509
|
|
|
510
|
+
// Hard gate: after clarification, require at least one non-template file changed
|
|
511
|
+
try {
|
|
512
|
+
const markerPath = path.join(workDir, '..', '_clarification_' + job.id + '.json');
|
|
513
|
+
if (fs.existsSync(markerPath)) {
|
|
514
|
+
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
|
|
515
|
+
const at = Number(marker?.at || 0);
|
|
516
|
+
const templateSet = new Set(['README.md', 'CLARIFICATIONS.md', 'solution.py', 'FAILSAFE_REPORT.md']);
|
|
517
|
+
const changed = files.some((rel) => {
|
|
518
|
+
if (templateSet.has(rel)) return false;
|
|
519
|
+
const full = path.join(workDir, rel);
|
|
520
|
+
try { return fs.statSync(full).mtimeMs >= at; } catch (_) { return false; }
|
|
521
|
+
});
|
|
522
|
+
if (!changed) {
|
|
523
|
+
await agent.setProgress(job.id, 88);
|
|
524
|
+
await agent.sendMessage(job.id, 'ā ļø Final gate blocked: no non-template file changed after your latest clarification. Continuing implementation.');
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
} catch (_) {}
|
|
529
|
+
|
|
530
|
+
// Second QA worker gate (OpenClaw brain review) before final completion
|
|
531
|
+
await agent.sendMessage(job.id, 'š Running second QA gate before final completion...');
|
|
532
|
+
const qaPrompt = [
|
|
533
|
+
'You are QA worker. Validate whether deliverables satisfy mission requirements.',
|
|
534
|
+
'Return ONLY one token: QA_PASS or QA_FAIL, then one short reason line.',
|
|
535
|
+
'',
|
|
536
|
+
'MISSION TITLE: ' + job.title,
|
|
537
|
+
'MISSION DESCRIPTION: ' + job.description,
|
|
538
|
+
'FILES: ' + files.join(', '),
|
|
539
|
+
'',
|
|
540
|
+
'CONTENT SAMPLE:',
|
|
541
|
+
combinedText.slice(0, 6000),
|
|
542
|
+
].join('\n');
|
|
543
|
+
|
|
544
|
+
const qaArg = JSON.stringify(qaPrompt);
|
|
545
|
+
const qaCmds = [
|
|
546
|
+
'openclaw sessions spawn --task ' + qaArg,
|
|
547
|
+
'openclaw sessions_spawn --task ' + qaArg,
|
|
548
|
+
];
|
|
549
|
+
let qaPassed = false;
|
|
550
|
+
let qaOutput = '';
|
|
551
|
+
for (const qcmd of qaCmds) {
|
|
552
|
+
const q = await agent.execute(job.id, qcmd, { timeout: 600000, shell: true });
|
|
553
|
+
qaOutput = q.output || '';
|
|
554
|
+
if (q.exitCode === 0 && qaOutput.toUpperCase().includes('QA_PASS')) { qaPassed = true; break; }
|
|
555
|
+
if (!qaOutput.toLowerCase().includes('unknown command')) break;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (!qaPassed) {
|
|
559
|
+
const why = (qaOutput || 'No QA output').slice(-300);
|
|
560
|
+
const qaStatePath = path.join(workDir, '.qa_fail_count');
|
|
561
|
+
let qaFails = 0;
|
|
562
|
+
try { qaFails = Number(fs.readFileSync(qaStatePath, 'utf8') || '0'); } catch (_) {}
|
|
563
|
+
qaFails += 1;
|
|
564
|
+
fs.writeFileSync(qaStatePath, String(qaFails));
|
|
565
|
+
|
|
566
|
+
await agent.setProgress(job.id, 80);
|
|
567
|
+
if (qaFails >= 5) {
|
|
568
|
+
await agent.sendMessage(job.id, 'šØ Escalation: repeated QA failures (' + qaFails + '). Pausing finalization. Please provide concrete acceptance criteria/examples so I can resolve this precisely.');
|
|
569
|
+
} else if (qaFails >= 3) {
|
|
570
|
+
await agent.sendMessage(job.id, 'ā ļø Second QA gate failed (attempt ' + qaFails + '). I am revising strategy. Reason: ' + why);
|
|
571
|
+
} else {
|
|
572
|
+
await agent.sendMessage(job.id, 'ā ļø Second QA gate failed. I will revise before marking done. Reason: ' + why);
|
|
573
|
+
}
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
try { fs.unlinkSync(path.join(workDir, '.qa_fail_count')); } catch (_) {}
|
|
578
|
+
|
|
579
|
+
const crypto = require('crypto');
|
|
580
|
+
const fingerprint = crypto.createHash('sha256').update(combinedText).digest('hex');
|
|
581
|
+
const fpPath = path.join(workDir, '.last_delivery_hash');
|
|
582
|
+
if (fs.existsSync(fpPath)) {
|
|
583
|
+
const prev = fs.readFileSync(fpPath, 'utf8').trim();
|
|
584
|
+
if (prev === fingerprint) {
|
|
585
|
+
await agent.setProgress(job.id, 90);
|
|
586
|
+
await agent.sendMessage(job.id, 'ā ļø Anti-repeat gate blocked completion: output matches previous failed attempt. Revising strategy now.');
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
fs.writeFileSync(fpPath, fingerprint);
|
|
591
|
+
|
|
592
|
+
const summary = [
|
|
593
|
+
'# DELIVERY_SUMMARY',
|
|
594
|
+
'',
|
|
595
|
+
'Mission: ' + job.id,
|
|
596
|
+
'Task Type: ' + contract.taskType,
|
|
597
|
+
'Language: ' + contract.language,
|
|
598
|
+
'Files Uploaded: ' + uploaded,
|
|
599
|
+
'Acceptance Checks:',
|
|
600
|
+
'- Relevance QA: PASS',
|
|
601
|
+
'- Second QA Gate: PASS',
|
|
602
|
+
'- Non-template change after clarification: PASS',
|
|
603
|
+
].join('\n');
|
|
604
|
+
try { await agent.uploadRepoFile(job.id, 'DELIVERY_SUMMARY.md', summary, false); } catch (_) {}
|
|
605
|
+
|
|
344
606
|
await agent.setProgress(job.id, 100);
|
|
345
607
|
if (repo?.id) {
|
|
346
|
-
await agent.sendMessage(job.id, "ā
Execution complete. Deliverables uploaded: " + uploaded + " files. Repo: https://rentabots.com/repos/" + repo.id);
|
|
608
|
+
await agent.sendMessage(job.id, "ā
Execution complete. Deliverables uploaded: " + uploaded + " files. Repo: https://rentabots.com/repos/" + repo.id + " (see DELIVERY_SUMMARY.md)");
|
|
347
609
|
} else {
|
|
348
610
|
await agent.sendMessage(job.id, "ā
Execution complete. Deliverables uploaded: " + uploaded + " files to mission repository.");
|
|
349
611
|
}
|
|
@@ -352,16 +614,28 @@ async function main() {
|
|
|
352
614
|
const snippet = lastOutput.slice(-500) || 'No output captured';
|
|
353
615
|
console.error("Worker Error:", snippet);
|
|
354
616
|
|
|
355
|
-
// Fail-safe: keep user informed + attach diagnostic report
|
|
617
|
+
// Fail-safe: keep user informed + attach diagnostic report + fallback draft
|
|
356
618
|
await agent.setProgress(job.id, 25);
|
|
357
|
-
await agent.sendMessage(job.id, "ā ļø
|
|
619
|
+
await agent.sendMessage(job.id, "ā ļø OpenClaw run failed. Switching to backup adapter mode now so you don't wait idle.");
|
|
358
620
|
|
|
359
621
|
try {
|
|
622
|
+
generateAdapterFallbackFiles(workDir, job, contract);
|
|
623
|
+
|
|
360
624
|
const repoRes = await agent.getRepo(job.id);
|
|
361
625
|
if (!repoRes.success || !repoRes.exists) {
|
|
362
626
|
await agent.createRepo(job.id, 'mission-recovery-' + job.id.slice(0, 8));
|
|
363
627
|
}
|
|
364
628
|
|
|
629
|
+
// Upload fallback draft artifacts first
|
|
630
|
+
const fallbackFiles = fs.readdirSync(workDir).filter((f) => fs.statSync(path.join(workDir, f)).isFile());
|
|
631
|
+
for (const f of fallbackFiles) {
|
|
632
|
+
try {
|
|
633
|
+
const full = path.join(workDir, f);
|
|
634
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
635
|
+
await agent.uploadRepoFile(job.id, f, content, false);
|
|
636
|
+
} catch (_) {}
|
|
637
|
+
}
|
|
638
|
+
|
|
365
639
|
const report = [
|
|
366
640
|
'# FAILSAFE_REPORT',
|
|
367
641
|
'',
|
package/init_templates.js
CHANGED
|
@@ -40,6 +40,20 @@ async function main() {
|
|
|
40
40
|
const agentApiKey = process.env.RENTABOTS_API_KEY || process.env.RENTABOTS_SECRET_KEY;
|
|
41
41
|
const apiBase = (process.env.RENTABOTS_API_URL || 'https://rentabots.com/api').replace(/\/$/, '');
|
|
42
42
|
|
|
43
|
+
const notifyOwner = async (message, data = null) => {
|
|
44
|
+
try {
|
|
45
|
+
if (!agentId || !agentApiKey) return;
|
|
46
|
+
await fetch(\`${apiBase}/agents/\${agentId}/notify\`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
Authorization: \`Bearer \${agentApiKey}\`
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify({ message, data })
|
|
53
|
+
});
|
|
54
|
+
} catch (_) {}
|
|
55
|
+
};
|
|
56
|
+
|
|
43
57
|
const pushLog = async (level, message) => {
|
|
44
58
|
try {
|
|
45
59
|
if (!agentId || !agentApiKey) return;
|
|
@@ -51,10 +65,26 @@ async function main() {
|
|
|
51
65
|
},
|
|
52
66
|
body: JSON.stringify({ level, message })
|
|
53
67
|
});
|
|
68
|
+
|
|
69
|
+
if (level === 'ERROR' || String(message).includes('INCIDENT')) {
|
|
70
|
+
await notifyOwner(message);
|
|
71
|
+
}
|
|
54
72
|
} catch (_) {}
|
|
55
73
|
};
|
|
56
74
|
|
|
57
|
-
const
|
|
75
|
+
const getRepoFileCount = async (jobId) => {
|
|
76
|
+
try {
|
|
77
|
+
const repoRes = await queen.getRepo(jobId);
|
|
78
|
+
if (!repoRes.success || !repoRes.exists || !repoRes.repo?.id) return 0;
|
|
79
|
+
const res = await fetch(apiBase + '/repos/' + repoRes.repo.id + '/files', {
|
|
80
|
+
headers: { Authorization: 'Bearer ' + agentApiKey }
|
|
81
|
+
});
|
|
82
|
+
const data = await res.json();
|
|
83
|
+
return Array.isArray(data.files) ? data.files.length : 0;
|
|
84
|
+
} catch (_) { return 0; }
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const spawnMissionWorker = async (job, source = 'assignment', opts = {}) => {
|
|
58
88
|
try {
|
|
59
89
|
const jobDataPath = path.join(__dirname, 'workspace', '_job_' + job.id + '.json');
|
|
60
90
|
fs.mkdirSync(path.dirname(jobDataPath), { recursive: true });
|
|
@@ -93,6 +123,25 @@ async function main() {
|
|
|
93
123
|
await pushLog('ERROR', 'Worker process error for mission ' + job.id + ': ' + err.message);
|
|
94
124
|
});
|
|
95
125
|
|
|
126
|
+
// Hard fail-safe: after human clarification, auto-restart if no new repo files appear.
|
|
127
|
+
if (opts.watchAfterClarification) {
|
|
128
|
+
const baseline = typeof opts.baselineFiles === 'number' ? opts.baselineFiles : await getRepoFileCount(job.id);
|
|
129
|
+
const minutes = Number(process.env.RENTABOTS_CLARIFICATION_TIMEOUT_MIN || 3);
|
|
130
|
+
setTimeout(async () => {
|
|
131
|
+
try {
|
|
132
|
+
if (!queen.workers.has(job.id)) return;
|
|
133
|
+
const nowFiles = await getRepoFileCount(job.id);
|
|
134
|
+
if (nowFiles <= baseline) {
|
|
135
|
+
await pushLog('ERROR', 'INCIDENT: No new repo files after clarification window (' + minutes + 'm) for ' + job.id + '. Auto-restarting worker.');
|
|
136
|
+
await queen.sendMessage(job.id, 'š Incident detected: no new deliverables within ' + minutes + ' minutes after clarification. Restarting worker now.');
|
|
137
|
+
try { queen.workers.get(job.id)?.kill(); } catch (_) {}
|
|
138
|
+
queen.workers.delete(job.id);
|
|
139
|
+
await spawnMissionWorker(job, 'clarification-watchdog-restart');
|
|
140
|
+
}
|
|
141
|
+
} catch (_) {}
|
|
142
|
+
}, minutes * 60 * 1000);
|
|
143
|
+
}
|
|
144
|
+
|
|
96
145
|
await queen.sendMessage(job.id, 'š· Worker dispatched by Queen. Streaming execution logs...');
|
|
97
146
|
} catch (err) {
|
|
98
147
|
const msg = err?.message || String(err);
|
|
@@ -121,17 +170,30 @@ async function main() {
|
|
|
121
170
|
|
|
122
171
|
queen.on('message', async (msg) => {
|
|
123
172
|
if (msg.sender.type === 'agent') return;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
await queen.sendMessage(msg.jobId, "š¤ Supervisor here. Dispatching a worker to this channel shortly.");
|
|
133
|
-
await spawnMissionWorker(job, 'message-trigger');
|
|
173
|
+
|
|
174
|
+
const job = queen.activeMissions.get(msg.jobId);
|
|
175
|
+
if (!job) {
|
|
176
|
+
await queen.sendMessage(msg.jobId, "š¤ Supervisor online. Mission sync in progress.");
|
|
177
|
+
await pushLog('WARN', 'Message received for unknown mission ' + msg.jobId);
|
|
178
|
+
return;
|
|
134
179
|
}
|
|
180
|
+
|
|
181
|
+
// Human clarification should immediately re-trigger implementation.
|
|
182
|
+
if (queen.workers.has(msg.jobId)) {
|
|
183
|
+
await pushLog('INFO', 'Human update received for ' + msg.jobId + '; restarting worker for immediate implementation.');
|
|
184
|
+
try { queen.workers.get(msg.jobId)?.kill(); } catch (_) {}
|
|
185
|
+
queen.workers.delete(msg.jobId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const baselineFiles = await getRepoFileCount(msg.jobId);
|
|
189
|
+
try {
|
|
190
|
+
const markerPath = path.join(__dirname, 'workspace', '_clarification_' + msg.jobId + '.json');
|
|
191
|
+
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
|
192
|
+
fs.writeFileSync(markerPath, JSON.stringify({ at: Date.now(), content: msg.content || '' }));
|
|
193
|
+
} catch (_) {}
|
|
194
|
+
|
|
195
|
+
await queen.sendMessage(msg.jobId, "ā
Clarification received. Worker is resuming implementation now.");
|
|
196
|
+
await spawnMissionWorker(job, 'human-followup', { watchAfterClarification: true, baselineFiles });
|
|
135
197
|
});
|
|
136
198
|
|
|
137
199
|
queen.startAutopilot({
|
|
@@ -157,6 +219,65 @@ const path = require('path');
|
|
|
157
219
|
const jobDataPath = process.argv[2];
|
|
158
220
|
const job = JSON.parse(fs.readFileSync(jobDataPath, 'utf8'));
|
|
159
221
|
|
|
222
|
+
function inferContract(job) {
|
|
223
|
+
const text = ((job.title || '') + ' ' + (job.description || '')).toLowerCase();
|
|
224
|
+
const adapter = text.includes('api') ? 'api' : text.includes('csv') || text.includes('dataset') || text.includes('etl') ? 'data' : text.includes('landing page') || text.includes('frontend') || text.includes('ui') || text.includes('web') ? 'web' : text.includes('docs') || text.includes('documentation') ? 'docs' : 'script';
|
|
225
|
+
const contract = {
|
|
226
|
+
adapter,
|
|
227
|
+
taskType: adapter,
|
|
228
|
+
language: text.includes('python') ? 'python' : text.includes('javascript') || text.includes('node') ? 'javascript' : 'unspecified',
|
|
229
|
+
requires: [],
|
|
230
|
+
deliverables: ['README.md'],
|
|
231
|
+
acceptance: []
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (text.includes('csv')) contract.requires.push('csv');
|
|
235
|
+
if (text.includes('blank') || text.includes('whitespace') || text.includes('trim')) contract.requires.push('trim');
|
|
236
|
+
if (contract.language === 'python') contract.deliverables.push('solution.py');
|
|
237
|
+
if (adapter === 'api') contract.deliverables.push('openapi-or-endpoint-docs.md');
|
|
238
|
+
if (adapter === 'web') contract.deliverables.push('index.html');
|
|
239
|
+
if (adapter === 'data') contract.deliverables.push('data_dictionary.md');
|
|
240
|
+
if (adapter === 'docs') contract.deliverables.push('DOCUMENTATION.md');
|
|
241
|
+
contract.acceptance.push('output reflects mission title and description');
|
|
242
|
+
contract.acceptance.push('at least one non-template implementation file');
|
|
243
|
+
return contract;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function generateAdapterFallbackFiles(workDir, job, contract) {
|
|
247
|
+
const brief = (job.description || '').trim();
|
|
248
|
+
const readme = [
|
|
249
|
+
'# Mission Recovery Draft',
|
|
250
|
+
'',
|
|
251
|
+
'This draft was auto-generated because OpenClaw execution failed at runtime.',
|
|
252
|
+
'',
|
|
253
|
+
'## Mission',
|
|
254
|
+
'- Title: ' + (job.title || ''),
|
|
255
|
+
'- Adapter: ' + contract.adapter,
|
|
256
|
+
'- Language: ' + contract.language,
|
|
257
|
+
'',
|
|
258
|
+
'## Brief',
|
|
259
|
+
brief || '(none provided)',
|
|
260
|
+
'',
|
|
261
|
+
'## Next step',
|
|
262
|
+
'Review this draft and send clarification in mission chat for refinement.'
|
|
263
|
+
].join('\n');
|
|
264
|
+
fs.writeFileSync(path.join(workDir, 'README.md'), readme);
|
|
265
|
+
|
|
266
|
+
if (contract.adapter === 'api') {
|
|
267
|
+
fs.writeFileSync(path.join(workDir, 'openapi-or-endpoint-docs.md'), '# API Draft\n\n- GET /health\n- POST /process\n\nDescribe request/response schema and validations.');
|
|
268
|
+
} else if (contract.adapter === 'web') {
|
|
269
|
+
fs.writeFileSync(path.join(workDir, 'index.html'), '<!doctype html><html><body><h1>Recovery Draft</h1><p>Mission: ' + (job.title || '') + '</p></body></html>');
|
|
270
|
+
} else if (contract.adapter === 'data') {
|
|
271
|
+
fs.writeFileSync(path.join(workDir, 'data_dictionary.md'), '# Data Dictionary Draft\n\n| Field | Type | Notes |\n|---|---|---|\n| id | string | unique id |');
|
|
272
|
+
} else if (contract.adapter === 'docs') {
|
|
273
|
+
fs.writeFileSync(path.join(workDir, 'DOCUMENTATION.md'), '# Documentation Draft\n\n## Overview\n\n## Setup\n\n## Usage\n');
|
|
274
|
+
} else if (contract.language === 'python') {
|
|
275
|
+
fs.writeFileSync(path.join(workDir, 'solution.py'), 'def main():\n print("recovery draft")\n\nif __name__ == "__main__":\n main()\n');
|
|
276
|
+
} else {
|
|
277
|
+
fs.writeFileSync(path.join(workDir, 'solution.js'), 'console.log("recovery draft");\n');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
160
281
|
async function main() {
|
|
161
282
|
const agent = new Agent({
|
|
162
283
|
persistState: false,
|
|
@@ -168,8 +289,14 @@ async function main() {
|
|
|
168
289
|
// Announce arrival in the mission channel
|
|
169
290
|
await agent.sendMessage(job.id, "š· Worker Unit [PID " + process.pid + "] deployed to workspace. Analyzing requirements...");
|
|
170
291
|
|
|
292
|
+
const contract = inferContract(job);
|
|
293
|
+
const workDir = path.join(agent.workspaceRoot, job.id);
|
|
294
|
+
fs.mkdirSync(workDir, { recursive: true });
|
|
295
|
+
fs.writeFileSync(path.join(workDir, 'ACCEPTANCE.json'), JSON.stringify(contract, null, 2));
|
|
296
|
+
await agent.sendMessage(job.id, 'š Contract parsed: ' + contract.taskType + ' / ' + contract.language + '. Deliverables: ' + contract.deliverables.join(', '));
|
|
297
|
+
|
|
171
298
|
// Construct the prompt for the autonomous brain
|
|
172
|
-
const task = \`PROJECT: \${job.title}. BRIEF: \${job.description}. INSTRUCTION: Execute this task in the current directory. Do not leave the workspace. You MUST produce tangible deliverables (code + README + run/test notes) in local files.\`;
|
|
299
|
+
const task = \`PROJECT: \${job.title}. BRIEF: \${job.description}. CONTRACT: \${JSON.stringify(contract)}. INSTRUCTION: Execute this task in the current directory. Do not leave the workspace. You MUST produce tangible deliverables (code + README + run/test notes) in local files.\`;
|
|
173
300
|
|
|
174
301
|
// --- š¦ EXECUTE VIA OPENCLAW BRAIN ---
|
|
175
302
|
const taskArg = JSON.stringify(task);
|
|
@@ -245,7 +372,7 @@ async function main() {
|
|
|
245
372
|
|
|
246
373
|
if (overallSuccess) {
|
|
247
374
|
// Verify actual deliverables exist before claiming success
|
|
248
|
-
|
|
375
|
+
// reuse workDir initialized earlier
|
|
249
376
|
const files = [];
|
|
250
377
|
const walk = (dir, rel = '') => {
|
|
251
378
|
if (!fs.existsSync(dir)) return;
|
|
@@ -259,21 +386,11 @@ async function main() {
|
|
|
259
386
|
};
|
|
260
387
|
walk(workDir);
|
|
261
388
|
|
|
262
|
-
|
|
389
|
+
const fallbackGenerated = false;
|
|
263
390
|
if (files.length === 0) {
|
|
264
391
|
await agent.setProgress(job.id, 40);
|
|
265
|
-
await agent.sendMessage(job.id, "ā ļø No
|
|
266
|
-
|
|
267
|
-
const readme = '# Mission Output\n\nTitle: ' + job.title + '\n\nDescription: ' + job.description + '\n\n## Status\n- Auto-generated fallback deliverable because runtime produced no files.\n- Worker is continuing execution and awaiting optional clarifications.';
|
|
268
|
-
const questions = '# Clarifications Needed\n\n1. Exact output file name(s)?\n2. Provide sample input data?\n3. Any strict acceptance criteria/tests?';
|
|
269
|
-
const script = "# TODO: Implement mission logic\n# Mission: " + job.title + "\n\nprint('Placeholder fallback script generated. Update with final implementation.')\n";
|
|
270
|
-
|
|
271
|
-
fs.writeFileSync(path.join(workDir, 'README.md'), readme);
|
|
272
|
-
fs.writeFileSync(path.join(workDir, 'CLARIFICATIONS.md'), questions);
|
|
273
|
-
fs.writeFileSync(path.join(workDir, 'solution.py'), script);
|
|
274
|
-
|
|
275
|
-
files.push('README.md', 'CLARIFICATIONS.md', 'solution.py');
|
|
276
|
-
fallbackGenerated = true;
|
|
392
|
+
await agent.sendMessage(job.id, "ā ļø No usable code files were generated from the mission brief. I need 3 clarifications to continue: 1) expected output file names, 2) sample input CSV row(s), 3) exact clean-up rules (trim spaces only or additional normalization).");
|
|
393
|
+
return;
|
|
277
394
|
}
|
|
278
395
|
|
|
279
396
|
const repoRes = await agent.getRepo(job.id);
|
|
@@ -299,13 +416,7 @@ async function main() {
|
|
|
299
416
|
return;
|
|
300
417
|
}
|
|
301
418
|
|
|
302
|
-
//
|
|
303
|
-
const missionText = (job.title + ' ' + job.description).toLowerCase();
|
|
304
|
-
const required = [];
|
|
305
|
-
if (missionText.includes('python')) required.push('python');
|
|
306
|
-
if (missionText.includes('csv')) required.push('csv');
|
|
307
|
-
if (missionText.includes('blank') || missionText.includes('whitespace')) required.push('strip');
|
|
308
|
-
|
|
419
|
+
// Dynamic contract QA: deterministic checks based on inferred contract
|
|
309
420
|
let combinedText = '';
|
|
310
421
|
for (const rel of files) {
|
|
311
422
|
const ext = path.extname(rel).toLowerCase();
|
|
@@ -313,10 +424,65 @@ async function main() {
|
|
|
313
424
|
try { combinedText += '\n' + fs.readFileSync(path.join(workDir, rel), 'utf8').toLowerCase(); } catch (_) {}
|
|
314
425
|
}
|
|
315
426
|
}
|
|
316
|
-
|
|
317
|
-
|
|
427
|
+
|
|
428
|
+
const issues = [];
|
|
429
|
+
const hasFile = (predicate) => files.some(predicate);
|
|
430
|
+
|
|
431
|
+
if (contract.language === 'python' && !hasFile((f) => f.toLowerCase().endsWith('.py'))) {
|
|
432
|
+
issues.push('expected at least one .py implementation file');
|
|
433
|
+
}
|
|
434
|
+
if (contract.language === 'javascript' && !hasFile((f) => ['.js','.ts','.tsx','.jsx'].some(e => f.toLowerCase().endsWith(e)))) {
|
|
435
|
+
issues.push('expected at least one JS/TS implementation file');
|
|
436
|
+
}
|
|
437
|
+
if (contract.requires.includes('csv') && !(combinedText.includes('csv') || hasFile((f) => f.toLowerCase().endsWith('.csv')))) {
|
|
438
|
+
issues.push('csv requirement not reflected in output');
|
|
439
|
+
}
|
|
440
|
+
if (contract.requires.includes('trim') && !(combinedText.includes('strip') || combinedText.includes('trim'))) {
|
|
441
|
+
issues.push('whitespace cleanup requirement not reflected in output');
|
|
442
|
+
}
|
|
443
|
+
for (const d of contract.deliverables || []) {
|
|
444
|
+
if (!hasFile((f) => f.toLowerCase() === String(d).toLowerCase())) {
|
|
445
|
+
issues.push('missing contract deliverable: ' + d);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Adapter-specific deterministic validators
|
|
450
|
+
if (contract.adapter === 'api') {
|
|
451
|
+
const hasApiSource = hasFile((f) => ['.js','.ts','.py','.go','.java','.rb','.php'].some(e => f.toLowerCase().endsWith(e)));
|
|
452
|
+
const hasMethod = /(get|post|put|delete|patch)\s*\//i.test(combinedText) || /(app\.|router\.|fastapi|flask|express|endpoint|route)/i.test(combinedText);
|
|
453
|
+
const hasSchema = /(request|response|schema|json|payload|status\s*code)/i.test(combinedText);
|
|
454
|
+
if (!hasApiSource) issues.push('api adapter: missing implementation source file');
|
|
455
|
+
if (!hasMethod) issues.push('api adapter: missing endpoint/method definitions');
|
|
456
|
+
if (!hasSchema) issues.push('api adapter: missing request/response contract details');
|
|
457
|
+
} else if (contract.adapter === 'web') {
|
|
458
|
+
const hasHtml = hasFile((f) => f.toLowerCase().endsWith('.html') || f.toLowerCase().endsWith('.tsx') || f.toLowerCase().endsWith('.jsx'));
|
|
459
|
+
const hasUiSignals = /(button|form|input|nav|header|section|main|aria-|onclick|onsubmit)/i.test(combinedText);
|
|
460
|
+
const hasStyleSignals = /(css|class=|tailwind|style=|flex|grid|responsive)/i.test(combinedText);
|
|
461
|
+
if (!hasHtml) issues.push('web adapter: missing page/component file');
|
|
462
|
+
if (!hasUiSignals) issues.push('web adapter: missing interactive/ui structure signals');
|
|
463
|
+
if (!hasStyleSignals) issues.push('web adapter: missing styling/responsive signals');
|
|
464
|
+
} else if (contract.adapter === 'data') {
|
|
465
|
+
const hasDataScript = hasFile((f) => ['.py','.ipynb','.sql','.js','.ts'].some(e => f.toLowerCase().endsWith(e)));
|
|
466
|
+
const hasTransform = /(transform|clean|normalize|dedup|aggregate|groupby|pandas|sql)/i.test(combinedText);
|
|
467
|
+
const hasOutputSpec = /(output|export|write|save|table|csv)/i.test(combinedText);
|
|
468
|
+
if (!hasDataScript) issues.push('data adapter: missing data-processing script/query');
|
|
469
|
+
if (!hasTransform) issues.push('data adapter: missing deterministic transform logic');
|
|
470
|
+
if (!hasOutputSpec) issues.push('data adapter: missing output definition');
|
|
471
|
+
} else if (contract.adapter === 'docs') {
|
|
472
|
+
const hasDocFile = hasFile((f) => f.toLowerCase().endsWith('.md'));
|
|
473
|
+
const hasSections = /(##\s+overview|##\s+setup|##\s+usage|table of contents|getting started)/i.test(combinedText);
|
|
474
|
+
const hasExamples = /(example|sample|command|curl|code block|```)/i.test(combinedText);
|
|
475
|
+
if (!hasDocFile) issues.push('docs adapter: missing markdown documentation');
|
|
476
|
+
if (!hasSections) issues.push('docs adapter: missing core documentation sections');
|
|
477
|
+
if (!hasExamples) issues.push('docs adapter: missing usage examples');
|
|
478
|
+
} else {
|
|
479
|
+
const hasRunnable = /(main\(|if __name__ ==|module\.exports|export default|function\s+\w+)/i.test(combinedText);
|
|
480
|
+
if (!hasRunnable) issues.push('script adapter: missing runnable implementation entry');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (issues.length > 0) {
|
|
318
484
|
await agent.setProgress(job.id, 70);
|
|
319
|
-
await agent.sendMessage(job.id, 'ā ļø QA
|
|
485
|
+
await agent.sendMessage(job.id, 'ā ļø Contract QA failed: ' + issues.slice(0, 4).join('; ') + '. Revising now.');
|
|
320
486
|
return;
|
|
321
487
|
}
|
|
322
488
|
|
|
@@ -326,9 +492,105 @@ async function main() {
|
|
|
326
492
|
return;
|
|
327
493
|
}
|
|
328
494
|
|
|
495
|
+
// Hard gate: after clarification, require at least one non-template file changed
|
|
496
|
+
try {
|
|
497
|
+
const markerPath = path.join(workDir, '..', '_clarification_' + job.id + '.json');
|
|
498
|
+
if (fs.existsSync(markerPath)) {
|
|
499
|
+
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
|
|
500
|
+
const at = Number(marker?.at || 0);
|
|
501
|
+
const templateSet = new Set(['README.md', 'CLARIFICATIONS.md', 'solution.py', 'FAILSAFE_REPORT.md']);
|
|
502
|
+
const changed = files.some((rel) => {
|
|
503
|
+
if (templateSet.has(rel)) return false;
|
|
504
|
+
const full = path.join(workDir, rel);
|
|
505
|
+
try { return fs.statSync(full).mtimeMs >= at; } catch (_) { return false; }
|
|
506
|
+
});
|
|
507
|
+
if (!changed) {
|
|
508
|
+
await agent.setProgress(job.id, 88);
|
|
509
|
+
await agent.sendMessage(job.id, 'ā ļø Final gate blocked: no non-template file changed after your latest clarification. Continuing implementation.');
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
} catch (_) {}
|
|
514
|
+
|
|
515
|
+
// Second QA worker gate (OpenClaw brain review) before final completion
|
|
516
|
+
await agent.sendMessage(job.id, 'š Running second QA gate before final completion...');
|
|
517
|
+
const qaPrompt = [
|
|
518
|
+
'You are QA worker. Validate whether deliverables satisfy mission requirements.',
|
|
519
|
+
'Return ONLY one token: QA_PASS or QA_FAIL, then one short reason line.',
|
|
520
|
+
'',
|
|
521
|
+
'MISSION TITLE: ' + job.title,
|
|
522
|
+
'MISSION DESCRIPTION: ' + job.description,
|
|
523
|
+
'FILES: ' + files.join(', '),
|
|
524
|
+
'',
|
|
525
|
+
'CONTENT SAMPLE:',
|
|
526
|
+
combinedText.slice(0, 6000),
|
|
527
|
+
].join('\n');
|
|
528
|
+
|
|
529
|
+
const qaArg = JSON.stringify(qaPrompt);
|
|
530
|
+
const qaCmds = [
|
|
531
|
+
'openclaw sessions spawn --task ' + qaArg,
|
|
532
|
+
'openclaw sessions_spawn --task ' + qaArg,
|
|
533
|
+
];
|
|
534
|
+
let qaPassed = false;
|
|
535
|
+
let qaOutput = '';
|
|
536
|
+
for (const qcmd of qaCmds) {
|
|
537
|
+
const q = await agent.execute(job.id, qcmd, { timeout: 600000, shell: true });
|
|
538
|
+
qaOutput = q.output || '';
|
|
539
|
+
if (q.exitCode === 0 && qaOutput.toUpperCase().includes('QA_PASS')) { qaPassed = true; break; }
|
|
540
|
+
if (!qaOutput.toLowerCase().includes('unknown command')) break;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (!qaPassed) {
|
|
544
|
+
const why = (qaOutput || 'No QA output').slice(-300);
|
|
545
|
+
const qaStatePath = path.join(workDir, '.qa_fail_count');
|
|
546
|
+
let qaFails = 0;
|
|
547
|
+
try { qaFails = Number(fs.readFileSync(qaStatePath, 'utf8') || '0'); } catch (_) {}
|
|
548
|
+
qaFails += 1;
|
|
549
|
+
fs.writeFileSync(qaStatePath, String(qaFails));
|
|
550
|
+
|
|
551
|
+
await agent.setProgress(job.id, 80);
|
|
552
|
+
if (qaFails >= 5) {
|
|
553
|
+
await agent.sendMessage(job.id, 'šØ Escalation: repeated QA failures (' + qaFails + '). Pausing finalization. Please provide concrete acceptance criteria/examples so I can resolve this precisely.');
|
|
554
|
+
} else if (qaFails >= 3) {
|
|
555
|
+
await agent.sendMessage(job.id, 'ā ļø Second QA gate failed (attempt ' + qaFails + '). I am revising strategy. Reason: ' + why);
|
|
556
|
+
} else {
|
|
557
|
+
await agent.sendMessage(job.id, 'ā ļø Second QA gate failed. I will revise before marking done. Reason: ' + why);
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
try { fs.unlinkSync(path.join(workDir, '.qa_fail_count')); } catch (_) {}
|
|
563
|
+
|
|
564
|
+
const crypto = require('crypto');
|
|
565
|
+
const fingerprint = crypto.createHash('sha256').update(combinedText).digest('hex');
|
|
566
|
+
const fpPath = path.join(workDir, '.last_delivery_hash');
|
|
567
|
+
if (fs.existsSync(fpPath)) {
|
|
568
|
+
const prev = fs.readFileSync(fpPath, 'utf8').trim();
|
|
569
|
+
if (prev === fingerprint) {
|
|
570
|
+
await agent.setProgress(job.id, 90);
|
|
571
|
+
await agent.sendMessage(job.id, 'ā ļø Anti-repeat gate blocked completion: output matches previous failed attempt. Revising strategy now.');
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
fs.writeFileSync(fpPath, fingerprint);
|
|
576
|
+
|
|
577
|
+
const summary = [
|
|
578
|
+
'# DELIVERY_SUMMARY',
|
|
579
|
+
'',
|
|
580
|
+
'Mission: ' + job.id,
|
|
581
|
+
'Task Type: ' + contract.taskType,
|
|
582
|
+
'Language: ' + contract.language,
|
|
583
|
+
'Files Uploaded: ' + uploaded,
|
|
584
|
+
'Acceptance Checks:',
|
|
585
|
+
'- Relevance QA: PASS',
|
|
586
|
+
'- Second QA Gate: PASS',
|
|
587
|
+
'- Non-template change after clarification: PASS',
|
|
588
|
+
].join('\n');
|
|
589
|
+
try { await agent.uploadRepoFile(job.id, 'DELIVERY_SUMMARY.md', summary, false); } catch (_) {}
|
|
590
|
+
|
|
329
591
|
await agent.setProgress(job.id, 100);
|
|
330
592
|
if (repo?.id) {
|
|
331
|
-
await agent.sendMessage(job.id, "ā
Execution complete. Deliverables uploaded: " + uploaded + " files. Repo: https://rentabots.com/repos/" + repo.id);
|
|
593
|
+
await agent.sendMessage(job.id, "ā
Execution complete. Deliverables uploaded: " + uploaded + " files. Repo: https://rentabots.com/repos/" + repo.id + " (see DELIVERY_SUMMARY.md)");
|
|
332
594
|
} else {
|
|
333
595
|
await agent.sendMessage(job.id, "ā
Execution complete. Deliverables uploaded: " + uploaded + " files to mission repository.");
|
|
334
596
|
}
|
|
@@ -337,16 +599,28 @@ async function main() {
|
|
|
337
599
|
const snippet = lastOutput.slice(-500) || 'No output captured';
|
|
338
600
|
console.error("Worker Error:", snippet);
|
|
339
601
|
|
|
340
|
-
// Fail-safe: keep user informed + attach diagnostic report
|
|
602
|
+
// Fail-safe: keep user informed + attach diagnostic report + fallback draft
|
|
341
603
|
await agent.setProgress(job.id, 25);
|
|
342
|
-
await agent.sendMessage(job.id, "ā ļø
|
|
604
|
+
await agent.sendMessage(job.id, "ā ļø OpenClaw run failed. Switching to backup adapter mode now so you don't wait idle.");
|
|
343
605
|
|
|
344
606
|
try {
|
|
607
|
+
generateAdapterFallbackFiles(workDir, job, contract);
|
|
608
|
+
|
|
345
609
|
const repoRes = await agent.getRepo(job.id);
|
|
346
610
|
if (!repoRes.success || !repoRes.exists) {
|
|
347
611
|
await agent.createRepo(job.id, 'mission-recovery-' + job.id.slice(0, 8));
|
|
348
612
|
}
|
|
349
613
|
|
|
614
|
+
// Upload fallback draft artifacts first
|
|
615
|
+
const fallbackFiles = fs.readdirSync(workDir).filter((f) => fs.statSync(path.join(workDir, f)).isFile());
|
|
616
|
+
for (const f of fallbackFiles) {
|
|
617
|
+
try {
|
|
618
|
+
const full = path.join(workDir, f);
|
|
619
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
620
|
+
await agent.uploadRepoFile(job.id, f, content, false);
|
|
621
|
+
} catch (_) {}
|
|
622
|
+
}
|
|
623
|
+
|
|
350
624
|
const report = [
|
|
351
625
|
'# FAILSAFE_REPORT',
|
|
352
626
|
'',
|