rentabots-sdk 1.7.18 → 1.7.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js 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.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);
@@ -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,10 +439,65 @@ async function main() {
335
439
  try { combinedText += '\n' + fs.readFileSync(path.join(workDir, rel), 'utf8').toLowerCase(); } catch (_) {}
336
440
  }
337
441
  }
338
- 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.');
500
+ await agent.sendMessage(job.id, 'āš ļø Contract QA failed: ' + issues.slice(0, 4).join('; ') + '. Revising now.');
342
501
  return;
343
502
  }
344
503
 
@@ -348,9 +507,105 @@ async function main() {
348
507
  return;
349
508
  }
350
509
 
510
+ // Hard gate: after clarification, require at least one non-template file changed
511
+ try {
512
+ const markerPath = path.join(workDir, '..', '_clarification_' + job.id + '.json');
513
+ if (fs.existsSync(markerPath)) {
514
+ const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
515
+ const at = Number(marker?.at || 0);
516
+ const templateSet = new Set(['README.md', 'CLARIFICATIONS.md', 'solution.py', 'FAILSAFE_REPORT.md']);
517
+ const changed = files.some((rel) => {
518
+ if (templateSet.has(rel)) return false;
519
+ const full = path.join(workDir, rel);
520
+ try { return fs.statSync(full).mtimeMs >= at; } catch (_) { return false; }
521
+ });
522
+ if (!changed) {
523
+ await agent.setProgress(job.id, 88);
524
+ await agent.sendMessage(job.id, 'āš ļø Final gate blocked: no non-template file changed after your latest clarification. Continuing implementation.');
525
+ return;
526
+ }
527
+ }
528
+ } catch (_) {}
529
+
530
+ // Second QA worker gate (OpenClaw brain review) before final completion
531
+ await agent.sendMessage(job.id, 'šŸ”Ž Running second QA gate before final completion...');
532
+ const qaPrompt = [
533
+ 'You are QA worker. Validate whether deliverables satisfy mission requirements.',
534
+ 'Return ONLY one token: QA_PASS or QA_FAIL, then one short reason line.',
535
+ '',
536
+ 'MISSION TITLE: ' + job.title,
537
+ 'MISSION DESCRIPTION: ' + job.description,
538
+ 'FILES: ' + files.join(', '),
539
+ '',
540
+ 'CONTENT SAMPLE:',
541
+ combinedText.slice(0, 6000),
542
+ ].join('\n');
543
+
544
+ const qaArg = JSON.stringify(qaPrompt);
545
+ const qaCmds = [
546
+ 'openclaw sessions spawn --task ' + qaArg,
547
+ 'openclaw sessions_spawn --task ' + qaArg,
548
+ ];
549
+ let qaPassed = false;
550
+ let qaOutput = '';
551
+ for (const qcmd of qaCmds) {
552
+ const q = await agent.execute(job.id, qcmd, { timeout: 600000, shell: true });
553
+ qaOutput = q.output || '';
554
+ if (q.exitCode === 0 && qaOutput.toUpperCase().includes('QA_PASS')) { qaPassed = true; break; }
555
+ if (!qaOutput.toLowerCase().includes('unknown command')) break;
556
+ }
557
+
558
+ if (!qaPassed) {
559
+ const why = (qaOutput || 'No QA output').slice(-300);
560
+ const qaStatePath = path.join(workDir, '.qa_fail_count');
561
+ let qaFails = 0;
562
+ try { qaFails = Number(fs.readFileSync(qaStatePath, 'utf8') || '0'); } catch (_) {}
563
+ qaFails += 1;
564
+ fs.writeFileSync(qaStatePath, String(qaFails));
565
+
566
+ await agent.setProgress(job.id, 80);
567
+ if (qaFails >= 5) {
568
+ await agent.sendMessage(job.id, '🚨 Escalation: repeated QA failures (' + qaFails + '). Pausing finalization. Please provide concrete acceptance criteria/examples so I can resolve this precisely.');
569
+ } else if (qaFails >= 3) {
570
+ await agent.sendMessage(job.id, 'āš ļø Second QA gate failed (attempt ' + qaFails + '). I am revising strategy. Reason: ' + why);
571
+ } else {
572
+ await agent.sendMessage(job.id, 'āš ļø Second QA gate failed. I will revise before marking done. Reason: ' + why);
573
+ }
574
+ return;
575
+ }
576
+
577
+ try { fs.unlinkSync(path.join(workDir, '.qa_fail_count')); } catch (_) {}
578
+
579
+ const crypto = require('crypto');
580
+ const fingerprint = crypto.createHash('sha256').update(combinedText).digest('hex');
581
+ const fpPath = path.join(workDir, '.last_delivery_hash');
582
+ if (fs.existsSync(fpPath)) {
583
+ const prev = fs.readFileSync(fpPath, 'utf8').trim();
584
+ if (prev === fingerprint) {
585
+ await agent.setProgress(job.id, 90);
586
+ await agent.sendMessage(job.id, 'āš ļø Anti-repeat gate blocked completion: output matches previous failed attempt. Revising strategy now.');
587
+ return;
588
+ }
589
+ }
590
+ fs.writeFileSync(fpPath, fingerprint);
591
+
592
+ const summary = [
593
+ '# DELIVERY_SUMMARY',
594
+ '',
595
+ 'Mission: ' + job.id,
596
+ 'Task Type: ' + contract.taskType,
597
+ 'Language: ' + contract.language,
598
+ 'Files Uploaded: ' + uploaded,
599
+ 'Acceptance Checks:',
600
+ '- Relevance QA: PASS',
601
+ '- Second QA Gate: PASS',
602
+ '- Non-template change after clarification: PASS',
603
+ ].join('\n');
604
+ try { await agent.uploadRepoFile(job.id, 'DELIVERY_SUMMARY.md', summary, false); } catch (_) {}
605
+
351
606
  await agent.setProgress(job.id, 100);
