Skip to content

Commit 811daec

Browse files
RajjjAryanCopilot
andcommitted
feat: enhance Greenhouse adapter with comprehensive question matching
- Skip standard form fields (first_name, last_name, email, resume) in question loop - Add 60+ auto-answer patterns for common Greenhouse questions: visa/sponsorship, country residence, employment history, current employer, education, skill ratings, commercial experience, privacy acknowledgements - Handle multi_value_single_select with value matching (exact → partial → boolean) - Handle multi_value_multi_select with 'none of the above' fallback - Parse location section from profile.yml for country matching - Update profile.yml with Raj's actual details Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f7f07a3 commit 811daec

2 files changed

Lines changed: 197 additions & 24 deletions

File tree

lib/adapters/greenhouse.mjs

Lines changed: 195 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,38 +32,207 @@ export async function fetchJobDetails(boardToken, jobId) {
3232
};
3333
}
3434

35+
// Standard Greenhouse form fields — already set via form.set(), skip in question loop
36+
const STANDARD_FIELDS = new Set([
37+
'first_name', 'last_name', 'email', 'phone', 'resume', 'cover_letter',
38+
]);
39+
3540
/**
3641
* Build auto-answerable question map from profile.
42+
* Returns Map<keyword, { text: string, selectMatch?: string }>
3743
*/
3844
function buildAutoAnswers(profile, autoApplyConfig = {}) {
3945
const answers = new Map();
4046
const cand = profile.candidate || {};
47+
const loc = profile.location || {};
48+
49+
// Text answers
50+
answers.set('linkedin', { text: cand.linkedin || '' });
51+
answers.set('github', { text: cand.github || '' });
52+
answers.set('gitlab', { text: cand.github || '' });
53+
answers.set('portfolio', { text: cand.portfolio_url || '' });
54+
answers.set('website', { text: cand.portfolio_url || '' });
55+
answers.set('phone', { text: cand.phone || '' });
56+
answers.set('location', { text: cand.location || '' });
57+
answers.set('city', { text: cand.location || '' });
58+
answers.set('preferred', { text: (cand.full_name || '').split(/\s+/)[0] || '' });
59+
answers.set('preferred name', { text: (cand.full_name || '').split(/\s+/)[0] || '' });
60+
answers.set('nickname', { text: (cand.full_name || '').split(/\s+/)[0] || '' });
61+
62+
// Visa / sponsorship — text + select matching
63+
answers.set('visa', { text: autoApplyConfig.visa_status || 'No', selectMatch: 'no' });
64+
answers.set('authorized', { text: 'Yes', selectMatch: 'yes' });
65+
answers.set('sponsorship', { text: 'No', selectMatch: 'no' });
66+
answers.set('require sponsorship', { text: 'No', selectMatch: 'no' });
67+
answers.set('sponsor you', { text: 'No', selectMatch: 'no' });
68+
answers.set('sponsor', { text: 'No', selectMatch: 'no' });
69+
70+
// Start date / availability
71+
answers.set('start', { text: autoApplyConfig.start_date || '30 days notice period' });
72+
answers.set('available', { text: autoApplyConfig.start_date || '30 days notice period' });
73+
answers.set('notice period', { text: autoApplyConfig.start_date || '30 days' });
74+
75+
// Referral / source
76+
answers.set('hear about', { text: autoApplyConfig.referral_source || 'Company career page' });
77+
answers.set('how did you', { text: autoApplyConfig.referral_source || 'Company career page' });
78+
answers.set('source', { text: autoApplyConfig.referral_source || 'Company career page' });
79+
answers.set('referral', { text: autoApplyConfig.referral_source || 'Company career page' });
80+
81+
// Salary
82+
answers.set('salary', { text: autoApplyConfig.salary_expectation || 'Competitive, based on role' });
83+
answers.set('compensation', { text: autoApplyConfig.salary_expectation || 'Competitive, based on role' });
84+
85+
// Employment restrictions
86+
answers.set('employment agreement', { text: 'No', selectMatch: 'no' });
87+
answers.set('post-employment', { text: 'No', selectMatch: 'no' });
88+
answers.set('non-compete', { text: 'No', selectMatch: 'no' });
89+
answers.set('restrictive covenant', { text: 'No', selectMatch: 'no' });
90+
91+
// Country of residence
92+
answers.set('country of residence', { text: loc.country || 'India', selectMatch: loc.country || 'india' });
93+
answers.set('current country', { text: loc.country || 'India', selectMatch: loc.country || 'india' });
94+
answers.set('where are you located', { text: loc.country || 'India', selectMatch: loc.country || 'india' });
95+
answers.set('country where you currently reside', { text: loc.country || 'India', selectMatch: loc.country || 'india' });
96+
answers.set('anticipate working in', { text: loc.country || 'India', selectMatch: loc.country || 'india' });
97+
answers.set('country', { text: loc.country || 'India', selectMatch: loc.country || 'india' });
98+
99+
// Work authorization
100+
answers.set('legally authorized', { text: 'Yes', selectMatch: 'yes' });
101+
answers.set('work authorization', { text: 'Yes', selectMatch: 'yes' });
102+
answers.set('right to work', { text: 'Yes', selectMatch: 'yes' });
103+
answers.set('eligible to work', { text: 'Yes', selectMatch: 'yes' });
104+
105+
// Years of experience
106+
answers.set('years of experience', { text: '4', selectMatch: '3-5' });
107+
answers.set('experience', { text: '4 years' });
108+
answers.set('over 5 years', { text: 'No', selectMatch: 'no' });
109+
answers.set('5 years of professional', { text: 'No', selectMatch: 'no' });
110+
111+
// Current employer details (from profile narrative/proof_points)
112+
answers.set('current firm', { text: 'Zomato Limited' });
113+
answers.set('current company', { text: 'Zomato Limited' });
114+
answers.set('current employer', { text: 'Zomato Limited' });
115+
answers.set('previous employer', { text: 'Zomato Limited' });
116+
answers.set('current title', { text: 'Software Development Engineer II' });
117+
answers.set('current or previous job title', { text: 'Software Development Engineer II' });
118+
answers.set('previous job title', { text: 'Software Development Engineer II' });
119+
120+
// Education
121+
answers.set('school', { text: 'Indian Institute of Information Technology, Nagpur' });
122+
answers.set('most recent school', { text: 'Indian Institute of Information Technology, Nagpur' });
123+
answers.set('university', { text: 'Indian Institute of Information Technology, Nagpur' });
124+
answers.set('degree', { text: 'BTech in Computer Science & Engineering' });
125+
answers.set('most recent degree', { text: 'BTech in Computer Science & Engineering' });
126+
127+
// Previously worked at company X? → No
128+
answers.set('previously worked', { text: 'No', selectMatch: 'no' });
129+
answers.set('have you ever been employed', { text: 'No', selectMatch: 'no' });
130+
answers.set('have you previously worked', { text: 'No', selectMatch: 'no' });
131+
answers.set('do you currently work for', { text: 'No', selectMatch: 'no' });
132+
answers.set('currently or have you previously', { text: 'No', selectMatch: 'no' });
133+
answers.set('been involved in procurement', { text: 'No', selectMatch: 'no' });
134+
135+
// Do you live in sanctioned country? → No
136+
answers.set('belarus, cuba', { text: 'No', selectMatch: 'no' });
137+
answers.set('sanctioned', { text: 'No', selectMatch: 'no' });
138+
answers.set('citizenship one of the following', { text: 'No', selectMatch: 'no' });
139+
answers.set('permanent residency in one of', { text: 'No', selectMatch: 'no' });
140+
141+
// Acknowledge / confirm
142+
answers.set('i acknowledge', { text: 'I acknowledge', selectMatch: 'i acknowledge' });
143+
answers.set('acknowledge', { text: 'I acknowledge', selectMatch: 'i acknowledge' });
144+
answers.set('privacy policy', { text: 'I acknowledge', selectMatch: 'i acknowledge' });
145+
answers.set('confidential information', { text: 'I acknowledge', selectMatch: 'i acknowledge' });
146+
147+
// Opt-in for comms
148+
answers.set('opt-in', { text: 'Yes', selectMatch: 'yes' });
149+
answers.set('whatsapp', { text: 'Yes', selectMatch: 'yes' });
150+
151+
// Do you live in location? → Yes
152+
answers.set('do you currently live', { text: 'Yes', selectMatch: 'yes' });
153+
answers.set('this role is open to candidates', { text: 'Yes', selectMatch: 'yes' });
41154

42-
answers.set('linkedin', cand.linkedin || '');
43-
answers.set('github', cand.github || '');
44-
answers.set('portfolio', cand.portfolio_url || '');
45-
answers.set('website', cand.portfolio_url || '');
46-
answers.set('phone', cand.phone || '');
47-
answers.set('location', cand.location || '');
48-
answers.set('city', cand.location || '');
49-
answers.set('visa', autoApplyConfig.visa_status || '');
50-
answers.set('authorized', autoApplyConfig.visa_status || '');
51-
answers.set('sponsorship', autoApplyConfig.visa_status || '');
52-
answers.set('start', autoApplyConfig.start_date || '');
53-
answers.set('available', autoApplyConfig.start_date || '');
54-
answers.set('hear', autoApplyConfig.referral_source || 'Company career page');
55-
answers.set('source', autoApplyConfig.referral_source || 'Company career page');
56-
answers.set('referral', autoApplyConfig.referral_source || 'Company career page');
57-
answers.set('salary', autoApplyConfig.salary_expectation || '');
58-
answers.set('compensation', autoApplyConfig.salary_expectation || '');
155+
// Remote work
156+
answers.set('plan to work from', { text: 'No', selectMatch: 'no' });
157+
answers.set('plan to work remotely', { text: 'Yes', selectMatch: 'yes' });
158+
answers.set('work remotely', { text: 'Yes', selectMatch: 'yes' });
159+
answers.set('intend to work', { text: 'Yes', selectMatch: 'yes' });
160+
161+
// Commercial experience questions
162+
answers.set('commercial experience using go', { text: 'Yes', selectMatch: 'yes' });
163+
answers.set('commercial experience using kubernetes', { text: 'Yes', selectMatch: 'yes' });
164+
answers.set('hands-on experience', { text: 'Yes', selectMatch: 'yes' });
165+
answers.set('production experience', { text: 'Yes', selectMatch: 'yes' });
166+
167+
// Skill ratings
168+
answers.set('rate your level of skill', { text: 'Intermediate', selectMatch: 'intermediate' });
169+
answers.set('level of skill programming', { text: 'Intermediate', selectMatch: 'intermediate' });
170+
171+
// How did you hear / learn about
172+
answers.set('how did you learn', { text: 'Company career page', selectMatch: 'career' });
173+
answers.set('learn about this job', { text: 'Company career page', selectMatch: 'career' });
174+
answers.set('how did you hear', { text: 'Company career page', selectMatch: 'career' });
175+
answers.set('hear about', { text: 'Company career page', selectMatch: 'career' });
176+
177+
// Conditional follow-ups ("if you answered yes/selected above")
178+
answers.set('if you answered', { text: 'N/A', selectMatch: 'none' });
179+
answers.set('if you selected', { text: 'N/A', selectMatch: 'none' });
180+
answers.set('select all that apply', { text: 'N/A', selectMatch: 'none' });
181+
182+
// AI tools
183+
answers.set('ai tools', { text: 'GitHub Copilot for daily coding, ChatGPT for research and brainstorming, and custom AI pipelines for job search automation.' });
59184

60185
return answers;
61186
}
62187

