rentabots-sdk 1.7.18 ā 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 +298 -31
- package/init_templates.js +298 -30
- 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);
|
|
@@ -167,8 +216,15 @@ async function main() {
|
|
|
167
216
|
queen.workers.delete(msg.jobId);
|
|
168
217
|
}
|
|
169
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
|
+
|
|
170
226
|
await queen.sendMessage(msg.jobId, "ā
Clarification received. Worker is resuming implementation now.");
|
|
171
|
-
await spawnMissionWorker(job, 'human-followup');
|
|
227
|
+
await spawnMissionWorker(job, 'human-followup', { watchAfterClarification: true, baselineFiles });
|
|
172
228
|
});
|
|
173
229
|
|
|
174
230
|
queen.startAutopilot({ scoutingInterval: 60000, minBudget: 5 });
|
|
@@ -183,16 +239,80 @@ const { Agent } = require('rentabots-sdk');
|
|
|
183
239
|
const fs = require('fs');
|
|
184
240
|
const path = require('path');
|
|
185
241
|
|
|
186
|
-
// Read job from temp file (not CLI arg
|
|
242
|
+
// Read job from temp file (not CLI arg - avoids OS length limits)
|
|
187
243
|
const jobDataPath = process.argv[2];
|
|
188
244
|
const job = JSON.parse(fs.readFileSync(jobDataPath, 'utf8'));
|
|
189
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
|
+
|
|
190
304
|
async function main() {
|
|
191
305
|
const agent = new Agent({ persistState: false, debug: true });
|
|
192
306
|
await agent.connect();
|
|
193
307
|
await agent.sendMessage(job.id, "š· Worker Unit [PID " + process.pid + "] deployed. Analyzing requirements...");
|
|
194
308
|
|
|
195
|
-
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.';
|
|
196
316
|
|
|
197
317
|
// Use shell command string (not array args)
|
|
198
318
|
const taskArg = JSON.stringify(task);
|
|
@@ -267,7 +387,7 @@ async function main() {
|
|
|
267
387
|
|
|
268
388
|
if (overallSuccess) {
|
|
269
389
|
// Verify actual deliverables exist before claiming success
|
|
270
|
-
|
|
390
|
+
// reuse workDir initialized earlier
|
|
271
391
|
const files = [];
|
|
272
392
|
const walk = (dir, rel = '') => {
|
|
273
393
|
if (!fs.existsSync(dir)) return;
|
|
@@ -281,21 +401,11 @@ async function main() {
|
|
|
281
401
|
};
|
|
282
402
|
walk(workDir);
|
|
283
403
|
|
|
284
|
-
|
|
404
|
+
const fallbackGenerated = false;
|
|
285
405
|
if (files.length === 0) {
|
|
286
406
|
await agent.setProgress(job.id, 40);
|
|
287
|
-
await agent.sendMessage(job.id, "ā ļø No
|
|
288
|
-
|
|
289
|
-
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.';
|
|
290
|
-
const questions = '# Clarifications Needed\n\n1. Exact output file name(s)?\n2. Provide sample input data?\n3. Any strict acceptance criteria/tests?';
|
|
291
|
-
const script = "# TODO: Implement mission logic\n# Mission: " + job.title + "\n\nprint('Placeholder fallback script generated. Update with final implementation.')\n";
|
|
292
|
-
|
|
293
|
-
fs.writeFileSync(path.join(workDir, 'README.md'), readme);
|
|
294
|
-
fs.writeFileSync(path.join(workDir, 'CLARIFICATIONS.md'), questions);
|
|
295
|
-
fs.writeFileSync(path.join(workDir, 'solution.py'), script);
|
|
296
|
-
|
|
297
|
-
files.push('README.md', 'CLARIFICATIONS.md', 'solution.py');
|
|
298
|
-
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;
|
|
299
409
|
}
|
|
300
410
|
|
|
301
411
|
const repoRes = await agent.getRepo(job.id);
|
|
@@ -321,13 +431,7 @@ async function main() {
|
|
|
321
431
|
return;
|
|
322
432
|
}
|
|
323
433
|
|
|
324
|
-
//
|
|
325
|
-
const missionText = (job.title + ' ' + job.description).toLowerCase();
|
|
326
|
-
const required = [];
|
|
327
|
-
if (missionText.includes('python')) required.push('python');
|
|
328
|
-
if (missionText.includes('csv')) required.push('csv');
|
|
329
|
-
if (missionText.includes('blank') || missionText.includes('whitespace')) required.push('strip');
|
|
330
|
-
|
|
434
|
+
// Dynamic contract QA: deterministic checks based on inferred contract
|
|
331
435
|
let combinedText = '';
|
|
332
436
|
for (const rel of files) {
|
|
333
437
|
const ext = path.extname(rel).toLowerCase();
|
|
@@ -335,10 +439,65 @@ async function main() {
|
|
|
335
439
|
try { combinedText += '\n' + fs.readFileSync(path.join(workDir, rel), 'utf8').toLowerCase(); } catch (_) {}
|
|
336
440
|
}
|
|
337
441
|
}
|
|
338
|
-
|
|
339
|
-
|
|
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) {
|
|
340
499
|
await agent.setProgress(job.id, 70);
|
|
341
|
-
await agent.sendMessage(job.id, 'ā ļø QA
|
|
500
|
+
await agent.sendMessage(job.id, 'ā ļø Contract QA failed: ' + issues.slice(0, 4).join('; ') + '. Revising now.');
|
|
342
501
|
return;
|
|
343
502
|
}
|
|
344
503
|
|
|
@@ -348,9 +507,105 @@ async function main() {
|
|
|
348
507
|
return;
|
|
349
508
|
}
|
|
350
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
|
+
|
|
351
606
|
await agent.setProgress(job.id, 100);
|
|
352
607
|
if (repo?.id) {
|
|
353
|
-
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)");
|
|
354
609
|
} else {
|
|
355
610
|
await agent.sendMessage(job.id, "ā
Execution complete. Deliverables uploaded: " + uploaded + " files to mission repository.");
|
|
356
611
|
}
|
|
@@ -359,16 +614,28 @@ async function main() {
|
|
|
359
614
|
const snippet = lastOutput.slice(-500) || 'No output captured';
|
|
360
615
|
console.error("Worker Error:", snippet);
|
|
361
616
|
|
|
362
|
-
// Fail-safe: keep user informed + attach diagnostic report
|
|
617
|
+
// Fail-safe: keep user informed + attach diagnostic report + fallback draft
|
|
363
618
|
await agent.setProgress(job.id, 25);
|
|
364
|
-
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.");
|
|
365
620
|
|
|
366
621
|
try {
|
|
622
|
+
generateAdapterFallbackFiles(workDir, job, contract);
|
|
623
|
+
|
|
367
624
|
const repoRes = await agent.getRepo(job.id);
|
|
368
625
|
if (!repoRes.success || !repoRes.exists) {
|
|
369
626
|
await agent.createRepo(job.id, 'mission-recovery-' + job.id.slice(0, 8));
|
|
370
627
|
}
|
|
371
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
|
+
|
|
372
639
|
const report = [
|
|
373
640
|
'# FAILSAFE_REPORT',
|
|
374
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);
|
|
@@ -136,8 +185,15 @@ async function main() {
|
|
|
136
185
|
queen.workers.delete(msg.jobId);
|
|
137
186
|
}
|
|
138
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
|
+
|
|
139
195
|
await queen.sendMessage(msg.jobId, "ā
Clarification received. Worker is resuming implementation now.");
|
|
140
|
-
await spawnMissionWorker(job, 'human-followup');
|
|
196
|
+
await spawnMissionWorker(job, 'human-followup', { watchAfterClarification: true, baselineFiles });
|
|
141
197
|
});
|
|
142
198
|
|
|
143
199
|
queen.startAutopilot({
|
|
@@ -163,6 +219,65 @@ const path = require('path');
|
|
|
163
219
|
const jobDataPath = process.argv[2];
|
|
164
220
|
const job = JSON.parse(fs.readFileSync(jobDataPath, 'utf8'));
|
|
165
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
|
+
|
|
166
281
|
async function main() {
|
|
167
282
|
const agent = new Agent({
|
|
168
283
|
persistState: false,
|
|
@@ -174,8 +289,14 @@ async function main() {
|
|
|
174
289
|
// Announce arrival in the mission channel
|
|
175
290
|
await agent.sendMessage(job.id, "š· Worker Unit [PID " + process.pid + "] deployed to workspace. Analyzing requirements...");
|
|
176
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
|
+
|
|
177
298
|
// Construct the prompt for the autonomous brain
|
|
178
|
-
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.\`;
|
|
179
300
|
|
|
180
301
|
// --- š¦ EXECUTE VIA OPENCLAW BRAIN ---
|
|
181
302
|
const taskArg = JSON.stringify(task);
|
|
@@ -251,7 +372,7 @@ async function main() {
|
|
|
251
372
|
|
|
252
373
|
if (overallSuccess) {
|
|
253
374
|
// Verify actual deliverables exist before claiming success
|
|
254
|
-
|
|
375
|
+
// reuse workDir initialized earlier
|
|
255
376
|
const files = [];
|
|
256
377
|
const walk = (dir, rel = '') => {
|
|
257
378
|
if (!fs.existsSync(dir)) return;
|
|
@@ -265,21 +386,11 @@ async function main() {
|
|
|
265
386
|
};
|
|
266
387
|
walk(workDir);
|
|
267
388
|
|
|
268
|
-
|
|
389
|
+
const fallbackGenerated = false;
|
|
269
390
|
if (files.length === 0) {
|
|
270
391
|
await agent.setProgress(job.id, 40);
|
|
271
|
-
await agent.sendMessage(job.id, "ā ļø No
|
|
272
|
-
|
|
273
|
-
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.';
|
|
274
|
-
const questions = '# Clarifications Needed\n\n1. Exact output file name(s)?\n2. Provide sample input data?\n3. Any strict acceptance criteria/tests?';
|
|
275
|
-
const script = "# TODO: Implement mission logic\n# Mission: " + job.title + "\n\nprint('Placeholder fallback script generated. Update with final implementation.')\n";
|
|
276
|
-
|
|
277
|
-
fs.writeFileSync(path.join(workDir, 'README.md'), readme);
|
|
278
|
-
fs.writeFileSync(path.join(workDir, 'CLARIFICATIONS.md'), questions);
|
|
279
|
-
fs.writeFileSync(path.join(workDir, 'solution.py'), script);
|
|
280
|
-
|
|
281
|
-
files.push('README.md', 'CLARIFICATIONS.md', 'solution.py');
|
|
282
|
-
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;
|
|
283
394
|
}
|
|
284
395
|
|
|
285
396
|
const repoRes = await agent.getRepo(job.id);
|
|
@@ -305,13 +416,7 @@ async function main() {
|
|
|
305
416
|
return;
|
|
306
417
|
}
|
|
307
418
|
|
|
308
|
-
//
|
|
309
|
-
const missionText = (job.title + ' ' + job.description).toLowerCase();
|
|
310
|
-
const required = [];
|
|
311
|
-
if (missionText.includes('python')) required.push('python');
|
|
312
|
-
if (missionText.includes('csv')) required.push('csv');
|
|
313
|
-
if (missionText.includes('blank') || missionText.includes('whitespace')) required.push('strip');
|
|
314
|
-
|
|
419
|
+
// Dynamic contract QA: deterministic checks based on inferred contract
|
|
315
420
|
let combinedText = '';
|
|
316
421
|
for (const rel of files) {
|
|
317
422
|
const ext = path.extname(rel).toLowerCase();
|
|
@@ -319,10 +424,65 @@ async function main() {
|
|
|
319
424
|
try { combinedText += '\n' + fs.readFileSync(path.join(workDir, rel), 'utf8').toLowerCase(); } catch (_) {}
|
|
320
425
|
}
|
|
321
426
|
}
|
|
322
|
-
|
|
323
|
-
|
|
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) {
|
|
324
484
|
await agent.setProgress(job.id, 70);
|
|
325
|
-
await agent.sendMessage(job.id, 'ā ļø QA
|
|
485
|
+
await agent.sendMessage(job.id, 'ā ļø Contract QA failed: ' + issues.slice(0, 4).join('; ') + '. Revising now.');
|
|
326
486
|
return;
|
|
327
487
|
}
|
|
328
488
|
|
|
@@ -332,9 +492,105 @@ async function main() {
|
|
|
332
492
|
return;
|
|
333
493
|
}
|
|
334
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
|
+
|
|
335
591
|
await agent.setProgress(job.id, 100);
|
|
336
592
|
if (repo?.id) {
|
|
337
|
-
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)");
|
|
338
594
|
} else {
|
|
339
595
|
await agent.sendMessage(job.id, "ā
Execution complete. Deliverables uploaded: " + uploaded + " files to mission repository.");
|
|
340
596
|
}
|
|
@@ -343,16 +599,28 @@ async function main() {
|
|
|
343
599
|
const snippet = lastOutput.slice(-500) || 'No output captured';
|
|
344
600
|
console.error("Worker Error:", snippet);
|
|
345
601
|
|
|
346
|
-
// Fail-safe: keep user informed + attach diagnostic report
|
|
602
|
+
// Fail-safe: keep user informed + attach diagnostic report + fallback draft
|
|
347
603
|
await agent.setProgress(job.id, 25);
|
|
348
|
-
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.");
|
|
349
605
|
|
|
350
606
|
try {
|
|
607
|
+
generateAdapterFallbackFiles(workDir, job, contract);
|
|
608
|
+
|
|
351
609
|
const repoRes = await agent.getRepo(job.id);
|
|
352
610
|
if (!repoRes.success || !repoRes.exists) {
|
|
353
611
|
await agent.createRepo(job.id, 'mission-recovery-' + job.id.slice(0, 8));
|
|
354
612
|
}
|
|
355
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
|
+
|
|
356
624
|
const report = [
|
|
357
625
|
'# FAILSAFE_REPORT',
|
|
358
626
|
'',
|