rentabots-sdk 1.7.18 → 1.7.27

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 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.18'; // fallback when package.json is unavailable
74
+ let SDK_VERSION = '1.7.27'; // 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 spawnMissionWorker = async (job, source = 'assignment') => {
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 avoids OS length limits)
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 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.';
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
- const workDir = path.join(agent.workspaceRoot, job.id);
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
- let fallbackGenerated = false;
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 deliverable files were produced yet. Entering forced-build fallback and generating starter deliverables now.");
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
- // Lightweight QA: ensure deliverables are relevant to mission intent
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,11 +439,101 @@ async function main() {
335
439
  try { combinedText += '\n' + fs.readFileSync(path.join(workDir, rel), 'utf8').toLowerCase(); } catch (_) {}
336
440
  }
337
441
  }
338
- const missing = required.filter(k => !combinedText.includes(k));
339
- if (missing.length > 0) {
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 check failed: output appears misaligned with mission (' + missing.join(', ') + ' not found). I will revise now.');
342
- return;
500
+ await agent.sendMessage(job.id, '⚠️ Contract QA failed: ' + issues.slice(0, 4).join('; ') + '. Running targeted OpenClaw repair pass now.');
501
+
502
+ const repairPath = path.join(workDir, '.repair_count');
503
+ let repairCount = 0;
504
+ try { repairCount = Number(fs.readFileSync(repairPath, 'utf8') || '0'); } catch (_) {}
505
+ repairCount += 1;
506
+ fs.writeFileSync(repairPath, String(repairCount));
507
+
508
+ if (repairCount > 6) {
509
+ await agent.sendMessage(job.id, '🚨 OpenClaw repair limit reached. Please send one concrete clarification so I can continue with a precise fix.');
510
+ return;
511
+ }
512
+
513
+ const repairPrompt = [
514
+ 'Repair the existing workspace to satisfy missing deterministic gates.',
515
+ 'Do not explain. Edit files now and finish.',
516
+ 'MISSING GATES:',
517
+ ...issues.map(i => '- ' + i),
518
+ '',
519
+ 'MISSION TITLE: ' + job.title,
520
+ 'MISSION DESCRIPTION: ' + job.description,
521
+ ].join('\n');
522
+
523
+ const repairArg = JSON.stringify(repairPrompt);
524
+ const repairCmds = [
525
+ 'openclaw sessions spawn --task ' + repairArg,
526
+ 'openclaw sessions_spawn --task ' + repairArg,
527
+ ];
528
+ for (const rcmd of repairCmds) {
529
+ const r = await agent.execute(job.id, rcmd, { timeout: 900000, shell: true });
530
+ const out = (r.output || '').toLowerCase();
531
+ const pseudo = out.includes('usage: openclaw') || out.includes('unknown command') || out.includes('pass --to');
532
+ if (r.exitCode === 0 && !pseudo) break;
533
+ }
534
+
535
+ await agent.sendMessage(job.id, '🔁 OpenClaw repair pass complete. Re-validating now.');
536
+ return main();
343
537
  }
344
538
 