352
607
  if (repo?.id) {
353
- await agent.sendMessage(job.id, "āœ… Execution complete. Deliverables uploaded: " + uploaded + " files. Repo: https://rentabots.com/repos/" + repo.id);
608
+ await agent.sendMessage(job.id, "āœ… Execution complete. Deliverables uploaded: " + uploaded + " files. Repo: https://rentabots.com/repos/" + repo.id + " (see DELIVERY_SUMMARY.md)");
354
609
  } else {
355
610
  await agent.sendMessage(job.id, "āœ… Execution complete. Deliverables uploaded: " + uploaded + " files to mission repository.");
356
611
  }
@@ -359,16 +614,28 @@ async function main() {
359
614
  const snippet = lastOutput.slice(-500) || 'No output captured';
360
615
  console.error("Worker Error:", snippet);
361
616
 
362
- // Fail-safe: keep user informed + attach diagnostic report to mission repo
617
+ // Fail-safe: keep user informed + attach diagnostic report + fallback draft
363
618
  await agent.setProgress(job.id, 25);
364
- await agent.sendMessage(job.id, "āš ļø 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.");
365
620
 
366
621
  try {
622
+ generateAdapterFallbackFiles(workDir, job, contract);
623
+
367
624
  const repoRes = await agent.getRepo(job.id);
368
625
  if (!repoRes.success || !repoRes.exists) {
369
626
  await agent.createRepo(job.id, 'mission-recovery-' + job.id.slice(0, 8));
370
627
  }
371
628
 
629
+ // Upload fallback draft artifacts first
630
+ const fallbackFiles = fs.readdirSync(workDir).filter((f) => fs.statSync(path.join(workDir, f)).isFile());
631
+ for (const f of fallbackFiles) {
632
+ try {
633
+ const full = path.join(workDir, f);
634
+ const content = fs.readFileSync(full, 'utf8');
635
+ await agent.uploadRepoFile(job.id, f, content, false);
636
+ } catch (_) {}
637
+ }
638
+
372
639
  const report = [
373
640
  '# FAILSAFE_REPORT',
374
641
  '',
package/init_templates.js CHANGED
@@ -40,6 +40,20 @@ async function main() {
40
40
  const agentApiKey = process.env.RENTABOTS_API_KEY || process.env.RENTABOTS_SECRET_KEY;
41
41
  const apiBase = (process.env.RENTABOTS_API_URL || 'https://rentabots.com/api').replace(/\/$/, '');
42
42
 
43
+ const notifyOwner = async (message, data = null) => {
44
+ try {
45
+ if (!agentId || !agentApiKey) return;
46
+ await fetch(\`${apiBase}/agents/\${agentId}/notify\`, {
47
+ method: 'POST',
48
+ headers: {
49
+ 'Content-Type': 'application/json',
50
+ Authorization: \`Bearer \${agentApiKey}\`
51
+ },
52
+ body: JSON.stringify({ message, data })
53
+ });
54
+ } catch (_) {}
55
+ };
56
+
43
57
  const pushLog = async (level, message) => {
44
58
  try {
45
59
  if (!agentId || !agentApiKey) return;
@@ -51,10 +65,26 @@ async function main() {
51
65
  },
52
66
  body: JSON.stringify({ level, message })
53
67
  });
68
+
69
+ if (level === 'ERROR' || String(message).includes('INCIDENT')) {
70
+ await notifyOwner(message);
71
+ }
54
72
  } catch (_) {}
55
73
  };
56
74
 
57
- const 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,10 +424,65 @@ async function main() {
319
424
  try { combinedText += '\n' + fs.readFileSync(path.join(workDir, rel), 'utf8').toLowerCase(); } catch (_) {}
320
425
  }
321
426
  }
322
- 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.');
485
+ await agent.sendMessage(job.id, 'āš ļø Contract QA failed: ' + issues.slice(0, 4).join('; ') + '. Revising now.');
326
486
  return;
327
487
  }
328
488
 
@@ -332,9 +492,105 @@ async function main() {
332
492
  return;
333
493
  }
334
494
 
495
+ // Hard gate: after clarification, require at least one non-template file changed
496
+ try {
497
+ const markerPath = path.join(workDir, '..', '_clarification_' + job.id + '.json');
498
+ if (fs.existsSync(markerPath)) {
499
+ const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
500
+ const at = Number(marker?.at || 0);
501
+ const templateSet = new Set(['README.md', 'CLARIFICATIONS.md', 'solution.py', 'FAILSAFE_REPORT.md']);
502
+ const changed = files.some((rel) => {
503
+ if (templateSet.has(rel)) return false;
504
+ const full = path.join(workDir, rel);
505
+ try { return fs.statSync(full).mtimeMs >= at; } catch (_) { return false; }
506
+ });
507
+ if (!changed) {
508
+ await agent.setProgress(job.id, 88);
509
+ await agent.sendMessage(job.id, 'āš ļø Final gate blocked: no non-template file changed after your latest clarification. Continuing implementation.');
510
+ return;
511
+ }
512
+ }
513
+ } catch (_) {}
514
+
515
+ // Second QA worker gate (OpenClaw brain review) before final completion
516
+ await agent.sendMessage(job.id, 'šŸ”Ž Running second QA gate before final completion...');
517
+ const qaPrompt = [
518
+ 'You are QA worker. Validate whether deliverables satisfy mission requirements.',
519
+ 'Return ONLY one token: QA_PASS or QA_FAIL, then one short reason line.',
520
+ '',
521
+ 'MISSION TITLE: ' + job.title,
522
+ 'MISSION DESCRIPTION: ' + job.description,
523
+ 'FILES: ' + files.join(', '),
524
+ '',
525
+ 'CONTENT SAMPLE:',
526
+ combinedText.slice(0, 6000),
527
+ ].join('\n');
528
+
529
+ const qaArg = JSON.stringify(qaPrompt);
530
+ const qaCmds = [
531
+ 'openclaw sessions spawn --task ' + qaArg,
532
+ 'openclaw sessions_spawn --task ' + qaArg,
533
+ ];
534
+ let qaPassed = false;
535
+ let qaOutput = '';
536
+ for (const qcmd of qaCmds) {
537
+ const q = await agent.execute(job.id, qcmd, { timeout: 600000, shell: true });
538
+ qaOutput = q.output || '';
539
+ if (q.exitCode === 0 && qaOutput.toUpperCase().includes('QA_PASS')) { qaPassed = true; break; }
540
+ if (!qaOutput.toLowerCase().includes('unknown command')) break;
541
+ }
542
+
543
+ if (!qaPassed) {
544
+ const why = (qaOutput || 'No QA output').slice(-300);
545
+ const qaStatePath = path.join(workDir, '.qa_fail_count');
546
+ let qaFails = 0;
547
+ try { qaFails = Number(fs.readFileSync(qaStatePath, 'utf8') || '0'); } catch (_) {}
548
+ qaFails += 1;
549
+ fs.writeFileSync(qaStatePath, String(qaFails));
550
+
551
+ await agent.setProgress(job.id, 80);
552
+ if (qaFails >= 5) {
553
+ await agent.sendMessage(job.id, '🚨 Escalation: repeated QA failures (' + qaFails + '). Pausing finalization. Please provide concrete acceptance criteria/examples so I can resolve this precisely.');
554
+ } else if (qaFails >= 3) {
555
+ await agent.sendMessage(job.id, 'āš ļø Second QA gate failed (attempt ' + qaFails + '). I am revising strategy. Reason: ' + why);
556
+ } else {
557
+ await agent.sendMessage(job.id, 'āš ļø Second QA gate failed. I will revise before marking done. Reason: ' + why);
558
+ }
559
+ return;
560
+ }
561
+
562
+ try { fs.unlinkSync(path.join(workDir, '.qa_fail_count')); } catch (_) {}
563
+
564
+ const crypto = require('crypto');
565
+ const fingerprint = crypto.createHash('sha256').update(combinedText).digest('hex');
566
+ const fpPath = path.join(workDir, '.last_delivery_hash');
567
+ if (fs.existsSync(fpPath)) {
568
+ const prev = fs.readFileSync(fpPath, 'utf8').trim();
569
+ if (prev === fingerprint) {
570
+ await agent.setProgress(job.id, 90);
571
+ await agent.sendMessage(job.id, 'āš ļø Anti-repeat gate blocked completion: output matches previous failed attempt. Revising strategy now.');
572
+ return;
573
+ }
574
+ }
575
+ fs.writeFileSync(fpPath, fingerprint);
576
+
577
+ const summary = [
578
+ '# DELIVERY_SUMMARY',
579
+ '',
580
+ 'Mission: ' + job.id,
581
+ 'Task Type: ' + contract.taskType,
582
+ 'Language: ' + contract.language,
583
+ 'Files Uploaded: ' + uploaded,
584
+ 'Acceptance Checks:',
585
+ '- Relevance QA: PASS',
586
+ '- Second QA Gate: PASS',
587
+ '- Non-template change after clarification: PASS',
588
+ ].join('\n');
589
+ try { await agent.uploadRepoFile(job.id, 'DELIVERY_SUMMARY.md', summary, false); } catch (_) {}
590
+
335
591
  await agent.setProgress(job.id, 100);
336
592
  if (repo?.id) {
337
- await agent.sendMessage(job.id, "āœ… Execution complete. Deliverables uploaded: " + uploaded + " files. Repo: https://rentabots.com/repos/" + repo.id);
593
+ await agent.sendMessage(job.id, "āœ… Execution complete. Deliverables uploaded: " + uploaded + " files. Repo: https://rentabots.com/repos/" + repo.id + " (see DELIVERY_SUMMARY.md)");
338
594
  } else {
339
595
  await agent.sendMessage(job.id, "āœ… Execution complete. Deliverables uploaded: " + uploaded + " files to mission repository.");
340
596
  }
@@ -343,16 +599,28 @@ async function main() {
343
599
  const snippet = lastOutput.slice(-500) || 'No output captured';
344
600
  console.error("Worker Error:", snippet);
345
601
 
346
- // Fail-safe: keep user informed + attach diagnostic report to mission repo
602
+ // Fail-safe: keep user informed + attach diagnostic report + fallback draft
347
603
  await agent.setProgress(job.id, 25);
348
- await agent.sendMessage(job.id, "āš ļø 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.");
349
605
 
350
606
  try {
607
+ generateAdapterFallbackFiles(workDir, job, contract);
608
+
351
609
  const repoRes = await agent.getRepo(job.id);
352
610
  if (!repoRes.success || !repoRes.exists) {
353
611
  await agent.createRepo(job.id, 'mission-recovery-' + job.id.slice(0, 8));
354
612
  }
355
613
 
614
+ // Upload fallback draft artifacts first
615
+ const fallbackFiles = fs.readdirSync(workDir).filter((f) => fs.statSync(path.join(workDir, f)).isFile());
616
+ for (const f of fallbackFiles) {
617
+ try {
618
+ const full = path.join(workDir, f);
619
+ const content = fs.readFileSync(full, 'utf8');
620
+ await agent.uploadRepoFile(job.id, f, content, false);
621
+ } catch (_) {}
622
+ }
623
+
356
624
  const report = [
357
625
  '# FAILSAFE_REPORT',
358
626
  '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rentabots-sdk",
3
- "version": "1.7.18",
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",