63-
function matchAnswer(label, autoAnswers) {
188+
/**
189+
* Match a question label to an auto-answer.
190+
* For select-type questions, find the best matching option value.
191+
*/
192+
function matchAnswer(label, autoAnswers, question = null) {
64193
const lower = label.toLowerCase();
65194
for (const [pattern, answer] of autoAnswers) {
66-
if (answer && lower.includes(pattern)) return answer;
195+
if (!lower.includes(pattern)) continue;
196+
197+
// For select questions, find the matching option
198+
if (question && question.type.includes('select') && question.values?.length > 0) {
199+
const target = (answer.selectMatch || answer.text || '').toLowerCase();
200+
if (!target) continue;
201+
202+
// Exact label match first
203+
const exact = question.values.find(v => v.label.toLowerCase() === target);
204+
if (exact) return exact.value;
205+
206+
// Partial match
207+
const partial = question.values.find(v => v.label.toLowerCase().includes(target));
208+
if (partial) return partial.value;
209+
210+
// Try matching "No" / "Yes" for boolean-like selects
211+
if (['yes', 'no'].includes(target)) {
212+
const boolMatch = question.values.find(v => v.label.toLowerCase() === target);
213+
if (boolMatch) return boolMatch.value;
214+
}
215+
216+
// For multi_value_multi_select: look for "none of the above" or "N/A"
217+
if (question.type === 'multi_value_multi_select') {
218+
const noneOpt = question.values.find(v => {
219+
const vl = v.label.toLowerCase();
220+
return vl.includes('none of the above') || vl.includes('n/a') ||
221+
vl.includes('not applicable') || vl.includes('none');
222+
});
223+
if (noneOpt) return noneOpt.value;
224+
225+
// For acknowledge/confirm multi-selects, pick the first/only option
226+
if (target.includes('acknowledge') || target.includes('confirm')) {
227+
return question.values[0]?.value;
228+
}
229+
}
230+
231+
continue; // selectMatch didn't find an option
232+
}
233+
234+
// Text answer
235+
if (answer.text) return answer.text;
67236
}
68237
return null;
69238
}
@@ -102,11 +271,14 @@ export async function submitApplication({ boardToken, jobId, profile, pdfPath, c
102271
const unanswered = [];
103272

104273
for (const q of job.questions) {
105-
const answer = matchAnswer(q.label, autoAnswers);
106-
if (answer) {
107-
form.set(q.id, answer);
274+
// Skip standard fields already handled above
275+
if (STANDARD_FIELDS.has(q.id)) continue;
276+
277+
const answer = matchAnswer(q.label, autoAnswers, q);
278+
if (answer !== null && answer !== undefined) {
279+
form.set(q.id, String(answer));
108280
} else if (q.required) {
109-
unanswered.push({ id: q.id, label: q.label, type: q.type });
281+
unanswered.push({ id: q.id, label: q.label, type: q.type, values: q.values?.slice(0, 5) });
110282
}
111283
}
112284

lib/apply-engine.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function recordSubmission() {
2929
*/
3030
function loadProfile(profilePath) {
3131
const content = readFileSync(profilePath, 'utf-8');
32-
const profile = { candidate: {}, auto_apply: {} };
32+
const profile = { candidate: {}, auto_apply: {}, location: {} };
3333
let currentSection = null;
3434

3535
for (const line of content.split('\n')) {
@@ -51,6 +51,7 @@ function loadProfile(profilePath) {
5151
const val = kvMatch[2].trim();
5252
if (currentSection === 'candidate') profile.candidate[key] = val;
5353
else if (currentSection === 'auto_apply') profile.auto_apply[key] = val;
54+
else if (currentSection === 'location') profile.location[key] = val;
5455
}
5556
}
5657
}

0 commit comments

Comments
 (0)