345
539
  if (fallbackGenerated) {
@@ -348,9 +542,105 @@ async function main() {
348
542
  return;
349
543
  }
350
544
 
545
+ // Hard gate: after clarification, require at least one non-template file changed
546
+ try {
547
+ const markerPath = path.join(workDir, '..', '_clarification_' + job.id + '.json');
548
+ if (fs.existsSync(markerPath)) {
549
+ const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
550
+ const at = Number(marker?.at || 0);
551
+ const templateSet = new Set(['README.md', 'CLARIFICATIONS.md', 'solution.py', 'FAILSAFE_REPORT.md']);
552
+ const changed = files.some((rel) => {
553
+ if (templateSet.has(rel)) return false;
554
+ const full = path.join(workDir, rel);
555
+ try { return fs.statSync(full).mtimeMs >= at; } catch (_) { return false; }
556
+ });
557
+ if (!changed) {
558
+ await agent.setProgress(job.id, 88);
559
+ await agent.sendMessage(job.id, '⚠️ Final gate blocked: no non-template file changed after your latest clarification. Continuing implementation.');
560
+ return;
561
+ }
562
+ }
563
+ } catch (_) {}
564
+
565
+ // Second QA worker gate (OpenClaw brain review) before final completion
566
+ await agent.sendMessage(job.id, '🔎 Running second QA gate before final completion...');
567
+ const qaPrompt = [
568
+ 'You are QA worker. Validate whether deliverables satisfy mission requirements.',
569
+ 'Return ONLY one token: QA_PASS or QA_FAIL, then one short reason line.',
570
+ '',
571
+ 'MISSION TITLE: ' + job.title,
572
+ 'MISSION DESCRIPTION: ' + job.description,
573
+ 'FILES: ' + files.join(', '),
574
+ '',
575
+ 'CONTENT SAMPLE:',
576
+ combinedText.slice(0, 6000),
577
+ ].join('\n');
578
+
579
+ const qaArg = JSON.stringify(qaPrompt);
580
+ const qaCmds = [
581
+ 'openclaw sessions spawn --task ' + qaArg,
582
+ 'openclaw sessions_spawn --task ' + qaArg,
583
+ ];
584
+ let qaPassed = false;
585
+ let qaOutput = '';
586
+ for (const qcmd of qaCmds) {
587
+ const q = await agent.execute(job.id, qcmd, { timeout: 600000, shell: true });
588
+ qaOutput = q.output || '';
589
+ if (q.exitCode === 0 && qaOutput.toUpperCase().includes('QA_PASS')) { qaPassed = true; break; }
590
+ if (!qaOutput.toLowerCase().includes('unknown command')) break;
591
+ }
592
+
593
+ if (!qaPassed) {
594
+ const why = (qaOutput || 'No QA output').slice(-300);
595
+ const qaStatePath = path.join(workDir, '.qa_fail_count');
596
+ let qaFails = 0;
597
+ try { qaFails = Number(fs.readFileSync(qaStatePath, 'utf8') || '0'); } catch (_) {}
598
+ qaFails += 1;
599
+ fs.writeFileSync(qaStatePath, String(qaFails));
600
+
601
+ await agent.setProgress(job.id, 80);
602
+ if (qaFails >= 5) {
603
+ await agent.sendMessage(job.id, '🚨 Escalation: repeated QA failures (' + qaFails + '). Pausing finalization. Please provide concrete acceptance criteria/examples so I can resolve this precisely.');
604
+ } else if (qaFails >= 3) {
605
+ await agent.sendMessage(job.id, '⚠️ Second QA gate failed (attempt ' + qaFails + '). I am revising strategy. Reason: ' + why);
606
+ } else {
607
+ await agent.sendMessage(job.id, '⚠️ Second QA gate failed. I will revise before marking done. Reason: ' + why);
608
+ }
609
+ return;
610
+ }
611
+
612
+ try { fs.unlinkSync(path.join(workDir, '.qa_fail_count')); } catch (_) {}
613
+
614
+ const crypto = require('crypto');
615
+ const fingerprint = crypto.createHash('sha256').update(combinedText).digest('hex');
616
+ const fpPath = path.join(workDir, '.last_delivery_hash');
617
+ if (fs.existsSync(fpPath)) {
618
+ const prev = fs.readFileSync(fpPath, 'utf8').trim();
619
+ if (prev === fingerprint) {
620
+ await agent.setProgress(job.id, 90);
621
+ await agent.sendMessage(job.id, '⚠️ Anti-repeat gate blocked completion: output matches previous failed attempt. Revising strategy now.');
622
+ return;
623
+ }
624
+ }
625
+ fs.writeFileSync(fpPath, fingerprint);
626
+
627
+ const summary = [
628
+ '# DELIVERY_SUMMARY',
629
+ '',
630
+ 'Mission: ' + job.id,
631
+ 'Task Type: ' + contract.taskType,
632
+ 'Language: ' + contract.language,
633
+ 'Files Uploaded: ' + uploaded,
634
+ 'Acceptance Checks:',
635
+ '- Relevance QA: PASS',
636
+ '- Second QA Gate: PASS',
637
+ '- Non-template change after clarification: PASS',
638
+ ].join('\n');
639
+ try { await agent.uploadRepoFile(job.id, 'DELIVERY_SUMMARY.md', summary, false); } catch (_) {}
640
+
351
641
  await agent.setProgress(job.id, 100);
352
642
  if (repo?.id) {
353
- await agent.sendMessage(job.id, "✅ Execution complete. Deliverables uploaded: " + uploaded + " files. Repo: https://rentabots.com/repos/" + repo.id);
643
+ await agent.sendMessage(job.id, "✅ Execution complete. Deliverables uploaded: " + uploaded + " files. Repo: https://rentabots.com/repos/" + repo.id + " (see DELIVERY_SUMMARY.md)");
354
644
  } else {
355
645
  await agent.sendMessage(job.id, "✅ Execution complete. Deliverables uploaded: " + uploaded + " files to mission repository.");
356
646
  }
@@ -359,9 +649,22 @@ async function main() {
359
649
  const snippet = lastOutput.slice(-500) || 'No output captured';
360
650
  console.error("Worker Error:", snippet);
361
651
 
362
- // Fail-safe: keep user informed + attach diagnostic report to mission repo
652
+ const failPath = path.join(workDir, '.openclaw_fail_count');
653
+ let fails = 0;
654
+ try { fails = Number(fs.readFileSync(failPath, 'utf8') || '0'); } catch (_) {}
655
+ fails += 1;
656
+ fs.writeFileSync(failPath, String(fails));
657
+
658
+ if (fails <= 5) {
659
+ await agent.setProgress(job.id, 25);
660
+ await agent.sendMessage(job.id, '⚠️ OpenClaw run failed (attempt ' + fails + '). Auto-retrying with OpenClaw only...');
661
+ await new Promise(r => setTimeout(r, 5000));
662
+ return main();
663
+ }
664
+
665
+ // Fail-safe report only after OpenClaw retry budget exhausted
363
666
  await agent.setProgress(job.id, 25);
364
- await agent.sendMessage(job.id, "⚠️ I hit an execution issue, but fail-safe mode is active. I’m preparing a diagnostic report and recovery path.");
667
+ await agent.sendMessage(job.id, "🚨 OpenClaw execution repeatedly failed. Please send one concrete clarification and I will continue immediately.");
365
668
 
366
669
  try {
367
670
  const repoRes = await agent.getRepo(job.id);
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 spawnMissionWorker = async (job, source = 'assignment') => {
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
- const workDir = path.join(agent.workspaceRoot, job.id);
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
- let fallbackGenerated = false;
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 deliverable files were produced yet. Entering forced-build fallback and generating starter deliverables now.");
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
- // Lightweight QA: ensure deliverables are relevant to mission intent
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,11 +424,101 @@ async function main() {
319
424
  try { combinedText += '\n' + fs.readFileSync(path.join(workDir, rel), 'utf8').toLowerCase(); } catch (_) {}
320
425
  }
321
426
  }
322
- const missing = required.filter(k => !combinedText.includes(k));
323
- if (missing.length > 0) {
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 check failed: output appears misaligned with mission (' + missing.join(', ') + ' not found). I will revise now.');
326
- return;
485
+ await agent.sendMessage(job.id, '⚠️ Contract QA failed: ' + issues.slice(0, 4).join('; ') + '. Running targeted OpenClaw repair pass now.');
486
+
487
+ const repairPath = path.join(workDir, '.repair_count');
488
+ let repairCount = 0;
489
+ try { repairCount = Number(fs.readFileSync(repairPath, 'utf8') || '0'); } catch (_) {}
490
+ repairCount += 1;
491
+ fs.writeFileSync(repairPath, String(repairCount));
492
+
493
+ if (repairCount > 6) {
494
+ await agent.sendMessage(job.id, '🚨 OpenClaw repair limit reached. Please send one concrete clarification so I can continue with a precise fix.');
495
+ return;
496
+ }
497
+
498
+ const repairPrompt = [
499
+ 'Repair the existing workspace to satisfy missing deterministic gates.',
500
+ 'Do not explain. Edit files now and finish.',
501
+ 'MISSING GATES:',
502
+ ...issues.map(i => '- ' + i),
503
+ '',
504
+ 'MISSION TITLE: ' + job.title,
505
+ 'MISSION DESCRIPTION: ' + job.description,
506
+ ].join('\n');
507
+
508
+ const repairArg = JSON.stringify(repairPrompt);
509
+ const repairCmds = [
510
+ 'openclaw sessions spawn --task ' + repairArg,
511
+ 'openclaw sessions_spawn --task ' + repairArg,
512
+ ];
513
+ for (const rcmd of repairCmds) {
514
+ const r = await agent.execute(job.id, rcmd, { timeout: 900000, shell: true });
515
+ const out = (r.output || '').toLowerCase();
516
+ const pseudo = out.includes('usage: openclaw') || out.includes('unknown command') || out.includes('pass --to');
517
+ if (r.exitCode === 0 && !pseudo) break;
518
+ }
519
+
520
+ await agent.sendMessage(job.id, '🔁 OpenClaw repair pass complete. Re-validating now.');
521
+ return main();
327
522
  }
328
523
 
329
524
  if (fallbackGenerated) {
@@ -332,9 +527,105 @@ async function main() {
332
527
  return;
333
528
  }
334
529
 
530
+ // Hard gate: after clarification, require at least one non-template file changed
531
+ try {
532
+ const markerPath = path.join(workDir, '..', '_clarification_' + job.id + '.json');
533
+ if (fs.existsSync(markerPath)) {
534
+ const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
535
+ const at = Number(marker?.at || 0);
536
+ const templateSet = new Set(['README.md', 'CLARIFICATIONS.md', 'solution.py', 'FAILSAFE_REPORT.md']);
537
+ const changed = files.some((rel) => {
538
+ if (templateSet.has(rel)) return false;
539
+ const full = path.join(workDir, rel);
540
+ try { return fs.statSync(full).mtimeMs >= at; } catch (_) { return false; }
541
+ });
542
+ if (!changed) {
543
+ await agent.setProgress(job.id, 88);
544
+ await agent.sendMessage(job.id, '⚠️ Final gate blocked: no non-template file changed after your latest clarification. Continuing implementation.');
545
+ return;
546
+ }
547
+ }
548
+ } catch (_) {}
549
+
550
+ // Second QA worker gate (OpenClaw brain review) before final completion
551
+ await agent.sendMessage(job.id, '🔎 Running second QA gate before final completion...');
552
+ const qaPrompt = [
553
+ 'You are QA worker. Validate whether deliverables satisfy mission requirements.',
554
+ 'Return ONLY one token: QA_PASS or QA_FAIL, then one short reason line.',
555
+ '',
556
+ 'MISSION TITLE: ' + job.title,
557
+ 'MISSION DESCRIPTION: ' + job.description,
558
+ 'FILES: ' + files.join(', '),
559
+ '',
560
+ 'CONTENT SAMPLE:',
561
+ combinedText.slice(0, 6000),
562
+ ].join('\n');
563
+
564
+ const qaArg = JSON.stringify(qaPrompt);
565
+ const qaCmds = [
566
+ 'openclaw sessions spawn --task ' + qaArg,
567
+ 'openclaw sessions_spawn --task ' + qaArg,
568
+ ];
569
+ let qaPassed = false;
570
+ let qaOutput = '';
571
+ for (const qcmd of qaCmds) {
572
+ const q = await agent.execute(job.id, qcmd, { timeout: 600000, shell: true });
573
+ qaOutput = q.output || '';
574
+ if (q.exitCode === 0 && qaOutput.toUpperCase().includes('QA_PASS')) { qaPassed = true; break; }
575
+ if (!qaOutput.toLowerCase().includes('unknown command')) break;
576
+ }
577
+
578
+ if (!qaPassed) {
579
+ const why = (qaOutput || 'No QA output').slice(-300);
580
+ const qaStatePath = path.join(workDir, '.qa_fail_count');
581
+ let qaFails = 0;
582
+ try { qaFails = Number(fs.readFileSync(qaStatePath, 'utf8') || '0'); } catch (_) {}
583
+ qaFails += 1;
584
+ fs.writeFileSync(qaStatePath, String(qaFails));
585
+
586
+ await agent.setProgress(job.id, 80);
587
+ if (qaFails >= 5) {
588
+ await agent.sendMessage(job.id, '🚨 Escalation: repeated QA failures (' + qaFails + '). Pausing finalization. Please provide concrete acceptance criteria/examples so I can resolve this precisely.');
589
+ } else if (qaFails >= 3) {
590
+ await agent.sendMessage(job.id, '⚠️ Second QA gate failed (attempt ' + qaFails + '). I am revising strategy. Reason: ' + why);
591
+ } else {
592
+ await agent.sendMessage(job.id, '⚠️ Second QA gate failed. I will revise before marking done. Reason: ' + why);
593
+ }
594
+ return;
595
+ }
596
+
597
+ try { fs.unlinkSync(path.join(workDir, '.qa_fail_count')); } catch (_) {}
598
+
599
+ const crypto = require('crypto');
600
+ const fingerprint = crypto.createHash('sha256').update(combinedText).digest('hex');
601
+ const fpPath = path.join(workDir, '.last_delivery_hash');
602
+ if (fs.existsSync(fpPath)) {
603
+ const prev = fs.readFileSync(fpPath, 'utf8').trim();
604
+ if (prev === fingerprint) {
605
+ await agent.setProgress(job.id, 90);
606
+ await agent.sendMessage(job.id, '⚠️ Anti-repeat gate blocked completion: output matches previous failed attempt. Revising strategy now.');
607
+ return;
608
+ }
609
+ }
610
+ fs.writeFileSync(fpPath, fingerprint);
611
+
612
+ const summary = [
613
+ '# DELIVERY_SUMMARY',
614
+ '',
615
+ 'Mission: ' + job.id,
616
+ 'Task Type: ' + contract.taskType,
617
+ 'Language: ' + contract.language,
618
+ 'Files Uploaded: ' + uploaded,
619
+ 'Acceptance Checks:',
620
+ '- Relevance QA: PASS',
621
+ '- Second QA Gate: PASS',
622
+ '- Non-template change after clarification: PASS',
623
+ ].join('\n');
624
+ try { await agent.uploadRepoFile(job.id, 'DELIVERY_SUMMARY.md', summary, false); } catch (_) {}
625
+
335
626
  await agent.setProgress(job.id, 100);
336
627
  if (repo?.id) {
337
- await agent.sendMessage(job.id, "✅ Execution complete. Deliverables uploaded: " + uploaded + " files. Repo: https://rentabots.com/repos/" + repo.id);
628
+ await agent.sendMessage(job.id, "✅ Execution complete. Deliverables uploaded: " + uploaded + " files. Repo: https://rentabots.com/repos/" + repo.id + " (see DELIVERY_SUMMARY.md)");
338
629
  } else {
339
630
  await agent.sendMessage(job.id, "✅ Execution complete. Deliverables uploaded: " + uploaded + " files to mission repository.");
340
631
  }
@@ -343,9 +634,22 @@ async function main() {
343
634
  const snippet = lastOutput.slice(-500) || 'No output captured';
344
635
  console.error("Worker Error:", snippet);
345
636
 
346
- // Fail-safe: keep user informed + attach diagnostic report to mission repo
637
+ const failPath = path.join(workDir, '.openclaw_fail_count');
638
+ let fails = 0;
639
+ try { fails = Number(fs.readFileSync(failPath, 'utf8') || '0'); } catch (_) {}
640
+ fails += 1;
641
+ fs.writeFileSync(failPath, String(fails));
642
+
643
+ if (fails <= 5) {
644
+ await agent.setProgress(job.id, 25);
645
+ await agent.sendMessage(job.id, '⚠️ OpenClaw run failed (attempt ' + fails + '). Auto-retrying with OpenClaw only...');
646
+ await new Promise(r => setTimeout(r, 5000));
647
+ return main();
648
+ }
649
+
650
+ // Fail-safe report only after OpenClaw retry budget exhausted
347
651
  await agent.setProgress(job.id, 25);
348
- await agent.sendMessage(job.id, "⚠️ I hit an execution issue, but fail-safe mode is active. I’m preparing a diagnostic report and recovery path.");
652
+ await agent.sendMessage(job.id, "🚨 OpenClaw execution repeatedly failed. Please send one concrete clarification and I will continue immediately.");
349
653
 
350
654
  try {
351
655
  const repoRes = await agent.getRepo(job.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rentabots-sdk",
3
- "version": "1.7.18",
3
+ "version": "1.7.27",
4
4
  "description": "Official SDK for RentaBots AI Agent Marketplace",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",