halo-agent 1.3.2 → 1.3.3
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/filler.js +25 -5
- package/orchestrator.js +23 -6
- package/package.json +1 -1
- package/scanPage.js +15 -2
package/filler.js
CHANGED
|
@@ -637,6 +637,8 @@ async function fillFields(page, aep, options = {}) {
|
|
|
637
637
|
|
|
638
638
|
// ── Pass 3: fill each field ───────────────────────────────────────────────
|
|
639
639
|
for (const { field, value, source } of resolved) {
|
|
640
|
+
const labelShort = (field.label || field.selector || '?').slice(0, 50);
|
|
641
|
+
|
|
640
642
|
// Skip if already answered on a previous page
|
|
641
643
|
const trackKey = field.label || field.selector;
|
|
642
644
|
if (ctx && trackKey && ctx.answeredFields.has(trackKey)) {
|
|
@@ -660,8 +662,15 @@ async function fillFields(page, aep, options = {}) {
|
|
|
660
662
|
continue;
|
|
661
663
|
}
|
|
662
664
|
|
|
663
|
-
// No value for a profile field — missing from profile, skip
|
|
664
|
-
|
|
665
|
+
// No value for a profile field — missing from profile, skip.
|
|
666
|
+
// Log this loudly: it's almost always the actual cause of "the agent
|
|
667
|
+
// didn't fill anything." profile_fill missing a value the user definitely
|
|
668
|
+
// entered means the AEP builder isn't sending it from strategic_profile.
|
|
669
|
+
if (!value) {
|
|
670
|
+
console.warn(`[filler] no-value ${source}: "${labelShort}" (category=${field.category}) — value missing from AEP`);
|
|
671
|
+
skipped++;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
665
674
|
|
|
666
675
|
// Locate the element — use the scanner's selector first, then fall back to semantic finder
|
|
667
676
|
let locator = null;
|
|
@@ -690,7 +699,10 @@ async function fillFields(page, aep, options = {}) {
|
|
|
690
699
|
locator = await semanticFindField(page, { field_id: field.id || field.name, label: field.label }).catch(() => null);
|
|
691
700
|
}
|
|
692
701
|
|
|
693
|
-
if (!locator) {
|
|
702
|
+
if (!locator) {
|
|
703
|
+
console.warn(`[filler] no-locator: "${labelShort}" (selector="${(field.selector || '').slice(0,60)}") — element not visible / selector dead`);
|
|
704
|
+
skipped++; continue;
|
|
705
|
+
}
|
|
694
706
|
|
|
695
707
|
// Special case: cover letter — type character by character
|
|
696
708
|
if (field.category === 'cover_letter' && field.tag === 'textarea') {
|
|
@@ -739,11 +751,19 @@ async function fillFields(page, aep, options = {}) {
|
|
|
739
751
|
try {
|
|
740
752
|
const ok = await fillLocator(page, locator, value, field.label);
|
|
741
753
|
if (ok) {
|
|
754
|
+
const valShort = String(value).slice(0, 40).replace(/\n/g, ' ');
|
|
755
|
+
console.log(`[filler] filled (${source}): "${labelShort}" = "${valShort}${String(value).length > 40 ? '...' : ''}"`);
|
|
742
756
|
filled++;
|
|
743
757
|
if (ctx && trackKey) ctx.answeredFields.set(trackKey, { value, pageIndex: ctx.currentPageIndex, source });
|
|
744
758
|
await delay();
|
|
745
|
-
} else {
|
|
746
|
-
|
|
759
|
+
} else {
|
|
760
|
+
console.warn(`[filler] fill-failed: "${labelShort}" — fillLocator returned false`);
|
|
761
|
+
failed++;
|
|
762
|
+
}
|
|
763
|
+
} catch (e) {
|
|
764
|
+
console.warn(`[filler] fill-threw: "${labelShort}" — ${e.message}`);
|
|
765
|
+
failed++;
|
|
766
|
+
}
|
|
747
767
|
}
|
|
748
768
|
|
|
749
769
|
// ── Legacy fallback: also run old profile map + semantic matcher ──────────
|
package/orchestrator.js
CHANGED
|
@@ -402,30 +402,47 @@ async function runJob(queueItem, chromeConn, config, reportStatus) {
|
|
|
402
402
|
console.warn(`[orchestrator] verify-submit unavailable: ${e.message}`);
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
|
|
405
|
+
// Three-way verdict:
|
|
406
|
+
// submitted: true → real success, mark DONE
|
|
407
|
+
// submitted: false → real failure, NEEDS_ATTENTION with the error
|
|
408
|
+
// submitted: null → could not verify (Firecrawl down/missing).
|
|
409
|
+
// Bounce to REVIEWING so the user eyeballs the
|
|
410
|
+
// screenshot — never silently mark DONE on
|
|
411
|
+
// unverified submits (that was the Chalk bug).
|
|
412
|
+
if (verdict.submitted === false) {
|
|
406
413
|
const reason = verdict.error_message || 'Submission did not confirm — form may still have errors';
|
|
407
414
|
console.warn(`[orchestrator] Submission NOT verified. Reason: ${reason}`);
|
|
408
415
|
await reportStatus('NEEDS_ATTENTION', {
|
|
409
416
|
review_screenshot_r2_key: confirmKey || null,
|
|
410
|
-
needs_attention_reason: `Submit clicked but
|
|
417
|
+
needs_attention_reason: `Submit clicked but Ashby/ATS rejected it: ${reason}`,
|
|
411
418
|
intervention_type: 'submit_failed',
|
|
412
419
|
step: 'VERIFY',
|
|
413
420
|
step_detail: reason.slice(0, 200),
|
|
414
421
|
fields_filled: cumulativeFilled,
|
|
415
422
|
});
|
|
416
|
-
// Do NOT clearCheckpoint — user may dismiss + re-queue, and a stale
|
|
417
|
-
// checkpoint would resume into the same failed state. The dismiss flow
|
|
418
|
-
// clears it (DELETE /apply-queue/:id sets form_checkpoint_json = NULL).
|
|
419
423
|
throw new Error(`Submission failed verification: ${reason}`);
|
|
420
424
|
}
|
|
421
425
|
|
|
426
|
+
if (verdict.submitted === null) {
|
|
427
|
+
console.warn(`[orchestrator] Could not verify submission (source: ${verdict.source}). Sending to REVIEWING for your eyeball.`);
|
|
428
|
+
await reportStatus('REVIEWING', {
|
|
429
|
+
review_screenshot_r2_key: confirmKey || null,
|
|
430
|
+
step: 'REVIEWING',
|
|
431
|
+
step_detail: 'Could not auto-verify — please confirm the submit',
|
|
432
|
+
fields_filled: cumulativeFilled,
|
|
433
|
+
});
|
|
434
|
+
// Stop here; user clicks Submit on dashboard → /apply-queue/submit/:id
|
|
435
|
+
// will flip to DONE. Don't return — let the function return naturally.
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
422
439
|
await reportStatus('DONE', {
|
|
423
440
|
confirmation_screenshot_r2_key: confirmKey || null,
|
|
424
441
|
fields_filled: cumulativeFilled,
|
|
425
442
|
});
|
|
426
443
|
await clearCheckpoint(config, queueId);
|
|
427
444
|
|
|
428
|
-
console.log(`[orchestrator] Done (verified): ${queueItem.company} - ${queueItem.title}
|
|
445
|
+
console.log(`[orchestrator] Done (verified): ${queueItem.company} - ${queueItem.title} · firecrawl-verified`);
|
|
429
446
|
|
|
430
447
|
// Post fill session data to backend for learning loop
|
|
431
448
|
await postFillSession(config, {
|
package/package.json
CHANGED
package/scanPage.js
CHANGED
|
@@ -339,10 +339,17 @@ async function scanAshby(page) {
|
|
|
339
339
|
if (f.name && f.name.startsWith('_systemfield_')) {
|
|
340
340
|
const sfField = f.name.replace('_systemfield_', '');
|
|
341
341
|
const profileMap = { name: 'full_name', email: 'email', phone: 'phone', linkedin: 'linkedin', website: 'portfolio', resume: null };
|
|
342
|
-
if (sfField === 'resume'
|
|
342
|
+
if (sfField === 'resume') return { ...f, category: 'file:resume' };
|
|
343
343
|
if (profileMap[sfField] !== undefined) return { ...f, category: 'profile:' + profileMap[sfField] };
|
|
344
344
|
}
|
|
345
|
-
|
|
345
|
+
// File inputs: distinguish resume vs cover-letter vs other by the visible
|
|
346
|
+
// label. Without this, Ashby's separate cover-letter file input was being
|
|
347
|
+
// shadowed by 'file:resume' and uploadResume() only handles one resume.
|
|
348
|
+
if (f.inputType === 'file') {
|
|
349
|
+
const lbl = (f.label || '').toLowerCase();
|
|
350
|
+
if (/cover\s*letter/.test(lbl)) return { ...f, category: 'file:cover_letter' };
|
|
351
|
+
return { ...f, category: 'file:resume' };
|
|
352
|
+
}
|
|
346
353
|
// Modern Ashby uses UUID names for ALL fields (Name, Email, Phone, custom).
|
|
347
354
|
// Classify by LABEL first — that's the only signal that distinguishes
|
|
348
355
|
// a profile field from a custom question when the name attr is opaque.
|
|
@@ -525,6 +532,12 @@ async function scanPage(page, ats) {
|
|
|
525
532
|
});
|
|
526
533
|
|
|
527
534
|
console.log(`[scanPage] ${platform}: found ${out.length} fields (${out.filter(f => f.category.startsWith('profile')).length} profile, ${out.filter(f => f.category === 'custom').length} custom, ${out.filter(f => f.category === 'eeo').length} eeo)`);
|
|
535
|
+
// Per-field breakdown so we can SEE why a "Name" field ended up classified
|
|
536
|
+
// as 'custom' instead of 'profile:full_name' — without this, every fill
|
|
537
|
+
// failure is a guessing game.
|
|
538
|
+
for (const f of out) {
|
|
539
|
+
console.log(`[scanPage] - ${f.category.padEnd(22)} | ${(f.label || '(no label)').slice(0, 60)} | ${f.tag}/${f.inputType}`);
|
|
540
|
+
}
|
|
528
541
|
return out;
|
|
529
542
|
}
|
|
530
543
|
|