rentabots-sdk 1.7.17 → 1.7.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js 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.17'; // fallback when package.json is unavailable
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 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);
@@ -152,16 +201,30 @@ async function main() {
152
201
 
153
202
  queen.on('message', async (msg) => {
154
203
  if (msg.sender.type === 'agent') return;
155
- if (!queen.workers.has(msg.jobId)) {
156
- const job = queen.activeMissions.get(msg.jobId);
157
- if (!job) {
158
- await queen.sendMessage(msg.jobId, "šŸ¤– Supervisor online. Mission sync in progress.");
159
- await pushLog('WARN', 'Message received for unknown mission ' + msg.jobId);
160
- return;
161
- }
162
- await queen.sendMessage(msg.jobId, "šŸ¤– Supervisor here. Dispatching a worker shortly.");
163
- await spawnMissionWorker(job, 'message-trigger');
204
+
205
+ const job = queen.activeMissions.get(msg.jobId);
206
+ if (!job) {
207
+ await queen.sendMessage(msg.jobId, "šŸ¤– Supervisor online. Mission sync in progress.");
208
+ await pushLog('WARN', 'Message received for unknown mission ' + msg.jobId);
209
+ return;
210
+ }
211
+
212
+ // Human clarification should immediately re-trigger implementation.
213
+ if (queen.workers.has(msg.jobId)) {
214
+ await pushLog('INFO', 'Human update received for ' + msg.jobId + '; restarting worker for immediate implementation.');
215
+ try { queen.workers.get(msg.jobId)?.kill(); } catch (_) {}
216
+ queen.workers.delete(msg.jobId);
164
217
  }
218
+
219
+ const baselineFiles = await getRepoFileCount(msg.jobId);
220
+ try {
221
+ const markerPath = path.join(__dirname, 'workspace', '_clarification_' + msg.jobId + '.json');
222
+ fs.mkdirSync(path.dirname(markerPath), { recursive: true });
223
+ fs.writeFileSync(markerPath, JSON.stringify({ at: Date.now(), content: msg.content || '' }));
224
+ } catch (_) {}
225
+
226
+ await queen.sendMessage(msg.jobId, "āœ… Clarification received. Worker is resuming implementation now.");
227
+ await spawnMissionWorker(job, 'human-followup', { watchAfterClarification: true, baselineFiles });
165
228
  });
166
229
 
167
230
  queen.startAutopilot({ scoutingInterval: 60000, minBudget: 5 });
@@ -176,16 +239,80 @@ const { Agent } = require('rentabots-sdk');
176
239
  const fs = require('fs');
177
240
  const path = require('path');
178
241
 
179
- // Read job from temp file (not CLI arg — avoids OS length limits)
242
+ // Read job from temp file (not CLI arg - avoids OS length limits)
180
243
  const jobDataPath = process.argv[2];
181
244
  const job = JSON.parse(fs.readFileSync(jobDataPath, 'utf8'));
182
245
 
246
+ function inferContract(job) {
247
+ const text = ((job.title || '') + ' ' + (job.description || '')).toLowerCase();
248
+ const adapter = text.includes('api') ? 'api' : text.includes('csv') || text.includes('dataset') || text.includes('etl') ? 'data' : text.includes('landing page') || text.includes('frontend') || text.includes('ui') || text.includes('web') ? 'web' : text.includes('docs') || text.includes('documentation') ? 'docs' : 'script';
249
+ const contract = {
250
+ adapter,
251
+ taskType: adapter,
252
+ language: text.includes('python') ? 'python' : (text.includes('javascript') || text.includes('node') ? 'javascript' : 'unspecified'),
253
+ requires: [],
254
+ deliverables: ['README.md'],
255
+ acceptance: []
256
+ };
257
+ if (text.includes('csv')) contract.requires.push('csv');
258
+ if (text.includes('blank') || text.includes('whitespace') || text.includes('trim')) contract.requires.push('trim');
259
+ if (contract.language === 'python') contract.deliverables.push('solution.py');
260
+ if (adapter === 'api') contract.deliverables.push('openapi-or-endpoint-docs.md');
261
+ if (adapter === 'web') contract.deliverables.push('index.html');
262
+ if (adapter === 'data') contract.deliverables.push('data_dictionary.md');
263
+ if (adapter === 'docs') contract.deliverables.push('DOCUMENTATION.md');
264
+ contract.acceptance.push('output reflects mission title and description');
265
+ contract.acceptance.push('at least one non-template implementation file');
266
+ return contract;
267
+ }
268
+
269
+ function generateAdapterFallbackFiles(workDir, job, contract) {
270
+ const brief = (job.description || '').trim();
271
+ const readme = [
272
+ '# Mission Recovery Draft',
273
+ '',
274
+ 'This draft was auto-generated because OpenClaw execution failed at runtime.',
275
+ '',
276
+ '## Mission',
277
+ '- Title: ' + (job.title || ''),
278
+ '- Adapter: ' + contract.adapter,
279
+ '- Language: ' + contract.language,
280
+ '',
281
+ '## Brief',
282
+ brief || '(none provided)',
283
+ '',
284
+ '## Next step',
285
+ 'Review this draft and send clarification in mission chat for refinement.'
286
+ ].join('\n');
287
+ fs.writeFileSync(path.join(workDir, 'README.md'), readme);
288
+
289
+ if (contract.adapter === 'api') {
290
+ fs.writeFileSync(path.join(workDir, 'openapi-or-endpoint-docs.md'), '# API Draft\n\n- GET /health\n- POST /process\n\nDescribe request/response schema and validations.');
291
+ } else if (contract.adapter === 'web') {
292
+ fs.writeFileSync(path.join(workDir, 'index.html'), '<!doctype html><html><body><h1>Recovery Draft</h1><p>Mission: ' + (job.title || '') + '</p></body></html>');
293
+ } else if (contract.adapter === 'data') {
294
+ fs.writeFileSync(path.join(workDir, 'data_dictionary.md'), '# Data Dictionary Draft\n\n| Field | Type | Notes |\n|---|---|---|\n| id | string | unique id |');
295
+ } else if (contract.adapter === 'docs') {
296
+ fs.writeFileSync(path.join(workDir, 'DOCUMENTATION.md'), '# Documentation Draft\n\n## Overview\n\n## Setup\n\n## Usage\n');
297
+ } else if (contract.language === 'python') {
298
+ fs.writeFileSync(path.join(workDir, 'solution.py'), 'def main():\n print("recovery draft")\n\nif __name__ == "__main__":\n main()\n');
299
+ } else {
300
+ fs.writeFileSync(path.join(workDir, 'solution.js'), 'console.log("recovery draft");\n');
301
+ }
302
+ }
303
+
183
304
  async function main() {
184
305
  const agent = new Agent({ persistState: false, debug: true });
185
306
  await agent.connect();
186
307
  await agent.sendMessage(job.id, "šŸ‘· Worker Unit [PID " + process.pid + "] deployed. Analyzing requirements...");
187
308
 
188
- const 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.';
189
316
 
190
317
  // Use shell command string (not array args)
191
318
  const taskArg = JSON.stringify(task);
@@ -260,7 +387,7 @@ async function main() {
260
387
 
261
388
  if (overallSuccess) {
262
389
  // Verify actual deliverables exist before claiming success
263
- const workDir = path.join(agent.workspaceRoot, job.id);
390
+ // reuse workDir initialized earlier
264
391
  const files = [];
265
392
  const walk = (dir, rel = '') => {
266
393
  if (!fs.existsSync(dir)) return;
@@ -274,21 +401,11 @@ async function main() {
274
401
  };
275
402
  walk(workDir);
276
403
 
277
- let fallbackGenerated = false;
404
+ const fallbackGenerated = false;
278
405
  if (files.length === 0) {
279
406
  await agent.setProgress(job.id, 40);
280
- await agent.sendMessage(job.id, "āš ļø No deliverable files were produced yet. Entering forced-build fallback and generating starter deliverables now.");
281
-
282
- const readme = '# Mission Output\n\nTitle: ' + job.title + '\n\nDescription: ' + job.description + '\n\n## Status\n- Auto-generated fallback deliverable because runtime produced no files.\n- Worker is continuing execution and awaiting optional clarifications.';
283
- const questions = '# Clarifications Needed\n\n1. Exact output file name(s)?\n2. Provide sample input data?\n3. Any strict acceptance criteria/tests?';
284
- const script = "# TODO: Implement mission logic\n# Mission: " + job.title + "\n\nprint('Placeholder fallback script generated. Update with final implementation.')\n";
285
-
286
- fs.writeFileSync(path.join(workDir, 'README.md'), readme);
287
- fs.writeFileSync(path.join(workDir, 'CLARIFICATIONS.md'), questions);
288
- fs.writeFileSync(path.join(workDir, 'solution.py'), script);
289
-
290
- files.push('README.md', 'CLARIFICATIONS.md', 'solution.py');
291
- fallbackGenerated = true;
407
+ await agent.sendMessage(job.id, "āš ļø No usable code files were generated from the mission brief. I need 3 clarifications to continue: 1) expected output file names, 2) sample input CSV row(s), 3) exact clean-up rules (trim spaces only or additional normalization).");
408
+ return;
292
409
  }
293
410
 
294
411
  const repoRes = await agent.getRepo(job.id);
@@ -314,13 +431,7 @@ async function main() {
314
431
  return;
315
432
  }
316
433
 
317
- // Lightweight QA: ensure deliverables are relevant to mission intent
318
- const missionText = (job.title + ' ' + job.description).toLowerCase();
319
- const required = [];
320
- if (missionText.includes('python')) required.push('python');
321
- if (missionText.includes('csv')) required.push('csv');
322
- if (missionText.includes('blank') || missionText.includes('whitespace')) required.push('strip');
323
-
434
+ // Dynamic contract QA: deterministic checks based on inferred contract
324
435
  let combinedText = '';
325
436
  for (const rel of files) {
326
437
  const ext = path.extname(rel).toLowerCase();
@@ -328,10 +439,65 @@ async function main() {
328
439
  try { combinedText += '\n' + fs.readFileSync(path.join(workDir, rel), 'utf8').toLowerCase(); } catch (_) {}
329
440
  }
330
441
  }
331
- const missing = required.filter(k => !combinedText.includes(k));
332
- 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) {
333
499
  await agent.setProgress(job.id, 70);
334
- await agent.sendMessage(job.id, 'āš ļø QA check failed: output appears misaligned with mission (' + missing.join(', ') + ' not found). I will revise now.');
500
+ await agent.sendMessage(job.id, 'āš ļø Contract QA failed: ' + issues.slice(0, 4).join('; ') + '. Revising now.');
335
501
  return;
336
502
  }
337
503
 
@@ -341,9 +507,105 @@ async function main() {
341
507
  return;
342
508
  }
343
509
 
510
+ // Hard gate: after clarification, require at least one non-template file changed
511
+ try {
512
+ const markerPath = path.join(workDir, '..', '_clarification_' + job.id + '.json');
513
+ if (fs.existsSync(markerPath)) {
514
+ const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
515
+ const at = Number(marker?.at || 0);
516
+ const templateSet = new Set(['README.md', 'CLARIFICATIONS.md', 'solution.py', 'FAILSAFE_REPORT.md']);
517
+ const changed = files.some((rel) => {
518
+ if (templateSet.has(rel)) return false;
519
+ const full = path.join(workDir, rel);
520
+ try { return fs.statSync(full).mtimeMs >= at; } catch (_) { return false; }
521
+ });
522
+ if (!changed) {
523
+ await agent.setProgress(job.id, 88);
524
+ await agent.sendMessage(job.id, 'āš ļø Final gate blocked: no non-template file changed after your latest clarification. Continuing implementation.');
525
+ return;
526
+ }
527
+ }
528
+ } catch (_) {}
529
+
530
+ // Second QA worker gate (OpenClaw brain review) before final completion
531
+ await agent.sendMessage(job.id, 'šŸ”Ž Running second QA gate before final completion...');
532
+ const qaPrompt = [
533
+ 'You are QA worker. Validate whether deliverables satisfy mission requirements.',
534
+ 'Return ONLY one token: QA_PASS or QA_FAIL, then one short reason line.',
535
+ '',
536
+ 'MISSION TITLE: ' + job.title,
537
+ 'MISSION DESCRIPTION: ' + job.description,
538
+ 'FILES: ' + files.join(', '),
539
+ '',
540
+ 'CONTENT SAMPLE:',
541
+ combinedText.slice(0, 6000),
542
+ ].join('\n');
543
+
544
+ const qaArg = JSON.stringify(qaPrompt);
545
+ const qaCmds = [
546
+ 'openclaw sessions spawn --task ' + qaArg,
547
+ 'openclaw sessions_spawn --task ' + qaArg,
548
+ ];
549
+ let qaPassed = false;
550
+ let qaOutput = '';
551
+ for (const qcmd of qaCmds) {
552
+ const q = await agent.execute(job.id, qcmd, { timeout: 600000, shell: true });
553
+ qaOutput = q.output || '';
554
+ if (q.exitCode === 0 && qaOutput.toUpperCase().includes('QA_PASS')) { qaPassed = true; break; }
555
+ if (!qaOutput.toLowerCase().includes('unknown command')) break;
556
+ }
557
+
558
+ if (!qaPassed) {
559
+ const why = (qaOutput || 'No QA output').slice(-300);
560
+ const qaStatePath = path.join(workDir, '.qa_fail_count');
561
+ let qaFails = 0;
562
+ try { qaFails = Number(fs.readFileSync(qaStatePath, 'utf8') || '0'); } catch (_) {}
563
+ qaFails += 1;
564
+ fs.writeFileSync(qaStatePath, String(qaFails));
565
+
566
+ await agent.setProgress(job.id, 80);
567
+ if (qaFails >= 5) {
568
+ await agent.sendMessage(job.id, '🚨 Escalation: repeated QA failures (' + qaFails + '). Pausing finalization. Please provide concrete acceptance criteria/examples so I can resolve this precisely.');
569
+ } else if (qaFails >= 3) {
570
+ await agent.sendMessage(job.id, 'āš ļø Second QA gate failed (attempt ' + qaFails + '). I am revising strategy. Reason: ' + why);
571
+ } else {
572
+ await agent.sendMessage(job.id, 'āš ļø Second QA gate failed. I will revise before marking done. Reason: ' + why);
573
+ }
574
+ return;
575
+ }
576
+
577
+ try { fs.unlinkSync(path.join(workDir, '.qa_fail_count')); } catch (_) {}
578
+
579
+ const crypto = require('crypto');
580
+ const fingerprint = crypto.createHash('sha256').update(combinedText).digest('hex');
581
+ const fpPath = path.join(workDir, '.last_delivery_hash');
582
+ if (fs.existsSync(fpPath)) {
583
+ const prev = fs.readFileSync(fpPath, 'utf8').trim();
584
+ if (prev === fingerprint) {
585
+ await agent.setProgress(job.id, 90);
586
+ await agent.sendMessage(job.id, 'āš ļø Anti-repeat gate blocked completion: output matches previous failed attempt. Revising strategy now.');
587
+ return;
588
+ }
589
+ }
590
+ fs.writeFileSync(fpPath, fingerprint);
591
+
592
+ const summary = [
593
+ '# DELIVERY_SUMMARY',
594
+ '',
595
+ 'Mission: ' + job.id,
596
+ 'Task Type: ' + contract.taskType,
597
+ 'Language: ' + contract.language,
598
+ 'Files Uploaded: ' + uploaded,
599
+ 'Acceptance Checks:',
600
+ '- Relevance QA: PASS',
601
+ '- Second QA Gate: PASS',
602
+ '- Non-template change after clarification: PASS',
603
+ ].join('\n');
604
+ try { await agent.uploadRepoFile(job.id, 'DELIVERY_SUMMARY.md', summary, false); } catch (_) {}
605
+
344
606
  await agent.setProgress(job.id, 100);
345
607
  if (repo?.id) {
346
- await agent.sendMessage(job.id, "āœ… Execution complete. Deliverables uploaded: " + uploaded + " files. Repo: https://rentabots.com/repos/" + repo.id);
608
+ await agent.sendMessage(job.id, "āœ… Execution complete. Deliverables uploaded: " + uploaded + " files. Repo: https://rentabots.com/repos/" + repo.id + " (see DELIVERY_SUMMARY.md)");
347
609
  } else {
348
610
  await agent.sendMessage(job.id, "āœ… Execution complete. Deliverables uploaded: " + uploaded + " files to mission repository.");
349
611
  }
@@ -352,16 +614,28 @@ async function main() {
352
614
  const snippet = lastOutput.slice(-500) || 'No output captured';
353
615
  console.error("Worker Error:", snippet);
354
616
 
355
- // Fail-safe: keep user informed + attach diagnostic report to mission repo
617
+ // Fail-safe: keep user informed + attach diagnostic report + fallback draft
356
618
  await agent.setProgress(job.id, 25);
357
- await agent.sendMessage(job.id, "āš ļø I hit an execution issue, but fail-safe mode is active. I’m preparing a diagnostic report and recovery path.");
619
+ await agent.sendMessage(job.id, "āš ļø OpenClaw run failed. Switching to backup adapter mode now so you don't wait idle.");
358
620
 
359
621
  try {
622
+ generateAdapterFallbackFiles(workDir, job, contract);
623
+
360
624
  const repoRes = await agent.getRepo(job.id);
361
625
  if (!repoRes.success || !repoRes.exists) {
362
626
  await agent.createRepo(job.id, 'mission-recovery-' + job.id.slice(0, 8));
363
627
  }
364
628
 
629
+ // Upload fallback draft artifacts first
630
+ const fallbackFiles = fs.readdirSync(workDir).filter((f) => fs.statSync(path.join(workDir, f)).isFile());
631
+ for (const f of fallbackFiles) {
632
+ try {
633
+ const full = path.join(workDir, f);
634
+ const content = fs.readFileSync(full, 'utf8');
635
+ await agent.uploadRepoFile(job.id, f, content, false);
636
+ } catch (_) {}
637
+ }
638
+
365
639
  const report = [
366
640
  '# FAILSAFE_REPORT',
367
641
  '',
package/init_templates.js CHANGED
@@ -40,6 +40,20 @@ async function main() {
40
40
  const agentApiKey = process.env.RENTABOTS_API_KEY || process.env.RENTABOTS_SECRET_KEY;
41
41
  const apiBase = (process.env.RENTABOTS_API_URL || 'https://rentabots.com/api').replace(/\/$/, '');
42
42
 
43
+ const notifyOwner = async (message, data = null) => {
44
+ try {
45
+ if (!agentId || !agentApiKey) return;
46
+ await fetch(\`${apiBase}/agents/\${agentId}/notify\`, {
47
+ method: 'POST',
48
+ headers: {
49
+ 'Content-Type': 'application/json',
50
+ Authorization: \`Bearer \${agentApiKey}\`
51
+ },
52
+ body: JSON.stringify({ message, data })
53
+ });
54
+ } catch (_) {}
55
+ };
56
+
43
57
  const pushLog = async (level, message) => {
44
58
  try {
45
59
  if (!agentId || !agentApiKey) return;
@@ -51,10 +65,26 @@ async function main() {
51
65
  },
52
66
  body: JSON.stringify({ level, message })
53
67
  });
68
+
69
+ if (level === 'ERROR' || String(message).includes('INCIDENT')) {
70
+ await notifyOwner(message);
71
+ }
54
72
  } catch (_) {}
55
73
  };
56
74
 
57
- const 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);
@@ -121,17 +170,30 @@ async function main() {
121
170
 
122
171
  queen.on('message', async (msg) => {
123
172
  if (msg.sender.type === 'agent') return;
124
- if (!queen.workers.has(msg.jobId)) {
125
- const job = queen.activeMissions.get(msg.jobId);
126
- if (!job) {
127
- await queen.sendMessage(msg.jobId, "šŸ¤– Supervisor online. Mission sync in progress.");
128
- await pushLog('WARN', 'Message received for unknown mission ' + msg.jobId);
129
- return;
130
- }
131
-
132
- await queen.sendMessage(msg.jobId, "šŸ¤– Supervisor here. Dispatching a worker to this channel shortly.");
133
- await spawnMissionWorker(job, 'message-trigger');
173
+
174
+ const job = queen.activeMissions.get(msg.jobId);
175
+ if (!job) {
176
+ await queen.sendMessage(msg.jobId, "šŸ¤– Supervisor online. Mission sync in progress.");
177
+ await pushLog('WARN', 'Message received for unknown mission ' + msg.jobId);
178
+ return;
134
179
  }
180
+
181
+ // Human clarification should immediately re-trigger implementation.
182
+ if (queen.workers.has(msg.jobId)) {
183
+ await pushLog('INFO', 'Human update received for ' + msg.jobId + '; restarting worker for immediate implementation.');
184
+ try { queen.workers.get(msg.jobId)?.kill(); } catch (_) {}
185
+ queen.workers.delete(msg.jobId);
186
+ }
187
+
188
+ const baselineFiles = await getRepoFileCount(msg.jobId);
189
+ try {
190
+ const markerPath = path.join(__dirname, 'workspace', '_clarification_' + msg.jobId + '.json');
191
+ fs.mkdirSync(path.dirname(markerPath), { recursive: true });
192
+ fs.writeFileSync(markerPath, JSON.stringify({ at: Date.now(), content: msg.content || '' }));
193
+ } catch (_) {}
194
+
195
+ await queen.sendMessage(msg.jobId, "āœ… Clarification received. Worker is resuming implementation now.");
196
+ await spawnMissionWorker(job, 'human-followup', { watchAfterClarification: true, baselineFiles });
135
197
  });
136
198
 
137
199
  queen.startAutopilot({
@@ -157,6 +219,65 @@ const path = require('path');
157
219
  const jobDataPath = process.argv[2];
158
220
  const job = JSON.parse(fs.readFileSync(jobDataPath, 'utf8'));
159
221
 
222
+ function inferContract(job) {
223
+ const text = ((job.title || '') + ' ' + (job.description || '')).toLowerCase();
224
+ const adapter = text.includes('api') ? 'api' : text.includes('csv') || text.includes('dataset') || text.includes('etl') ? 'data' : text.includes('landing page') || text.includes('frontend') || text.includes('ui') || text.includes('web') ? 'web' : text.includes('docs') || text.includes('documentation') ? 'docs' : 'script';
225
+ const contract = {
226
+ adapter,
227
+ taskType: adapter,
228
+ language: text.includes('python') ? 'python' : text.includes('javascript') || text.includes('node') ? 'javascript' : 'unspecified',
229
+ requires: [],
230
+ deliverables: ['README.md'],
231
+ acceptance: []
232
+ };
233
+
234
+ if (text.includes('csv')) contract.requires.push('csv');
235
+ if (text.includes('blank') || text.includes('whitespace') || text.includes('trim')) contract.requires.push('trim');
236
+ if (contract.language === 'python') contract.deliverables.push('solution.py');
237
+ if (adapter === 'api') contract.deliverables.push('openapi-or-endpoint-docs.md');
238
+ if (adapter === 'web') contract.deliverables.push('index.html');
239
+ if (adapter === 'data') contract.deliverables.push('data_dictionary.md');
240
+ if (adapter === 'docs') contract.deliverables.push('DOCUMENTATION.md');
241
+ contract.acceptance.push('output reflects mission title and description');
242
+ contract.acceptance.push('at least one non-template implementation file');
243
+ return contract;
244
+ }
245
+
246
+ function generateAdapterFallbackFiles(workDir, job, contract) {
247
+ const brief = (job.description || '').trim();
248
+ const readme = [
249
+ '# Mission Recovery Draft',
250
+ '',
251
+ 'This draft was auto-generated because OpenClaw execution failed at runtime.',
252
+ '',
253
+ '## Mission',
254
+ '- Title: ' + (job.title || ''),
255
+ '- Adapter: ' + contract.adapter,
256
+ '- Language: ' + contract.language,
257
+ '',
258
+ '## Brief',
259
+ brief || '(none provided)',
260
+ '',
261
+ '## Next step',
262
+ 'Review this draft and send clarification in mission chat for refinement.'
263
+ ].join('\n');
264
+ fs.writeFileSync(path.join(workDir, 'README.md'), readme);
265
+
266
+ if (contract.adapter === 'api') {
267
+ fs.writeFileSync(path.join(workDir, 'openapi-or-endpoint-docs.md'), '# API Draft\n\n- GET /health\n- POST /process\n\nDescribe request/response schema and validations.');
268
+ } else if (contract.adapter === 'web') {
269
+ fs.writeFileSync(path.join(workDir, 'index.html'), '<!doctype html><html><body><h1>Recovery Draft</h1><p>Mission: ' + (job.title || '') + '</p></body></html>');
270
+ } else if (contract.adapter === 'data') {
271
+ fs.writeFileSync(path.join(workDir, 'data_dictionary.md'), '# Data Dictionary Draft\n\n| Field | Type | Notes |\n|---|---|---|\n| id | string | unique id |');
272
+ } else if (contract.adapter === 'docs') {
273
+ fs.writeFileSync(path.join(workDir, 'DOCUMENTATION.md'), '# Documentation Draft\n\n## Overview\n\n## Setup\n\n## Usage\n');
274
+ } else if (contract.language === 'python') {
275
+ fs.writeFileSync(path.join(workDir, 'solution.py'), 'def main():\n print("recovery draft")\n\nif __name__ == "__main__":\n main()\n');
276
+ } else {
277
+ fs.writeFileSync(path.join(workDir, 'solution.js'), 'console.log("recovery draft");\n');
278
+ }
279
+ }
280
+
160
281
  async function main() {
161
282
  const agent = new Agent({
162
283
  persistState: false,
@@ -168,8 +289,14 @@ async function main() {
168
289
  // Announce arrival in the mission channel
169
290
  await agent.sendMessage(job.id, "šŸ‘· Worker Unit [PID " + process.pid + "] deployed to workspace. Analyzing requirements...");
170
291
 
292
+ const contract = inferContract(job);
293
+ const workDir = path.join(agent.workspaceRoot, job.id);
294
+ fs.mkdirSync(workDir, { recursive: true });
295
+ fs.writeFileSync(path.join(workDir, 'ACCEPTANCE.json'), JSON.stringify(contract, null, 2));
296
+ await agent.sendMessage(job.id, 'šŸ“‹ Contract parsed: ' + contract.taskType + ' / ' + contract.language + '. Deliverables: ' + contract.deliverables.join(', '));
297
+
171
298
  // Construct the prompt for the autonomous brain
172
- const task = \`PROJECT: \${job.title}. BRIEF: \${job.description}. INSTRUCTION: Execute this task in the current directory. Do not leave the workspace. You MUST produce tangible deliverables (code + README + run/test notes) in local files.\`;
299
+ const task = \`PROJECT: \${job.title}. BRIEF: \${job.description}. CONTRACT: \${JSON.stringify(contract)}. INSTRUCTION: Execute this task in the current directory. Do not leave the workspace. You MUST produce tangible deliverables (code + README + run/test notes) in local files.\`;
173
300
 
174
301
  // --- šŸ¦ž EXECUTE VIA OPENCLAW BRAIN ---
175
302
  const taskArg = JSON.stringify(task);
@@ -245,7 +372,7 @@ async function main() {
245
372
 
246
373
  if (overallSuccess) {
247
374
  // Verify actual deliverables exist before claiming success
248
- const workDir = path.join(agent.workspaceRoot, job.id);
375
+ // reuse workDir initialized earlier
249
376
  const files = [];
250
377
  const walk = (dir, rel = '') => {
251
378
  if (!fs.existsSync(dir)) return;
@@ -259,21 +386,11 @@ async function main() {
259
386
  };
260
387
  walk(workDir);
261
388
 
262
- let fallbackGenerated = false;
389
+ const fallbackGenerated = false;
263
390
  if (files.length === 0) {
264
391
  await agent.setProgress(job.id, 40);
265
- await agent.sendMessage(job.id, "āš ļø No deliverable files were produced yet. Entering forced-build fallback and generating starter deliverables now.");
266
-
267
- const readme = '# Mission Output\n\nTitle: ' + job.title + '\n\nDescription: ' + job.description + '\n\n## Status\n- Auto-generated fallback deliverable because runtime produced no files.\n- Worker is continuing execution and awaiting optional clarifications.';
268
- const questions = '# Clarifications Needed\n\n1. Exact output file name(s)?\n2. Provide sample input data?\n3. Any strict acceptance criteria/tests?';
269
- const script = "# TODO: Implement mission logic\n# Mission: " + job.title + "\n\nprint('Placeholder fallback script generated. Update with final implementation.')\n";
270
-
271
- fs.writeFileSync(path.join(workDir, 'README.md'), readme);
272
- fs.writeFileSync(path.join(workDir, 'CLARIFICATIONS.md'), questions);
273
- fs.writeFileSync(path.join(workDir, 'solution.py'), script);
274
-
275
- files.push('README.md', 'CLARIFICATIONS.md', 'solution.py');
276
- fallbackGenerated = true;
392
+ await agent.sendMessage(job.id, "āš ļø No usable code files were generated from the mission brief. I need 3 clarifications to continue: 1) expected output file names, 2) sample input CSV row(s), 3) exact clean-up rules (trim spaces only or additional normalization).");
393
+ return;
277
394
  }
278
395
 
279
396
  const repoRes = await agent.getRepo(job.id);
@@ -299,13 +416,7 @@ async function main() {
299
416
  return;
300
417
  }
301
418
 
302
- // Lightweight QA: ensure deliverables are relevant to mission intent
303
- const missionText = (job.title + ' ' + job.description).toLowerCase();
304
- const required = [];
305
- if (missionText.includes('python')) required.push('python');
306
- if (missionText.includes('csv')) required.push('csv');
307
- if (missionText.includes('blank') || missionText.includes('whitespace')) required.push('strip');
308
-
419
+ // Dynamic contract QA: deterministic checks based on inferred contract
309
420
  let combinedText = '';
310
421
  for (const rel of files) {
311
422
  const ext = path.extname(rel).toLowerCase();
@@ -313,10 +424,65 @@ async function main() {
313
424
  try { combinedText += '\n' + fs.readFileSync(path.join(workDir, rel), 'utf8').toLowerCase(); } catch (_) {}
314
425
  }
315
426
  }
316
- const missing = required.filter(k => !combinedText.includes(k));
317
- 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) {
318
484
  await agent.setProgress(job.id, 70);
319
- await agent.sendMessage(job.id, 'āš ļø QA check failed: output appears misaligned with mission (' + missing.join(', ') + ' not found). I will revise now.');
485
+ await agent.sendMessage(job.id, 'āš ļø Contract QA failed: ' + issues.slice(0, 4).join('; ') + '. Revising now.');
320
486
  return;
321
487
  }
322
488
 
@@ -326,9 +492,105 @@ async function main() {
326
492
  return;
327
493
  }
328
494
 
495
+ // Hard gate: after clarification, require at least one non-template file changed
496
+ try {
497
+ const markerPath = path.join(workDir, '..', '_clarification_' + job.id + '.json');
498
+ if (fs.existsSync(markerPath)) {
499
+ const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
500
+ const at = Number(marker?.at || 0);
501
+ const templateSet = new Set(['README.md', 'CLARIFICATIONS.md', 'solution.py', 'FAILSAFE_REPORT.md']);
502
+ const changed = files.some((rel) => {
503
+ if (templateSet.has(rel)) return false;
504
+ const full = path.join(workDir, rel);
505
+ try { return fs.statSync(full).mtimeMs >= at; } catch (_) { return false; }
506
+ });
507
+ if (!changed) {
508
+ await agent.setProgress(job.id, 88);
509
+ await agent.sendMessage(job.id, 'āš ļø Final gate blocked: no non-template file changed after your latest clarification. Continuing implementation.');
510
+ return;
511
+ }
512
+ }
513
+ } catch (_) {}
514
+
515
+ // Second QA worker gate (OpenClaw brain review) before final completion
516
+ await agent.sendMessage(job.id, 'šŸ”Ž Running second QA gate before final completion...');
517
+ const qaPrompt = [
518
+ 'You are QA worker. Validate whether deliverables satisfy mission requirements.',
519
+ 'Return ONLY one token: QA_PASS or QA_FAIL, then one short reason line.',
520
+ '',
521
+ 'MISSION TITLE: ' + job.title,
522
+ 'MISSION DESCRIPTION: ' + job.description,
523
+ 'FILES: ' + files.join(', '),
524
+ '',
525
+ 'CONTENT SAMPLE:',
526
+ combinedText.slice(0, 6000),
527
+ ].join('\n');
528
+
529
+ const qaArg = JSON.stringify(qaPrompt);
530
+ const qaCmds = [
531
+ 'openclaw sessions spawn --task ' + qaArg,
532
+ 'openclaw sessions_spawn --task ' + qaArg,
533
+ ];
534
+ let qaPassed = false;
535
+ let qaOutput = '';
536
+ for (const qcmd of qaCmds) {
537
+ const q = await agent.execute(job.id, qcmd, { timeout: 600000, shell: true });
538
+ qaOutput = q.output || '';
539
+ if (q.exitCode === 0 && qaOutput.toUpperCase().includes('QA_PASS')) { qaPassed = true; break; }
540
+ if (!qaOutput.toLowerCase().includes('unknown command')) break;
541
+ }
542
+
543
+ if (!qaPassed) {
544
+ const why = (qaOutput || 'No QA output').slice(-300);
545
+ const qaStatePath = path.join(workDir, '.qa_fail_count');
546
+ let qaFails = 0;
547
+ try { qaFails = Number(fs.readFileSync(qaStatePath, 'utf8') || '0'); } catch (_) {}
548
+ qaFails += 1;
549
+ fs.writeFileSync(qaStatePath, String(qaFails));
550
+
551
+ await agent.setProgress(job.id, 80);
552
+ if (qaFails >= 5) {
553
+ await agent.sendMessage(job.id, '🚨 Escalation: repeated QA failures (' + qaFails + '). Pausing finalization. Please provide concrete acceptance criteria/examples so I can resolve this precisely.');
554
+ } else if (qaFails >= 3) {
555
+ await agent.sendMessage(job.id, 'āš ļø Second QA gate failed (attempt ' + qaFails + '). I am revising strategy. Reason: ' + why);
556
+ } else {
557
+ await agent.sendMessage(job.id, 'āš ļø Second QA gate failed. I will revise before marking done. Reason: ' + why);
558
+ }
559
+ return;
560
+ }
561
+
562
+ try { fs.unlinkSync(path.join(workDir, '.qa_fail_count')); } catch (_) {}
563
+
564
+ const crypto = require('crypto');
565
+ const fingerprint = crypto.createHash('sha256').update(combinedText).digest('hex');
566
+ const fpPath = path.join(workDir, '.last_delivery_hash');
567
+ if (fs.existsSync(fpPath)) {
568
+ const prev = fs.readFileSync(fpPath, 'utf8').trim();
569
+ if (prev === fingerprint) {
570
+ await agent.setProgress(job.id, 90);
571
+ await agent.sendMessage(job.id, 'āš ļø Anti-repeat gate blocked completion: output matches previous failed attempt. Revising strategy now.');
572
+ return;
573
+ }
574
+ }
575
+ fs.writeFileSync(fpPath, fingerprint);
576
+
577
+ const summary = [
578
+ '# DELIVERY_SUMMARY',
579
+ '',
580
+ 'Mission: ' + job.id,
581
+ 'Task Type: ' + contract.taskType,
582
+ 'Language: ' + contract.language,
583
+ 'Files Uploaded: ' + uploaded,
584
+ 'Acceptance Checks:',
585
+ '- Relevance QA: PASS',
586
+ '- Second QA Gate: PASS',
587
+ '- Non-template change after clarification: PASS',
588
+ ].join('\n');
589
+ try { await agent.uploadRepoFile(job.id, 'DELIVERY_SUMMARY.md', summary, false); } catch (_) {}
590
+
329
591
  await agent.setProgress(job.id, 100);
330
592
  if (repo?.id) {
331
- await agent.sendMessage(job.id, "āœ… Execution complete. Deliverables uploaded: " + uploaded + " files. Repo: https://rentabots.com/repos/" + repo.id);
593
+ await agent.sendMessage(job.id, "āœ… Execution complete. Deliverables uploaded: " + uploaded + " files. Repo: https://rentabots.com/repos/" + repo.id + " (see DELIVERY_SUMMARY.md)");
332
594
  } else {
333
595
  await agent.sendMessage(job.id, "āœ… Execution complete. Deliverables uploaded: " + uploaded + " files to mission repository.");
334
596
  }
@@ -337,16 +599,28 @@ async function main() {
337
599
  const snippet = lastOutput.slice(-500) || 'No output captured';
338
600
  console.error("Worker Error:", snippet);
339
601
 
340
- // Fail-safe: keep user informed + attach diagnostic report to mission repo
602
+ // Fail-safe: keep user informed + attach diagnostic report + fallback draft
341
603
  await agent.setProgress(job.id, 25);
342
- await agent.sendMessage(job.id, "āš ļø I hit an execution issue, but fail-safe mode is active. I’m preparing a diagnostic report and recovery path.");
604
+ await agent.sendMessage(job.id, "āš ļø OpenClaw run failed. Switching to backup adapter mode now so you don't wait idle.");
343
605
 
344
606
  try {
607
+ generateAdapterFallbackFiles(workDir, job, contract);
608
+
345
609
  const repoRes = await agent.getRepo(job.id);
346
610
  if (!repoRes.success || !repoRes.exists) {
347
611
  await agent.createRepo(job.id, 'mission-recovery-' + job.id.slice(0, 8));
348
612
  }
349
613
 
614
+ // Upload fallback draft artifacts first
615
+ const fallbackFiles = fs.readdirSync(workDir).filter((f) => fs.statSync(path.join(workDir, f)).isFile());
616
+ for (const f of fallbackFiles) {
617
+ try {
618
+ const full = path.join(workDir, f);
619
+ const content = fs.readFileSync(full, 'utf8');
620
+ await agent.uploadRepoFile(job.id, f, content, false);
621
+ } catch (_) {}
622
+ }
623
+
350
624
  const report = [
351
625
  '# FAILSAFE_REPORT',
352
626
  '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rentabots-sdk",
3
- "version": "1.7.17",
3
+ "version": "1.7.26",
4
4
  "description": "Official SDK for RentaBots AI Agent Marketplace",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",