careervivid 1.12.26 → 1.12.31
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/agent/instructions.d.ts +0 -1
- package/dist/agent/instructions.d.ts.map +1 -1
- package/dist/agent/instructions.js +55 -40
- package/dist/agent/tools/jobs.d.ts.map +1 -1
- package/dist/agent/tools/jobs.js +21 -12
- package/dist/agent/tools/local-tracker.d.ts +6 -0
- package/dist/agent/tools/local-tracker.d.ts.map +1 -1
- package/dist/agent/tools/local-tracker.js +221 -34
- package/dist/agent/tools/urlVerifier.js +1 -1
- package/dist/commands/agent/configurator.d.ts.map +1 -1
- package/dist/commands/agent/configurator.js +16 -6
- package/dist/commands/agent/repl.d.ts.map +1 -1
- package/dist/commands/agent/repl.js +75 -11
- package/dist/eval/runner.d.ts +2 -2
- package/dist/eval/runner.js +7 -7
- package/dist/eval/test-cases/jobs-agent.js +40 -40
- package/package.json +1 -1
|
@@ -14,7 +14,6 @@ export declare const BASE_IDENTITY: string;
|
|
|
14
14
|
export declare const RESUME_SECTION: string;
|
|
15
15
|
export declare const CODING_SECTION: string;
|
|
16
16
|
export declare const JOBS_TOOLS_SECTION: string;
|
|
17
|
-
export declare const BROWSER_SECTION: string;
|
|
18
17
|
export declare const JOBS_HARNESS: string;
|
|
19
18
|
export declare const GREETING_PROTOCOL: string;
|
|
20
19
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../src/agent/instructions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,aAAa,QAalB,CAAC;AAMT,eAAO,MAAM,cAAc,QAcnB,CAAC;AAMT,eAAO,MAAM,cAAc,QAgBnB,CAAC;AAMT,eAAO,MAAM,kBAAkB,
|
|
1
|
+
{"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../src/agent/instructions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,aAAa,QAalB,CAAC;AAMT,eAAO,MAAM,cAAc,QAcnB,CAAC;AAMT,eAAO,MAAM,cAAc,QAgBnB,CAAC;AAMT,eAAO,MAAM,kBAAkB,QAoCvB,CAAC;AAMT,eAAO,MAAM,YAAY,QAsCjB,CAAC;AAMT,eAAO,MAAM,iBAAiB,QActB,CAAC;AAMT;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IACzC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,GAAG,MAAM,CAyBT"}
|
|
@@ -74,62 +74,78 @@ export const JOBS_TOOLS_SECTION = `
|
|
|
74
74
|
- **tailor_resume** — Tailor/refine the user's resume for a specific role or JD.
|
|
75
75
|
- **delete_resume** — Permanently delete a resume (ask for confirmation first).
|
|
76
76
|
|
|
77
|
-
### Job Search
|
|
78
|
-
- **search_jobs** — Search for jobs scored against the user's resume (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
- **
|
|
85
|
-
- **
|
|
86
|
-
- **
|
|
87
|
-
- **
|
|
88
|
-
- **
|
|
89
|
-
|
|
77
|
+
### Job Search
|
|
78
|
+
- **search_jobs** — Search for jobs scored against the user's resume. Returns results for review (dry_run by default — does NOT auto-save).
|
|
79
|
+
|
|
80
|
+
### CSV Pipeline Tracker (tracker_*)
|
|
81
|
+
These tools read/write jobs.csv — the local career-ops pipeline spreadsheet.
|
|
82
|
+
- **tracker_list_jobs** — Show the pipeline (supports tier/status filters and sort_by).
|
|
83
|
+
- **tracker_add_job** — Add a new company to the tracker.
|
|
84
|
+
- **tracker_update_job** — Update any field on a job entry (status, scores, notes, follow-up).
|
|
85
|
+
- **tracker_rank_priority** — Priority-ranked view; use for "what next?" questions.
|
|
86
|
+
- **tracker_dashboard** — Full analytics: apply rate, avg scores, salary data, stale count.
|
|
87
|
+
- **tracker_find_stale** — Surface cold companies with next-action recommendations.
|
|
88
|
+
- **tracker_inspect_quality**— Scan for duplicates, missing URLs, and corrupted data (read-only).
|
|
89
|
+
|
|
90
|
+
### Web Kanban Board (kanban_*)
|
|
91
|
+
These tools read/write the Firebase Kanban board at careervivid.app/job-tracker.
|
|
92
|
+
- **kanban_add_job** — Save a job card to the web Kanban board.
|
|
93
|
+
- **kanban_list_jobs** — Show the web Kanban board.
|
|
94
|
+
- **kanban_update_status** — Move a Kanban card to a new status column.
|
|
95
|
+
|
|
96
|
+
### Browser Automation (browser_*)
|
|
97
|
+
- **browser_autofill_application** ⭐ — Auto-fill an application form in Chrome (does NOT submit).
|
|
98
|
+
- browser_navigate, browser_state, browser_click, browser_type, browser_select, browser_scroll, browser_screenshot
|
|
90
99
|
|
|
91
100
|
### URL Safety (Mandatory)
|
|
92
|
-
- **verify_url**
|
|
93
|
-
- **
|
|
101
|
+
- **verify_url** — Verify a single link is alive before sharing it.
|
|
102
|
+
- **verify_job_urls** — Verify all URLs from a search_jobs result batch.
|
|
94
103
|
NEVER share a link without verifying it first.
|
|
95
104
|
`.trim();
|
|
96
105
|
// ---------------------------------------------------------------------------
|
|
97
|
-
// §5 —
|
|
106
|
+
// §5 — Autonomous execution harness (appended in --jobs mode)
|
|
98
107
|
// ---------------------------------------------------------------------------
|
|
99
|
-
export const
|
|
100
|
-
##
|
|
108
|
+
export const JOBS_HARNESS = `
|
|
109
|
+
## Autonomy Rules (Non-Negotiable)
|
|
101
110
|
|
|
102
|
-
|
|
103
|
-
-
|
|
111
|
+
### What you may do freely (no confirmation needed)
|
|
112
|
+
- Call any read-only tool (tracker_list_jobs, tracker_dashboard, tracker_rank_priority, tracker_find_stale, tracker_inspect_quality, search_jobs, get_resume, etc.)
|
|
113
|
+
- Add a new company/job entry via tracker_add_job (when the user explicitly names a company or approves a search result)
|
|
114
|
+
- Update non-status fields (attention_score, excitement, notes, follow_up_date, etc.) via tracker_update_job
|
|
104
115
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
### What requires explicit user confirmation
|
|
117
|
+
- Changing status to "Applied", "Rejected", or "Ghosted" — present the proposed change and wait for "yes"
|
|
118
|
+
- Deleting any entry
|
|
119
|
+
- Submitting any web form via browser tools
|
|
120
|
+
|
|
121
|
+
### Anti-hallucination rules (CRITICAL)
|
|
122
|
+
- NEVER invent company names. All companies added via tracker_add_job MUST come from:
|
|
123
|
+
(a) verified search_jobs results with a real URL, OR (b) explicit user input naming the company.
|
|
124
|
+
- If search_jobs returns 0 results: say so — do NOT generate fictional alternative companies.
|
|
125
|
+
- Do NOT add more than 5 new companies in a single response without user review.
|
|
126
|
+
- A company without a valid careers_url (starting with http) is flagged as unverified — add it with a note.
|
|
112
127
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
4. Complete the full workflow (e.g., tailoring a resume, finding jobs) ONLY if explicitly requested or obviously needed. However, if the user just asks a simple targeted question (e.g., "Did you find X?", "Is my resume up to date?"), DO NOT trigger an unprompted mass workflow (like saving extra companies and rewriting multiple resumes). Answer the question immediately.
|
|
128
|
+
### Deduplication rule (MANDATORY)
|
|
129
|
+
Before calling tracker_add_job, ALWAYS check tracker_list_jobs to see if the company+role already exists.
|
|
130
|
+
If it does, call tracker_update_job instead — never create a duplicate row.
|
|
117
131
|
|
|
118
132
|
## Mandatory Tool Dispatch Table
|
|
119
133
|
|
|
120
134
|
| User asks about… | Call… |
|
|
121
135
|
|-----------------------------------------------|--------------------------------|
|
|
122
|
-
| pipeline, job list, tracker |
|
|
123
|
-
| priority, what next, best ROI |
|
|
124
|
-
| stats, dashboard, apply rate, search health |
|
|
125
|
-
| stale, cold, neglecting, need attention |
|
|
126
|
-
|
|
|
127
|
-
|
|
|
136
|
+
| pipeline, job list, tracker | tracker_list_jobs |
|
|
137
|
+
| priority, what next, best ROI | tracker_rank_priority |
|
|
138
|
+
| stats, dashboard, apply rate, search health | tracker_dashboard |
|
|
139
|
+
| stale, cold, neglecting, need attention | tracker_find_stale |
|
|
140
|
+
| duplicates, data quality, audit | tracker_inspect_quality |
|
|
141
|
+
| adding a company, new job | tracker_add_job |
|
|
142
|
+
| updating status, marking applied, follow-up | tracker_update_job |
|
|
143
|
+
| Kanban board, web tracker | kanban_list_jobs |
|
|
128
144
|
| resume, background, skills, experience | get_resume |
|
|
129
145
|
| find jobs, search roles | get_resume → search_jobs |
|
|
130
146
|
`.trim();
|
|
131
147
|
// ---------------------------------------------------------------------------
|
|
132
|
-
// §
|
|
148
|
+
// §6 — Greeting protocol (shared across modes)
|
|
133
149
|
// ---------------------------------------------------------------------------
|
|
134
150
|
export const GREETING_PROTOCOL = `
|
|
135
151
|
## Greeting Protocol
|
|
@@ -147,7 +163,7 @@ When the user sends a generic greeting ("hey", "hi", "hello", "start"), respond
|
|
|
147
163
|
Just tell me what you need!"
|
|
148
164
|
`.trim();
|
|
149
165
|
// ---------------------------------------------------------------------------
|
|
150
|
-
// §
|
|
166
|
+
// §7 — Assembled system prompts per mode (the public API)
|
|
151
167
|
// ---------------------------------------------------------------------------
|
|
152
168
|
/**
|
|
153
169
|
* Returns the assembled system prompt for a given agent mode.
|
|
@@ -159,7 +175,6 @@ export function buildSystemPrompt(options) {
|
|
|
159
175
|
BASE_IDENTITY,
|
|
160
176
|
RESUME_SECTION,
|
|
161
177
|
JOBS_TOOLS_SECTION,
|
|
162
|
-
BROWSER_SECTION,
|
|
163
178
|
JOBS_HARNESS,
|
|
164
179
|
GREETING_PROTOCOL,
|
|
165
180
|
].join("\n\n---\n\n");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/jobs.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAmBlC,eAAO,MAAM,cAAc,EAAE,
|
|
1
|
+
{"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/jobs.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAmBlC,eAAO,MAAM,cAAc,EAAE,IAmG5B,CAAC;AAMF,eAAO,MAAM,WAAW,EAAE,IA4EzB,CAAC;AAMF,eAAO,MAAM,YAAY,EAAE,IAgE1B,CAAC;AAMF,eAAO,MAAM,mBAAmB,EAAE,IAwDjC,CAAC;AAMF,eAAO,MAAM,aAAa,EAAE,IAgC3B,CAAC;AAMF,eAAO,MAAM,eAAe,EAAE,IA6B7B,CAAC;AAMF,eAAO,MAAM,gBAAgB,EAAE,IAmE9B,CAAC;AAMF,eAAO,MAAM,gBAAgB,EAAE,IAwB9B,CAAC;AAMF,eAAO,MAAM,cAAc,EAAE,IAyO5B,CAAC;AAMF,eAAO,MAAM,aAAa,EAAE,IAAI,EAU/B,CAAC"}
|
package/dist/agent/tools/jobs.js
CHANGED
|
@@ -12,10 +12,10 @@ import { jobsHunt, jobsCreate, jobsList, jobsUpdate, resumeGet, resumesList, res
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
export const SearchJobsTool = {
|
|
14
14
|
name: "search_jobs",
|
|
15
|
-
description: `Search for jobs
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
description: `Search for jobs matching a role and location, scored against the user's resume.
|
|
16
|
+
Returns a scored list of job opportunities with AI summaries and missing skills.
|
|
17
|
+
Use this when the user asks to "find jobs", "search for roles", "look for positions", etc.
|
|
18
|
+
By default operates in dry_run mode — results are shown for review, nothing is saved automatically.`,
|
|
19
19
|
parameters: {
|
|
20
20
|
type: Type.OBJECT,
|
|
21
21
|
properties: {
|
|
@@ -33,12 +33,16 @@ Use this when the user asks to "find jobs", "search for roles", "look for positi
|
|
|
33
33
|
},
|
|
34
34
|
min_score: {
|
|
35
35
|
type: Type.NUMBER,
|
|
36
|
-
description: "Optional. Minimum match score (0-100)
|
|
36
|
+
description: "Optional. Minimum match score (0-100). Default is 70. Use 80 for excellent matches only.",
|
|
37
37
|
},
|
|
38
38
|
resume_id: {
|
|
39
39
|
type: Type.STRING,
|
|
40
40
|
description: "Optional. Specific resume ID to score against. If omitted, uses the user's latest resume.",
|
|
41
41
|
},
|
|
42
|
+
dry_run: {
|
|
43
|
+
type: Type.BOOLEAN,
|
|
44
|
+
description: "Optional. Default: true. If true, shows results for review without auto-saving. Set false only if user explicitly says to save all results.",
|
|
45
|
+
},
|
|
42
46
|
},
|
|
43
47
|
required: ["role"],
|
|
44
48
|
},
|
|
@@ -59,7 +63,7 @@ Use this when the user asks to "find jobs", "search for roles", "look for positi
|
|
|
59
63
|
role: args.role,
|
|
60
64
|
location: args.location,
|
|
61
65
|
count: args.count ?? 10,
|
|
62
|
-
minScore: args.min_score,
|
|
66
|
+
minScore: args.min_score ?? 70, // default 70 to filter low-quality results
|
|
63
67
|
});
|
|
64
68
|
if (isApiError(result)) {
|
|
65
69
|
return `Error searching jobs: ${result.message}`;
|
|
@@ -79,15 +83,20 @@ Use this when the user asks to "find jobs", "search for roles", "look for positi
|
|
|
79
83
|
` Job ID: ${job.id}`);
|
|
80
84
|
})
|
|
81
85
|
.join("\n\n");
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
const isDryRun = args.dry_run !== false; // default true
|
|
87
|
+
const dryRunNote = isDryRun
|
|
88
|
+
? `\n\n📋 Review above. Say "add [company] to my tracker" to save specific ones.`
|
|
89
|
+
: "";
|
|
90
|
+
return (`Found ${result.total} jobs for "${args.role}"${args.location ? ` in ${args.location}` : ""}. ` +
|
|
91
|
+
`Showing top ${result.jobs.length} (min score ${args.min_score ?? 70}%):\n\n${jobList}` +
|
|
92
|
+
dryRunNote);
|
|
84
93
|
},
|
|
85
94
|
};
|
|
86
95
|
// ---------------------------------------------------------------------------
|
|
87
96
|
// Tool: save_job
|
|
88
97
|
// ---------------------------------------------------------------------------
|
|
89
98
|
export const SaveJobTool = {
|
|
90
|
-
name: "
|
|
99
|
+
name: "kanban_add_job",
|
|
91
100
|
description: `Save a job to the user's CareerVivid job tracker Kanban board.
|
|
92
101
|
The job will appear in the "To Apply" column of the /job-tracker page.
|
|
93
102
|
Use this after finding interesting jobs via search_jobs, or when the user asks to "save", "add", or "track" a specific job.`,
|
|
@@ -154,7 +163,7 @@ Use this after finding interesting jobs via search_jobs, or when the user asks t
|
|
|
154
163
|
// Tool: list_jobs
|
|
155
164
|
// ---------------------------------------------------------------------------
|
|
156
165
|
export const ListJobsTool = {
|
|
157
|
-
name: "
|
|
166
|
+
name: "kanban_list_jobs",
|
|
158
167
|
description: `List the user's jobs from their CareerVivid job tracker Kanban board.
|
|
159
168
|
Can filter by status. Use this when the user asks "what jobs do I have?", "show my tracker",
|
|
160
169
|
"what's in my job pipeline?", "check my interviews", etc.`,
|
|
@@ -216,7 +225,7 @@ Can filter by status. Use this when the user asks "what jobs do I have?", "show
|
|
|
216
225
|
// Tool: update_job_status
|
|
217
226
|
// ---------------------------------------------------------------------------
|
|
218
227
|
export const UpdateJobStatusTool = {
|
|
219
|
-
name: "
|
|
228
|
+
name: "kanban_update_status",
|
|
220
229
|
description: `Move a job to a different status on the CareerVivid Kanban board.
|
|
221
230
|
Use this when the user mentions: "I got an interview", "I applied to X", "I got an offer",
|
|
222
231
|
"X rejected me", "move Y to applied", etc. The change will be visible in the /job-tracker UI.`,
|
|
@@ -412,7 +421,7 @@ MAKE SURE the user actually intends to delete it, as this is permanent.`,
|
|
|
412
421
|
// Tool: apply_to_job
|
|
413
422
|
// ---------------------------------------------------------------------------
|
|
414
423
|
export const ApplyToJobTool = {
|
|
415
|
-
name: "
|
|
424
|
+
name: "browser_autofill_application",
|
|
416
425
|
description: `Open a job application in the user's Chrome browser and auto-fill it with AI-tailored answers.
|
|
417
426
|
This tool launches Playwright, navigates to the job URL, extracts the form fields, fills them
|
|
418
427
|
with AI-generated answers from the user's resume, and shows the user a final preview before submitting.
|
|
@@ -19,11 +19,17 @@
|
|
|
19
19
|
* flag_stale_jobs → surface companies with no recent activity
|
|
20
20
|
*/
|
|
21
21
|
import { Tool } from "../Tool.js";
|
|
22
|
+
export interface JobValidationResult {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
blockers: string[];
|
|
25
|
+
warnings: string[];
|
|
26
|
+
}
|
|
22
27
|
export declare const ListLocalJobsTool: Tool;
|
|
23
28
|
export declare const UpdateLocalJobTool: Tool;
|
|
24
29
|
export declare const AddLocalJobTool: Tool;
|
|
25
30
|
export declare const ScorePipelineTool: Tool;
|
|
26
31
|
export declare const GetPipelineMetricsTool: Tool;
|
|
27
32
|
export declare const FlagStaleJobsTool: Tool;
|
|
33
|
+
export declare const InspectQualityTool: Tool;
|
|
28
34
|
export declare const ALL_LOCAL_TRACKER_TOOLS: Tool[];
|
|
29
35
|
//# sourceMappingURL=local-tracker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-tracker.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/local-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"local-tracker.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/local-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAkClC,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,OAAO,CAAC;IACZ,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAoOD,eAAO,MAAM,iBAAiB,EAAE,IA8H/B,CAAC;AAMF,eAAO,MAAM,kBAAkB,EAAE,IAqIhC,CAAC;AAMF,eAAO,MAAM,eAAe,EAAE,IAkJ7B,CAAC;AAMF,eAAO,MAAM,iBAAiB,EAAE,IA2F/B,CAAC;AAMF,eAAO,MAAM,sBAAsB,EAAE,IA+GpC,CAAC;AAMF,eAAO,MAAM,iBAAiB,EAAE,IAoE/B,CAAC;AAMF,eAAO,MAAM,kBAAkB,EAAE,IAsEhC,CAAC;AAMF,eAAO,MAAM,uBAAuB,EAAE,IAAI,EAQzC,CAAC"}
|
|
@@ -19,10 +19,81 @@
|
|
|
19
19
|
* flag_stale_jobs → surface companies with no recent activity
|
|
20
20
|
*/
|
|
21
21
|
import { Type } from "@google/genai";
|
|
22
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
22
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from "fs";
|
|
23
23
|
import { resolve, dirname } from "path";
|
|
24
24
|
import { fileURLToPath } from "url";
|
|
25
25
|
import { homedir } from "os";
|
|
26
|
+
import { verifyUrl } from "./urlVerifier.js";
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Job entry validation harness
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/** Patterns that strongly suggest an AI-hallucinated company name. */
|
|
31
|
+
const HALLUCINATED_COMPANY_PATTERNS = [
|
|
32
|
+
/synthai/i,
|
|
33
|
+
/innovatex/i,
|
|
34
|
+
/innovatech/i,
|
|
35
|
+
/datagenius/i,
|
|
36
|
+
/nexusai/i,
|
|
37
|
+
/quantumflow/i,
|
|
38
|
+
/skymind/i,
|
|
39
|
+
/integratex/i,
|
|
40
|
+
/aiworks/i,
|
|
41
|
+
/techsolutions\s*inc/i,
|
|
42
|
+
/aether\s*systems/i,
|
|
43
|
+
/cogni(serve|tech|flow)/i,
|
|
44
|
+
/gentech\s*ai/i,
|
|
45
|
+
/brightspark\s*labs/i,
|
|
46
|
+
/synapse\s*tech/i,
|
|
47
|
+
/veridian\s*ai/i,
|
|
48
|
+
// Generic filler patterns: "XYZ AI", "XYZ Solutions", "XYZ Labs" with single-word prefix
|
|
49
|
+
/^[A-Z][a-z]+(\s*(AI|Solutions|Labs|Systems|Technologies|Innovations|Tech|Ventures))+$/,
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Pre-add validation harness.
|
|
53
|
+
* Runs structural + network checks before any row is written to jobs.csv.
|
|
54
|
+
*/
|
|
55
|
+
async function validateJobEntry(company, careersUrl) {
|
|
56
|
+
const blockers = [];
|
|
57
|
+
const warnings = [];
|
|
58
|
+
// ── 1. Company name hallucination check ──────────────────────────────────
|
|
59
|
+
for (const pattern of HALLUCINATED_COMPANY_PATTERNS) {
|
|
60
|
+
if (pattern.test(company)) {
|
|
61
|
+
blockers.push(`Company name "${company}" matches a known hallucination pattern. ` +
|
|
62
|
+
`Verify this is a real company before adding.`);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// ── 2. URL verification (if provided) ────────────────────────────────────
|
|
67
|
+
if (careersUrl && careersUrl.startsWith("http")) {
|
|
68
|
+
const urlResult = await verifyUrl(careersUrl);
|
|
69
|
+
if (!urlResult.ok) {
|
|
70
|
+
blockers.push(`URL verification failed: ${urlResult.reason.replace(/^[❌⚠️✅]\s*/u, "")}`);
|
|
71
|
+
}
|
|
72
|
+
else if (urlResult.warning) {
|
|
73
|
+
warnings.push(`URL: ${urlResult.warning}`);
|
|
74
|
+
}
|
|
75
|
+
// ── 3. Domain ↔ company name coherence check ─────────────────────────
|
|
76
|
+
if (careersUrl.startsWith("http")) {
|
|
77
|
+
try {
|
|
78
|
+
const parsed = new URL(careersUrl);
|
|
79
|
+
const domain = parsed.hostname.replace(/^www\./, "").split(".")[0].toLowerCase();
|
|
80
|
+
const coName = company.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
81
|
+
const coShort = coName.slice(0, 5); // first 5 chars of company (no-spaces)
|
|
82
|
+
// Flag if domain has zero overlap with company name (e.g., stripe.com for "JPMorgan")
|
|
83
|
+
const domainMatchesCompany = domain.includes(coShort) ||
|
|
84
|
+
coName.includes(domain) ||
|
|
85
|
+
domain.length < 4; // short generics (e.g., "wk2" for Workday) are inconclusive
|
|
86
|
+
if (!domainMatchesCompany) {
|
|
87
|
+
warnings.push(`Domain "${parsed.hostname}" may not match company "${company}" — double-check this is the right URL.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// URL parse failed — already caught in verifyUrl
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { ok: blockers.length === 0, blockers, warnings };
|
|
96
|
+
}
|
|
26
97
|
// ---------------------------------------------------------------------------
|
|
27
98
|
// CSV path resolution
|
|
28
99
|
// ---------------------------------------------------------------------------
|
|
@@ -126,6 +197,10 @@ function loadCsv() {
|
|
|
126
197
|
return { rows: parseCsv(raw), path };
|
|
127
198
|
}
|
|
128
199
|
function saveCsv(rows, csvPath) {
|
|
200
|
+
// Auto-backup before every write — recoverable with jobs.csv.bak
|
|
201
|
+
if (existsSync(csvPath)) {
|
|
202
|
+
copyFileSync(csvPath, csvPath + ".bak");
|
|
203
|
+
}
|
|
129
204
|
writeFileSync(csvPath, serializeCsv(rows), "utf-8");
|
|
130
205
|
}
|
|
131
206
|
function today() {
|
|
@@ -172,11 +247,11 @@ const EFFORT_ICONS = {
|
|
|
172
247
|
// Tool: list_local_jobs
|
|
173
248
|
// ---------------------------------------------------------------------------
|
|
174
249
|
export const ListLocalJobsTool = {
|
|
175
|
-
name: "
|
|
176
|
-
description: `Read the
|
|
250
|
+
name: "tracker_list_jobs",
|
|
251
|
+
description: `Read the job pipeline from jobs.csv (the career-ops tracker spreadsheet).
|
|
177
252
|
Returns a formatted summary of target companies grouped by tier and status.
|
|
178
253
|
Use this when the user asks to "show my job pipeline", "list target companies",
|
|
179
|
-
"
|
|
254
|
+
"what companies am I targeting?", "check my pipeline", etc.
|
|
180
255
|
Shows attention scores, apply effort, excitement, and follow-up dates.`,
|
|
181
256
|
parameters: {
|
|
182
257
|
type: Type.OBJECT,
|
|
@@ -296,15 +371,16 @@ Shows attention scores, apply effort, excitement, and follow-up dates.`,
|
|
|
296
371
|
// Tool: update_local_job
|
|
297
372
|
// ---------------------------------------------------------------------------
|
|
298
373
|
export const UpdateLocalJobTool = {
|
|
299
|
-
name: "
|
|
300
|
-
description: `Update any field on a row in
|
|
374
|
+
name: "tracker_update_job",
|
|
375
|
+
description: `Update any field on a row in jobs.csv by job ID.
|
|
301
376
|
Use this for:
|
|
302
377
|
- Status changes: "Mark WorkOS as Applied"
|
|
303
378
|
- Attention updates: "Set attention_score for CUR-001 to 10"
|
|
304
379
|
- Follow-up dates: "Set follow-up for NEO-001 to 2026-04-20"
|
|
305
380
|
- Notes: "Add note to VER-001: great culture, hiring manager is Jane"
|
|
306
381
|
- Excitement: "Update excitement for TMP-001 to 9"
|
|
307
|
-
Auto-stamps last_activity_date on every update
|
|
382
|
+
Auto-stamps last_activity_date on every update.
|
|
383
|
+
IMPORTANT: To change status to 'Applied', you MUST also provide date_applied (YYYY-MM-DD). Otherwise return a confirmation prompt.`,
|
|
308
384
|
parameters: {
|
|
309
385
|
type: Type.OBJECT,
|
|
310
386
|
properties: {
|
|
@@ -338,12 +414,17 @@ Auto-stamps last_activity_date on every update.`,
|
|
|
338
414
|
if (!VALID_STATUSES.includes(args.status)) {
|
|
339
415
|
return `❌ Invalid status "${args.status}". Valid: ${VALID_STATUSES.join(", ")}`;
|
|
340
416
|
}
|
|
417
|
+
// Gate: Applied requires explicit date_applied to prevent autonomous escalation
|
|
418
|
+
if (args.status === "Applied" && !args.date_applied && row.status !== "Applied") {
|
|
419
|
+
return (`⚠️ CONFIRMATION REQUIRED\n` +
|
|
420
|
+
`To mark "${row.company} — ${row.role}" as Applied, please confirm with a date:\n` +
|
|
421
|
+
` Example: "Yes, mark ${args.job_id} as Applied on ${today()}"`);
|
|
422
|
+
}
|
|
341
423
|
changes.push(`status: ${row.status} → ${args.status}`);
|
|
342
424
|
row.status = args.status;
|
|
343
|
-
// Auto-set date_applied when marking Applied
|
|
344
425
|
if (args.status === "Applied" && !row.date_applied) {
|
|
345
|
-
row.date_applied = today();
|
|
346
|
-
changes.push(`date_applied:
|
|
426
|
+
row.date_applied = args.date_applied ?? today();
|
|
427
|
+
changes.push(`date_applied: ${row.date_applied}`);
|
|
347
428
|
}
|
|
348
429
|
}
|
|
349
430
|
if (args.date_applied) {
|
|
@@ -393,9 +474,11 @@ Auto-stamps last_activity_date on every update.`,
|
|
|
393
474
|
row.interview_rounds = String(args.interview_rounds);
|
|
394
475
|
}
|
|
395
476
|
if (args.notes) {
|
|
477
|
+
const MAX_NOTES = 500;
|
|
478
|
+
const appendText = args.notes.length > 200 ? args.notes.slice(0, 200) + "…" : args.notes;
|
|
396
479
|
const existing = row.notes ? row.notes + "; " : "";
|
|
397
|
-
row.notes = (existing +
|
|
398
|
-
changes.push(`notes: appended "${
|
|
480
|
+
row.notes = (existing + appendText).replace(/,/g, ";").slice(0, MAX_NOTES);
|
|
481
|
+
changes.push(`notes: appended "${appendText.substring(0, 60)}"`);
|
|
399
482
|
}
|
|
400
483
|
if (changes.length === 0) {
|
|
401
484
|
return `No changes specified for ${args.job_id}. Provide at least one field to update.`;
|
|
@@ -419,13 +502,16 @@ Auto-stamps last_activity_date on every update.`,
|
|
|
419
502
|
// Tool: add_local_job
|
|
420
503
|
// ---------------------------------------------------------------------------
|
|
421
504
|
export const AddLocalJobTool = {
|
|
422
|
-
name: "
|
|
423
|
-
description: `Add a new company/role row to career-ops
|
|
505
|
+
name: "tracker_add_job",
|
|
506
|
+
description: `Add a new company/role row to jobs.csv (the career-ops pipeline tracker).
|
|
424
507
|
Use this when the user says:
|
|
425
508
|
- "Add Linear to my job tracker"
|
|
426
509
|
- "Track this company: [name], [role], [url]"
|
|
427
510
|
- "Add a Tier 1 target: Neon"
|
|
428
|
-
Auto-generates a unique ID (e.g. LIN-001) and sets date_added + last_activity_date to today
|
|
511
|
+
Auto-generates a unique ID (e.g. LIN-001) and sets date_added + last_activity_date to today.
|
|
512
|
+
IMPORTANT: Before calling this, check tracker_list_jobs to ensure the company+role is not already tracked.
|
|
513
|
+
IMPORTANT: Provide a careers_url. The tool will verify the URL is live before writing.
|
|
514
|
+
IMPORTANT: The tool auto-validates: dead URLs and hallucinated company names are BLOCKED.`,
|
|
429
515
|
parameters: {
|
|
430
516
|
type: Type.OBJECT,
|
|
431
517
|
properties: {
|
|
@@ -449,10 +535,35 @@ Auto-generates a unique ID (e.g. LIN-001) and sets date_added + last_activity_da
|
|
|
449
535
|
execute: async (args) => {
|
|
450
536
|
try {
|
|
451
537
|
const { rows, path } = loadCsv();
|
|
538
|
+
// ── Fix 1: Duplicate detection (company + role must be unique) ──────
|
|
539
|
+
const duplicate = rows.find((r) => r.company.toLowerCase() === args.company.toLowerCase() &&
|
|
540
|
+
(r.role ?? "").toLowerCase() === (args.role ?? "").toLowerCase());
|
|
541
|
+
if (duplicate) {
|
|
542
|
+
return (`⚠️ Duplicate detected: "${args.company} — ${args.role ?? "TBD"}" already exists (ID: ${duplicate.id}).\n` +
|
|
543
|
+
`Use tracker_update_job with job_id="${duplicate.id}" to update it instead.`);
|
|
544
|
+
}
|
|
452
545
|
const prefix = args.company.toUpperCase().replace(/[^A-Z]/g, "").slice(0, 3) || "JOB";
|
|
453
546
|
const existing = rows.filter((r) => r.id.startsWith(prefix + "-")).length;
|
|
454
547
|
const id = `${prefix}-${String(existing + 1).padStart(3, "0")}`;
|
|
455
548
|
const todayStr = today();
|
|
549
|
+
// ── Validation harness (URL + company name) ────────────────────────
|
|
550
|
+
const validation = await validateJobEntry(args.company, args.careers_url);
|
|
551
|
+
if (!validation.ok) {
|
|
552
|
+
return (`❌ Cannot add "${args.company}" — validation failed:\n` +
|
|
553
|
+
validation.blockers.map(b => ` • ${b}`).join("\n") +
|
|
554
|
+
`\n\nFix these issues and try again, or ask the user to confirm the company is real.`);
|
|
555
|
+
}
|
|
556
|
+
// ── Fix 2: No URL = unverified origin → cap attention, add warning ──
|
|
557
|
+
const hasUrl = !!(args.careers_url && args.careers_url.startsWith("http"));
|
|
558
|
+
const cappedAttention = hasUrl
|
|
559
|
+
? Math.min(10, args.attention_score ?? 7)
|
|
560
|
+
: Math.min(4, args.attention_score ?? 4);
|
|
561
|
+
const unverifiedNote = hasUrl ? "" : "⚠️ No verified URL — confirm role exists before applying.";
|
|
562
|
+
// ── Fix 4: Salary fields must be numeric only ─────────────────────
|
|
563
|
+
const parseSalary = (v) => (v && !isNaN(v) ? String(Math.round(v)) : "");
|
|
564
|
+
const baseNotes = (args.notes ?? "").replace(/,/g, ";");
|
|
565
|
+
const warningNotes = validation.warnings.map(w => `⚠️ ${w}`).join("; ");
|
|
566
|
+
const combinedNotes = [baseNotes, unverifiedNote, warningNotes].filter(Boolean).join("; ").slice(0, 500);
|
|
456
567
|
const newRow = {
|
|
457
568
|
id,
|
|
458
569
|
company: args.company,
|
|
@@ -460,19 +571,19 @@ Auto-generates a unique ID (e.g. LIN-001) and sets date_added + last_activity_da
|
|
|
460
571
|
tier: String(args.tier ?? 2),
|
|
461
572
|
careers_url: args.careers_url ?? "",
|
|
462
573
|
ats: args.ats ?? "Direct",
|
|
463
|
-
status: "To Apply",
|
|
574
|
+
status: "To Apply", // Fix 3: always To Apply — never Applied at creation
|
|
464
575
|
date_added: todayStr,
|
|
465
576
|
date_applied: "",
|
|
466
577
|
follow_up_date: "",
|
|
467
578
|
contact: "",
|
|
468
579
|
contact_email: "",
|
|
469
|
-
salary_min:
|
|
470
|
-
salary_max:
|
|
580
|
+
salary_min: parseSalary(args.salary_min),
|
|
581
|
+
salary_max: parseSalary(args.salary_max),
|
|
471
582
|
location: args.location ?? "Remote",
|
|
472
|
-
notes:
|
|
583
|
+
notes: combinedNotes,
|
|
473
584
|
fit_score: String(args.fit_score ?? 7),
|
|
474
585
|
referral: "false",
|
|
475
|
-
attention_score: String(
|
|
586
|
+
attention_score: String(cappedAttention),
|
|
476
587
|
apply_effort: args.apply_effort ?? "Medium",
|
|
477
588
|
prep_time_hours: "2",
|
|
478
589
|
excitement: String(Math.min(10, args.excitement ?? 7)),
|
|
@@ -487,19 +598,25 @@ Auto-generates a unique ID (e.g. LIN-001) and sets date_added + last_activity_da
|
|
|
487
598
|
? `$${args.salary_min.toLocaleString()}–$${args.salary_max.toLocaleString()}`
|
|
488
599
|
: "Not specified";
|
|
489
600
|
const priority = priorityScore(newRow).toFixed(1);
|
|
601
|
+
const urlBadge = hasUrl ? "✅ Verified" : "⚠️ Unverified";
|
|
602
|
+
const warningSection = validation.warnings.length > 0
|
|
603
|
+
? `\n Warnings:\n` + validation.warnings.map(w => ` ⚠️ ${w}`).join("\n")
|
|
604
|
+
: "";
|
|
490
605
|
return (`✅ Added to jobs.csv!\n\n` +
|
|
491
606
|
` ID: ${id}\n` +
|
|
492
607
|
` Company: ${args.company}\n` +
|
|
493
|
-
` Role: ${args.role}\n` +
|
|
608
|
+
` Role: ${args.role ?? "TBD"}\n` +
|
|
494
609
|
` Tier: ${args.tier ?? 2}\n` +
|
|
495
610
|
` ATS: ${args.ats ?? "Direct"}\n` +
|
|
496
611
|
` Location: ${args.location ?? "Remote"}\n` +
|
|
497
612
|
` Salary: ${salary}\n` +
|
|
498
|
-
` Attention: ${
|
|
613
|
+
` Attention: ${cappedAttention}/10\n` +
|
|
499
614
|
` Excitement: ${args.excitement ?? 7}/10\n` +
|
|
500
615
|
` Apply Effort: ${args.apply_effort ?? "Medium"}\n` +
|
|
501
616
|
` Priority Score: ${priority}/10\n` +
|
|
502
|
-
` Status: To Apply\n`
|
|
617
|
+
` Status: To Apply\n` +
|
|
618
|
+
` URL: ${args.careers_url || "None"} [${urlBadge}]\n` +
|
|
619
|
+
warningSection);
|
|
503
620
|
}
|
|
504
621
|
catch (err) {
|
|
505
622
|
return `❌ Error adding to jobs.csv: ${err.message}`;
|
|
@@ -510,9 +627,9 @@ Auto-generates a unique ID (e.g. LIN-001) and sets date_added + last_activity_da
|
|
|
510
627
|
// Tool: score_pipeline
|
|
511
628
|
// ---------------------------------------------------------------------------
|
|
512
629
|
export const ScorePipelineTool = {
|
|
513
|
-
name: "
|
|
514
|
-
description: `
|
|
515
|
-
|
|
630
|
+
name: "tracker_rank_priority",
|
|
631
|
+
description: `Rank the jobs.csv pipeline by weighted priority score.
|
|
632
|
+
Formula: 40% attention_score + 30% excitement + 20% fit_score + 10% recency_bonus
|
|
516
633
|
Use this when the user asks: "What should I work on today?", "Which jobs are highest priority?",
|
|
517
634
|
"What's my best ROI right now?", "Rank my pipeline", "Easy wins first".
|
|
518
635
|
Also returns quick-apply opportunities (Low effort + high score) separately.`,
|
|
@@ -596,12 +713,12 @@ Also returns quick-apply opportunities (Low effort + high score) separately.`,
|
|
|
596
713
|
// Tool: get_pipeline_metrics
|
|
597
714
|
// ---------------------------------------------------------------------------
|
|
598
715
|
export const GetPipelineMetricsTool = {
|
|
599
|
-
name: "
|
|
600
|
-
description: `Return comprehensive analytics
|
|
601
|
-
Includes: apply
|
|
716
|
+
name: "tracker_dashboard",
|
|
717
|
+
description: `Return comprehensive analytics dashboard for the jobs.csv pipeline.
|
|
718
|
+
Includes: apply rate, avg attention/excitement/fit, ATS breakdown, salary stats,
|
|
602
719
|
stale company count, estimated total prep time, and a smart recommendation.
|
|
603
720
|
Use this when the user asks: "How is my job search going?", "Give me pipeline stats",
|
|
604
|
-
"What's my apply rate?", "Dashboard view of my search".`,
|
|
721
|
+
"What's my apply rate?", "Dashboard view of my search", "pipeline analytics".`,
|
|
605
722
|
parameters: {
|
|
606
723
|
type: Type.OBJECT,
|
|
607
724
|
properties: {
|
|
@@ -706,11 +823,11 @@ Use this when the user asks: "How is my job search going?", "Give me pipeline st
|
|
|
706
823
|
// Tool: flag_stale_jobs
|
|
707
824
|
// ---------------------------------------------------------------------------
|
|
708
825
|
export const FlagStaleJobsTool = {
|
|
709
|
-
name: "
|
|
710
|
-
description: `
|
|
826
|
+
name: "tracker_find_stale",
|
|
827
|
+
description: `Find jobs in jobs.csv with no recent activity and suggest next actions.
|
|
711
828
|
Use this when the user asks: "What jobs am I neglecting?", "Anything going stale?",
|
|
712
|
-
"Which companies need attention?", "Clean up my pipeline".
|
|
713
|
-
|
|
829
|
+
"Which companies need attention?", "Clean up my pipeline", "what's cold?".
|
|
830
|
+
Read-only — returns a prioritized action list: Apply Now / Follow Up / Deprioritize.`,
|
|
714
831
|
parameters: {
|
|
715
832
|
type: Type.OBJECT,
|
|
716
833
|
properties: {
|
|
@@ -773,6 +890,75 @@ Returns a prioritized action list: Apply Now / Follow Up / Deprioritize?.`,
|
|
|
773
890
|
},
|
|
774
891
|
};
|
|
775
892
|
// ---------------------------------------------------------------------------
|
|
893
|
+
// Tool: tracker_inspect_quality
|
|
894
|
+
// ---------------------------------------------------------------------------
|
|
895
|
+
export const InspectQualityTool = {
|
|
896
|
+
name: "tracker_inspect_quality",
|
|
897
|
+
description: `Scan jobs.csv for data quality issues without modifying anything.
|
|
898
|
+
Detects and reports: duplicate company+role entries, rows with no careers_url (unverified/hallucinated risk),
|
|
899
|
+
rows where salary fields contain non-numeric text (corruption), and notes fields over 400 chars.
|
|
900
|
+
Use this when the user asks: "clean up my tracker", "find duplicates", "audit my pipeline", "any bad data?".`,
|
|
901
|
+
parameters: {
|
|
902
|
+
type: Type.OBJECT,
|
|
903
|
+
properties: {
|
|
904
|
+
_run: { type: Type.BOOLEAN, description: "Always set to true to run the tool." },
|
|
905
|
+
},
|
|
906
|
+
required: ["_run"],
|
|
907
|
+
},
|
|
908
|
+
execute: async () => {
|
|
909
|
+
try {
|
|
910
|
+
const { rows, path } = loadCsv();
|
|
911
|
+
const issues = [];
|
|
912
|
+
// 1. Duplicates by company+role
|
|
913
|
+
const seen = new Map();
|
|
914
|
+
for (const r of rows) {
|
|
915
|
+
const key = `${r.company.toLowerCase()}|${r.role.toLowerCase()}`;
|
|
916
|
+
if (seen.has(key)) {
|
|
917
|
+
issues.push(` 🔁 Duplicate: [${r.id}] ${r.company} — ${r.role} (first seen: ${seen.get(key)})`);
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
seen.set(key, r.id);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
// 2. Missing careers_url
|
|
924
|
+
const noUrl = rows.filter((r) => !r.careers_url || !r.careers_url.startsWith("http"));
|
|
925
|
+
if (noUrl.length > 0) {
|
|
926
|
+
issues.push(`\n ⚠️ ${noUrl.length} rows have no verified careers_url:`);
|
|
927
|
+
noUrl.slice(0, 10).forEach((r) => issues.push(` [${r.id}] ${r.company} — ${r.role}`));
|
|
928
|
+
if (noUrl.length > 10)
|
|
929
|
+
issues.push(` ... and ${noUrl.length - 10} more.`);
|
|
930
|
+
}
|
|
931
|
+
// 3. Salary fields containing non-numeric text
|
|
932
|
+
const badSalary = rows.filter((r) => (r.salary_min && isNaN(Number(r.salary_min))) ||
|
|
933
|
+
(r.salary_max && isNaN(Number(r.salary_max))));
|
|
934
|
+
if (badSalary.length > 0) {
|
|
935
|
+
issues.push(`\n 💸 ${badSalary.length} rows with corrupted salary fields:`);
|
|
936
|
+
badSalary.forEach((r) => issues.push(` [${r.id}] salary_min="${r.salary_min}" salary_max="${r.salary_max}" (first 60 chars)`));
|
|
937
|
+
}
|
|
938
|
+
// 4. Notes overflow
|
|
939
|
+
const longNotes = rows.filter((r) => r.notes && r.notes.length > 400);
|
|
940
|
+
if (longNotes.length > 0) {
|
|
941
|
+
issues.push(`\n 📝 ${longNotes.length} rows with notes > 400 chars:`);
|
|
942
|
+
longNotes.forEach((r) => issues.push(` [${r.id}] ${r.company} (${r.notes.length} chars)`));
|
|
943
|
+
}
|
|
944
|
+
if (issues.length === 0) {
|
|
945
|
+
return `✅ jobs.csv looks clean! ${rows.length} rows, no duplicates, all URLs present, salary fields valid.`;
|
|
946
|
+
}
|
|
947
|
+
return [
|
|
948
|
+
`🔍 Quality Report — ${rows.length} rows in ${path}`,
|
|
949
|
+
"─".repeat(60),
|
|
950
|
+
...issues,
|
|
951
|
+
"─".repeat(60),
|
|
952
|
+
`\nTotal issues found: ${issues.filter(l => l.startsWith(" 🔁")).length} duplicates, ${noUrl.length} missing URLs, ${badSalary.length} bad salaries, ${longNotes.length} long notes.`,
|
|
953
|
+
`\nTo fix: say "remove duplicates from my tracker" or "fix salary fields" and I will present a confirmation before writing.`,
|
|
954
|
+
].join("\n");
|
|
955
|
+
}
|
|
956
|
+
catch (err) {
|
|
957
|
+
return `❌ Error inspecting jobs.csv: ${err.message}`;
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
};
|
|
961
|
+
// ---------------------------------------------------------------------------
|
|
776
962
|
// Export
|
|
777
963
|
// ---------------------------------------------------------------------------
|
|
778
964
|
export const ALL_LOCAL_TRACKER_TOOLS = [
|
|
@@ -782,4 +968,5 @@ export const ALL_LOCAL_TRACKER_TOOLS = [
|
|
|
782
968
|
ScorePipelineTool,
|
|
783
969
|
GetPipelineMetricsTool,
|
|
784
970
|
FlagStaleJobsTool,
|
|
971
|
+
InspectQualityTool,
|
|
785
972
|
];
|
|
@@ -247,7 +247,7 @@ and whether the URL is on a trusted ATS (Ashby, Greenhouse, Lever, etc.).`,
|
|
|
247
247
|
};
|
|
248
248
|
// ── Tool: verify_search_results ──────────────────────────────────────────────
|
|
249
249
|
export const VerifySearchResultsTool = {
|
|
250
|
-
name: "
|
|
250
|
+
name: "verify_job_urls",
|
|
251
251
|
description: `Verify a batch of job URLs returned from search_jobs are all reachable.
|
|
252
252
|
Use this after search_jobs to filter out dead or hallucinated links before showing results to the user.
|
|
253
253
|
Returns a summary of which URLs passed and which failed, so you can present only working links.`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"configurator.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/configurator.ts"],"names":[],"mappings":"AAEA,OAAO,EAA0D,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAI3G,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKpD,CAAC;AAEF,eAAO,MAAM,SAAS;;;;;GAyBrB,CAAC;AAEF,eAAO,MAAM,UAAU;;;;;GA+BtB,CAAC;AAEF,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAsEpD;AAED,wBAAsB,mBAAmB,CAAC,OAAO,GAAE,GAAQ,GAAG,OAAO,CAAC;IACpE,gBAAgB,EAAE,WAAW,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC,
|
|
1
|
+
{"version":3,"file":"configurator.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/configurator.ts"],"names":[],"mappings":"AAEA,OAAO,EAA0D,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAI3G,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKpD,CAAC;AAEF,eAAO,MAAM,SAAS;;;;;GAyBrB,CAAC;AAEF,eAAO,MAAM,UAAU;;;;;GA+BtB,CAAC;AAEF,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAsEpD;AAED,wBAAsB,mBAAmB,CAAC,OAAO,GAAE,GAAQ,GAAG,OAAO,CAAC;IACpE,gBAAgB,EAAE,WAAW,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC,CAiKD"}
|
|
@@ -187,16 +187,26 @@ export async function promptForAgentModel(options = {}) {
|
|
|
187
187
|
chalk.dim("\n Your key will be saved locally — you only need to enter it once."));
|
|
188
188
|
console.log(chalk.dim(`\n Get your key at: `) + chalk.cyan(providerKeyUrls[selectedProvider] ?? "the provider's website"));
|
|
189
189
|
console.log();
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
190
|
+
try {
|
|
191
|
+
const keyAnswer = await prompt({
|
|
192
|
+
type: "password",
|
|
193
|
+
name: "key",
|
|
194
|
+
message: `Enter your ${providerLabels[selectedProvider] ?? selectedProvider} API key:`,
|
|
195
|
+
});
|
|
196
|
+
apiKey = (keyAnswer?.key ?? "").trim();
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// User pressed Escape or Ctrl+C — exit cleanly
|
|
200
|
+
console.log(chalk.dim("\n Cancelled. Run `cv agent` again when you have your API key.\n"));
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
196
203
|
if (apiKey) {
|
|
197
204
|
setProviderKey(selectedProvider, apiKey);
|
|
198
205
|
console.log(chalk.green(`\n✔ Key saved for ${providerLabels[selectedProvider] ?? selectedProvider}\n`));
|
|
199
206
|
}
|
|
207
|
+
else {
|
|
208
|
+
console.log(chalk.yellow("\n⚠️ No key entered. Continuing without API key — the agent may fail.\n"));
|
|
209
|
+
}
|
|
200
210
|
}
|
|
201
211
|
else {
|
|
202
212
|
apiKey = savedKey;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/repl.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,
|
|
1
|
+
{"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/repl.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAA4D,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAI7G,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,GAAE,MAAM,GAAG,IAAW,QAwBtF;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,WAAW,GAAG,sBAAsB,GAAG,IAAI,EACnD,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAC9K,gBAAgB,EAAE,WAAW,EAC7B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,GAAG,EAAE,GACX,OAAO,CAAC,IAAI,CAAC,CAggBf"}
|
|
@@ -4,7 +4,7 @@ import ora from "ora";
|
|
|
4
4
|
import { isSafeCommand } from "../../agent/tools/coding.js";
|
|
5
5
|
import { CareerVividProxyEngine } from "../../agent/CareerVividProxyEngine.js";
|
|
6
6
|
import { CV_MODELS } from "./configurator.js";
|
|
7
|
-
import { loadConfig,
|
|
7
|
+
import { loadConfig, getProviderKey, setProviderKey } from "../../config.js";
|
|
8
8
|
const { prompt } = pkg;
|
|
9
9
|
export function printCreditStatus(remaining, limit = null) {
|
|
10
10
|
if (remaining === null)
|
|
@@ -203,12 +203,16 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
|
|
|
203
203
|
run_command: "⚙️ Running command...",
|
|
204
204
|
write_file: "✏️ Writing file...",
|
|
205
205
|
patch_file: "✏️ Patching file...",
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
206
|
+
tracker_list_jobs: "📊 Checking job pipeline...",
|
|
207
|
+
tracker_add_job: "➕ Adding job to pipeline...",
|
|
208
|
+
tracker_update_job: "🔄 Updating job record...",
|
|
209
|
+
tracker_rank_priority: "📈 Ranking pipeline...",
|
|
210
|
+
tracker_dashboard: "📊 Fetching pipeline analytics...",
|
|
211
|
+
tracker_find_stale: "🚩 Checking stale jobs...",
|
|
212
|
+
tracker_inspect_quality: "🔍 Inspecting data quality...",
|
|
213
|
+
kanban_add_job: "📌 Saving to Kanban board...",
|
|
214
|
+
kanban_list_jobs: "📋 Loading Kanban board...",
|
|
215
|
+
kanban_update_status: "🔄 Updating Kanban status...",
|
|
212
216
|
list_cover_letters: "📄 Loading cover letters...",
|
|
213
217
|
get_cover_letter: "📄 Reading cover letter...",
|
|
214
218
|
save_cover_letter: "💾 Saving cover letter...",
|
|
@@ -366,8 +370,8 @@ ${label}
|
|
|
366
370
|
sessionTurns++;
|
|
367
371
|
const { createOpenAICompatibleProvider } = await import("../../agent/providers/OpenAIProvider.js");
|
|
368
372
|
const { AnthropicProvider } = await import("../../agent/providers/AnthropicProvider.js");
|
|
369
|
-
const byoApiKey = options["api-key"] || loadConfig().llmApiKey;
|
|
370
|
-
const key = byoApiKey ||
|
|
373
|
+
const byoApiKey = options["api-key"] || getProviderKey(selectedProvider) || loadConfig().llmApiKey;
|
|
374
|
+
const key = byoApiKey || "";
|
|
371
375
|
const baseUrl = options["base-url"] || loadConfig().llmBaseUrl;
|
|
372
376
|
let provider;
|
|
373
377
|
if (selectedProvider === "anthropic") {
|
|
@@ -425,11 +429,71 @@ ${label}
|
|
|
425
429
|
return ask();
|
|
426
430
|
}
|
|
427
431
|
catch (err) {
|
|
428
|
-
|
|
432
|
+
const msg = err?.message ?? "";
|
|
433
|
+
// ── Clean exit on Ctrl+C / enquirer cancel ────────────────────────
|
|
434
|
+
if (!msg || msg.includes("cancelled") || msg.includes("User force closed")) {
|
|
429
435
|
console.log(chalk.gray("\nCancelled. Exiting.\n"));
|
|
430
436
|
process.exit(0);
|
|
431
437
|
}
|
|
432
|
-
|
|
438
|
+
// ── 401 unauthorized — offer key reset ───────────────────────────
|
|
439
|
+
const is401 = msg.includes("401") || msg.toLowerCase().includes("user not found") ||
|
|
440
|
+
msg.toLowerCase().includes("invalid api key") || msg.toLowerCase().includes("unauthorized");
|
|
441
|
+
if (is401 && selectedProvider && selectedProvider !== "careervivid") {
|
|
442
|
+
const providerLabels = {
|
|
443
|
+
openai: "OpenAI", anthropic: "Anthropic",
|
|
444
|
+
gemini: "Gemini", openrouter: "OpenRouter", custom: "Custom",
|
|
445
|
+
};
|
|
446
|
+
const providerKeyUrls = {
|
|
447
|
+
openai: "https://platform.openai.com/api-keys",
|
|
448
|
+
anthropic: "https://console.anthropic.com/settings/keys",
|
|
449
|
+
gemini: "https://aistudio.google.com/app/apikey",
|
|
450
|
+
openrouter: "https://openrouter.ai/settings/keys",
|
|
451
|
+
};
|
|
452
|
+
const label = providerLabels[selectedProvider] ?? selectedProvider;
|
|
453
|
+
console.log();
|
|
454
|
+
console.log(chalk.red(`❌ API key rejected by ${label} (401 Unauthorized).`));
|
|
455
|
+
console.log(chalk.dim(` The saved key may be expired or invalid.`));
|
|
456
|
+
if (providerKeyUrls[selectedProvider]) {
|
|
457
|
+
console.log(chalk.dim(` Get a new key at: `) + chalk.cyan(providerKeyUrls[selectedProvider]));
|
|
458
|
+
}
|
|
459
|
+
console.log();
|
|
460
|
+
try {
|
|
461
|
+
const resetAnswer = await prompt({
|
|
462
|
+
type: "select",
|
|
463
|
+
name: "action",
|
|
464
|
+
message: "What would you like to do?",
|
|
465
|
+
choices: [
|
|
466
|
+
{ name: "reset", message: `🔑 Enter a new ${label} API key` },
|
|
467
|
+
{ name: "continue", message: "⏭️ Continue anyway (will keep failing)" },
|
|
468
|
+
{ name: "exit", message: "🚪 Exit the agent" },
|
|
469
|
+
],
|
|
470
|
+
});
|
|
471
|
+
if (resetAnswer.action === "reset") {
|
|
472
|
+
const keyAnswer = await prompt({
|
|
473
|
+
type: "password",
|
|
474
|
+
name: "key",
|
|
475
|
+
message: `Enter your new ${label} API key:`,
|
|
476
|
+
});
|
|
477
|
+
const newKey = (keyAnswer?.key ?? "").trim();
|
|
478
|
+
if (newKey) {
|
|
479
|
+
setProviderKey(selectedProvider, newKey);
|
|
480
|
+
// Update the key used for subsequent turns this session
|
|
481
|
+
options["api-key"] = newKey;
|
|
482
|
+
console.log(chalk.green(`\n✔ New ${label} key saved. Resuming session...\n`));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
else if (resetAnswer.action === "exit") {
|
|
486
|
+
console.log(chalk.gray("\nGoodbye! 👋\n"));
|
|
487
|
+
process.exit(0);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
// User cancelled the reset prompt — just continue
|
|
492
|
+
}
|
|
493
|
+
return ask();
|
|
494
|
+
}
|
|
495
|
+
// ── Generic error ────────────────────────────────────────────────
|
|
496
|
+
console.error(chalk.red(`\nAgent encountered an error: ${msg}`));
|
|
433
497
|
return ask();
|
|
434
498
|
}
|
|
435
499
|
};
|
package/dist/eval/runner.d.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* conversation history never bleeds between test cases.
|
|
8
8
|
*
|
|
9
9
|
* 2. SAFE TOOL EXECUTION DURING EVAL:
|
|
10
|
-
* - READ tools: auto-approved (
|
|
11
|
-
* - WRITE tools (
|
|
10
|
+
* - READ tools: auto-approved (tracker_list_jobs, get_resume, search_jobs, etc.)
|
|
11
|
+
* - WRITE tools (tracker_add_job, tracker_update_job): auto-denied by default.
|
|
12
12
|
* Tests marked write-op use a TEMP COPY of jobs.csv so they can test
|
|
13
13
|
* write operations safely without modifying the real CSV.
|
|
14
14
|
*
|
package/dist/eval/runner.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* conversation history never bleeds between test cases.
|
|
8
8
|
*
|
|
9
9
|
* 2. SAFE TOOL EXECUTION DURING EVAL:
|
|
10
|
-
* - READ tools: auto-approved (
|
|
11
|
-
* - WRITE tools (
|
|
10
|
+
* - READ tools: auto-approved (tracker_list_jobs, get_resume, search_jobs, etc.)
|
|
11
|
+
* - WRITE tools (tracker_add_job, tracker_update_job): auto-denied by default.
|
|
12
12
|
* Tests marked write-op use a TEMP COPY of jobs.csv so they can test
|
|
13
13
|
* write operations safely without modifying the real CSV.
|
|
14
14
|
*
|
|
@@ -50,8 +50,8 @@ function getToolsForMode(mode) {
|
|
|
50
50
|
return ALL_JOB_TOOLS.filter((t) => ["get_resume", "tailor_resume", "list_resumes"].includes(t.name));
|
|
51
51
|
}
|
|
52
52
|
if (mode === "jobs") {
|
|
53
|
-
const jobTools = ALL_JOB_TOOLS.filter((t) => ["get_resume", "search_jobs", "
|
|
54
|
-
// All local tracker tools: list, update, add,
|
|
53
|
+
const jobTools = ALL_JOB_TOOLS.filter((t) => ["get_resume", "search_jobs", "kanban_add_job", "kanban_list_jobs", "kanban_update_status"].includes(t.name));
|
|
54
|
+
// All local tracker tools: list, update, add, tracker_rank_priority, tracker_dashboard, tracker_find_stale
|
|
55
55
|
return [...jobTools, ...ALL_LOCAL_TRACKER_TOOLS];
|
|
56
56
|
}
|
|
57
57
|
return []; // base mode: no domain-specific tools injected
|
|
@@ -202,11 +202,11 @@ export class AgentEvalRunner {
|
|
|
202
202
|
const onToolCall = async (toolName, _args) => {
|
|
203
203
|
toolsCalled.push(toolName);
|
|
204
204
|
// Write tools that mutate jobs.csv — deny on read-only tests
|
|
205
|
-
const isWriteTool = ["
|
|
205
|
+
const isWriteTool = ["tracker_update_job", "tracker_add_job"].includes(toolName);
|
|
206
206
|
if (isWriteTool && !isWriteOp)
|
|
207
207
|
return false;
|
|
208
|
-
// All read tools (
|
|
209
|
-
// get_resume, search_jobs,
|
|
208
|
+
// All read tools (tracker_list_jobs, tracker_rank_priority, tracker_dashboard, tracker_find_stale,
|
|
209
|
+
// get_resume, search_jobs, kanban_list_jobs) are auto-approved
|
|
210
210
|
};
|
|
211
211
|
for (const turn of tc.turns) {
|
|
212
212
|
const t0 = Date.now();
|
|
@@ -20,14 +20,14 @@ export const JOBS_AGENT_TESTS = [
|
|
|
20
20
|
turns: [
|
|
21
21
|
{
|
|
22
22
|
prompt: "Check my local tracker and show me all the companies I'm currently tracking — grouped by tier and status.",
|
|
23
|
-
expectTools: ["
|
|
24
|
-
forbidTools: ["
|
|
23
|
+
expectTools: ["tracker_list_jobs"],
|
|
24
|
+
forbidTools: ["tracker_update_job", "tracker_add_job"],
|
|
25
25
|
expectedKeywords: ["tier", "status", "company"],
|
|
26
26
|
},
|
|
27
27
|
],
|
|
28
28
|
rubric: {
|
|
29
29
|
intent: "User wants to see all companies they're tracking in jobs.csv.",
|
|
30
|
-
goodResponse: "Calls
|
|
30
|
+
goodResponse: "Calls tracker_list_jobs with no filters and presents a clear, well-formatted summary grouped by tier and status, showing attention scores, excitement, effort and pipeline summary counts.",
|
|
31
31
|
badResponse: "Calls a different tool, returns unstructured data, or fabricates company data.",
|
|
32
32
|
},
|
|
33
33
|
},
|
|
@@ -39,7 +39,7 @@ export const JOBS_AGENT_TESTS = [
|
|
|
39
39
|
turns: [
|
|
40
40
|
{
|
|
41
41
|
prompt: "I want to add Linear to my job tracker. It's a Tier 1 target for a Senior Software Engineer role. Their careers page is at https://linear.app/careers. They use Ashby ATS.",
|
|
42
|
-
expectTools: ["
|
|
42
|
+
expectTools: ["tracker_add_job"],
|
|
43
43
|
expectedKeywords: ["linear", "added", "tier", "LIN"],
|
|
44
44
|
},
|
|
45
45
|
{
|
|
@@ -49,7 +49,7 @@ export const JOBS_AGENT_TESTS = [
|
|
|
49
49
|
],
|
|
50
50
|
rubric: {
|
|
51
51
|
intent: "User wants to add a new Tier 1 company and recall the auto-generated ID.",
|
|
52
|
-
goodResponse: "Calls
|
|
52
|
+
goodResponse: "Calls tracker_add_job with all provided details, reports auto-generated ID (LIN-001), and recalls it in turn 2.",
|
|
53
53
|
badResponse: "Fails to call tool, misses ATS/tier, or can't recall ID in turn 2.",
|
|
54
54
|
},
|
|
55
55
|
},
|
|
@@ -61,18 +61,18 @@ export const JOBS_AGENT_TESTS = [
|
|
|
61
61
|
turns: [
|
|
62
62
|
{
|
|
63
63
|
prompt: "Show me all jobs I haven't applied to yet.",
|
|
64
|
-
expectTools: ["
|
|
65
|
-
forbidTools: ["
|
|
64
|
+
expectTools: ["tracker_list_jobs"],
|
|
65
|
+
forbidTools: ["tracker_update_job"],
|
|
66
66
|
expectedKeywords: ["to apply", "status"],
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
prompt: "Pick the first Tier 1 company in the list and mark it as Applied. Set today as the application date.",
|
|
70
|
-
expectTools: ["
|
|
70
|
+
expectTools: ["tracker_update_job"],
|
|
71
71
|
expectedKeywords: ["applied", "updated", "date"],
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
prompt: "Set a follow-up reminder for that same company in 5 days from today.",
|
|
75
|
-
expectTools: ["
|
|
75
|
+
expectTools: ["tracker_update_job"],
|
|
76
76
|
expectedKeywords: ["follow", "updated"],
|
|
77
77
|
},
|
|
78
78
|
],
|
|
@@ -140,16 +140,16 @@ export const JOBS_AGENT_TESTS = [
|
|
|
140
140
|
},
|
|
141
141
|
{
|
|
142
142
|
prompt: "For each of those 3 companies, pull up what I have in my local tracker if anything, and tell me the current status.",
|
|
143
|
-
expectTools: ["
|
|
143
|
+
expectTools: ["tracker_list_jobs"],
|
|
144
144
|
},
|
|
145
145
|
{
|
|
146
146
|
prompt: "Of those I'm not tracking yet, which one should I add first and why?",
|
|
147
|
-
forbidTools: ["
|
|
147
|
+
forbidTools: ["tracker_add_job"],
|
|
148
148
|
expectedKeywords: ["recommend", "add", "because", "first"],
|
|
149
149
|
},
|
|
150
150
|
{
|
|
151
151
|
prompt: "OK, go ahead and add that company to my tracker as a Tier 1 target.",
|
|
152
|
-
expectTools: ["
|
|
152
|
+
expectTools: ["tracker_add_job"],
|
|
153
153
|
expectedKeywords: ["added", "tier"],
|
|
154
154
|
},
|
|
155
155
|
],
|
|
@@ -162,58 +162,58 @@ export const JOBS_AGENT_TESTS = [
|
|
|
162
162
|
// ── NEW v2 tests (attention matrix + new tools) ───────────────────────────
|
|
163
163
|
{
|
|
164
164
|
id: "JOBS-007",
|
|
165
|
-
name: "Priority Pipeline Ranking (
|
|
165
|
+
name: "Priority Pipeline Ranking (tracker_rank_priority)",
|
|
166
166
|
agentMode: "jobs",
|
|
167
167
|
tags: ["single-turn", "tool-use", "attention-matrix"],
|
|
168
168
|
turns: [
|
|
169
169
|
{
|
|
170
170
|
prompt: "Run a priority ranking of my tracker data and tell me what to work on today. I want to see the quick apply opportunities at the top.",
|
|
171
|
-
expectTools: ["
|
|
172
|
-
forbidTools: ["
|
|
171
|
+
expectTools: ["tracker_rank_priority"],
|
|
172
|
+
forbidTools: ["tracker_update_job", "tracker_add_job"],
|
|
173
173
|
expectedKeywords: ["priority", "effort", "apply"],
|
|
174
174
|
},
|
|
175
175
|
],
|
|
176
176
|
rubric: {
|
|
177
177
|
intent: "User wants an attention-weighted ranked view of their pipeline to decide what to do next.",
|
|
178
|
-
goodResponse: "Calls
|
|
179
|
-
badResponse: "Uses
|
|
178
|
+
goodResponse: "Calls tracker_rank_priority, presents the ranked list with priority scores, highlights Low-effort quick-apply opportunities separately, and makes a concrete recommendation.",
|
|
179
|
+
badResponse: "Uses tracker_list_jobs without sorting, doesn't call tracker_rank_priority, or fails to highlight quick-apply opportunities.",
|
|
180
180
|
},
|
|
181
181
|
},
|
|
182
182
|
{
|
|
183
183
|
id: "JOBS-008",
|
|
184
|
-
name: "Pipeline Analytics Dashboard (
|
|
184
|
+
name: "Pipeline Analytics Dashboard (tracker_dashboard)",
|
|
185
185
|
agentMode: "jobs",
|
|
186
186
|
tags: ["single-turn", "tool-use", "analytics"],
|
|
187
187
|
turns: [
|
|
188
188
|
{
|
|
189
189
|
prompt: "Pull up my pipeline stats. I want the full analytics dashboard — apply rate, average attention scores, how many are stale, and a recommendation.",
|
|
190
|
-
expectTools: ["
|
|
191
|
-
forbidTools: ["
|
|
190
|
+
expectTools: ["tracker_dashboard"],
|
|
191
|
+
forbidTools: ["tracker_update_job", "tracker_add_job"],
|
|
192
192
|
expectedKeywords: ["apply rate", "attention", "stale", "recommend"],
|
|
193
193
|
},
|
|
194
194
|
],
|
|
195
195
|
rubric: {
|
|
196
196
|
intent: "User wants a comprehensive analytics view of their job search pipeline health.",
|
|
197
|
-
goodResponse: "Calls
|
|
198
|
-
badResponse: "Uses
|
|
197
|
+
goodResponse: "Calls tracker_dashboard, surfaces at minimum: apply rate %, avg attention/excitement/fit scores, stale count, ATS breakdown, and a smart recommendation. Formats results clearly.",
|
|
198
|
+
badResponse: "Uses tracker_list_jobs and manually counts, doesn't mention stale jobs or averages, or provides no recommendation.",
|
|
199
199
|
},
|
|
200
200
|
},
|
|
201
201
|
{
|
|
202
202
|
id: "JOBS-009",
|
|
203
|
-
name: "Stale Job Detection (
|
|
203
|
+
name: "Stale Job Detection (tracker_find_stale)",
|
|
204
204
|
agentMode: "jobs",
|
|
205
205
|
tags: ["single-turn", "tool-use", "attention-matrix"],
|
|
206
206
|
turns: [
|
|
207
207
|
{
|
|
208
208
|
prompt: "Scan my tracker for anything going stale — companies I've been neglecting. Flag them and give me a specific action for each one.",
|
|
209
|
-
expectTools: ["
|
|
210
|
-
forbidTools: ["
|
|
209
|
+
expectTools: ["tracker_find_stale"],
|
|
210
|
+
forbidTools: ["tracker_update_job", "tracker_add_job"],
|
|
211
211
|
expectedKeywords: ["stale", "action", "apply", "follow"],
|
|
212
212
|
},
|
|
213
213
|
],
|
|
214
214
|
rubric: {
|
|
215
215
|
intent: "User wants to surface neglected jobs with per-company action recommendations.",
|
|
216
|
-
goodResponse: "Calls
|
|
216
|
+
goodResponse: "Calls tracker_find_stale, presents stale companies with per-company actions (Apply Now / Follow Up / Deprioritize), and summarizes total counts. Offers next step.",
|
|
217
217
|
badResponse: "Lists all jobs without filtering for staleness, doesn't provide action recommendations, or guesses stale jobs without calling the tool.",
|
|
218
218
|
},
|
|
219
219
|
},
|
|
@@ -225,23 +225,23 @@ export const JOBS_AGENT_TESTS = [
|
|
|
225
225
|
turns: [
|
|
226
226
|
{
|
|
227
227
|
prompt: "Show me my Tier 1 companies sorted by attention score.",
|
|
228
|
-
expectTools: ["
|
|
229
|
-
forbidTools: ["
|
|
228
|
+
expectTools: ["tracker_list_jobs"],
|
|
229
|
+
forbidTools: ["tracker_update_job"],
|
|
230
230
|
},
|
|
231
231
|
{
|
|
232
232
|
prompt: "I'm feeling super energized about Cursor right now. Set my attention score for CUR-001 to 10 and excitement to 10.",
|
|
233
|
-
expectTools: ["
|
|
233
|
+
expectTools: ["tracker_update_job"],
|
|
234
234
|
expectedKeywords: ["updated", "attention", "excitement", "CUR-001"],
|
|
235
235
|
},
|
|
236
236
|
{
|
|
237
237
|
prompt: "Now re-rank my pipeline and tell me what the new top 3 are.",
|
|
238
|
-
expectTools: ["
|
|
238
|
+
expectTools: ["tracker_rank_priority"],
|
|
239
239
|
expectedKeywords: ["Cursor", "priority"],
|
|
240
240
|
},
|
|
241
241
|
],
|
|
242
242
|
rubric: {
|
|
243
243
|
intent: "User updates attention/excitement scores and immediately sees the effect on priority ranking.",
|
|
244
|
-
goodResponse: "Lists Tier 1 companies, updates CUR-001 attention=10 and excitement=10, then calls
|
|
244
|
+
goodResponse: "Lists Tier 1 companies, updates CUR-001 attention=10 and excitement=10, then calls tracker_rank_priority and shows Cursor at or near the top of the ranking.",
|
|
245
245
|
badResponse: "Fails to update scores, updates wrong company, or doesn't re-rank after update.",
|
|
246
246
|
},
|
|
247
247
|
},
|
|
@@ -253,24 +253,24 @@ export const JOBS_AGENT_TESTS = [
|
|
|
253
253
|
turns: [
|
|
254
254
|
{
|
|
255
255
|
prompt: "I have 30 minutes right now. Find me the highest-priority Low-effort job I can apply to immediately.",
|
|
256
|
-
expectTools: ["
|
|
257
|
-
forbidTools: ["
|
|
256
|
+
expectTools: ["tracker_rank_priority"],
|
|
257
|
+
forbidTools: ["tracker_update_job", "tracker_add_job"],
|
|
258
258
|
expectedKeywords: ["low", "effort", "apply"],
|
|
259
259
|
},
|
|
260
260
|
{
|
|
261
261
|
prompt: "Great. Mark that one as Applied right now and set a follow-up for 7 days from today.",
|
|
262
|
-
expectTools: ["
|
|
262
|
+
expectTools: ["tracker_update_job"],
|
|
263
263
|
expectedKeywords: ["applied", "follow"],
|
|
264
264
|
},
|
|
265
265
|
{
|
|
266
266
|
prompt: "What's my apply rate now? And what are my next 3 priorities?",
|
|
267
|
-
expectTools: ["
|
|
267
|
+
expectTools: ["tracker_dashboard"],
|
|
268
268
|
expectedKeywords: ["apply", "rate", "priority"],
|
|
269
269
|
},
|
|
270
270
|
],
|
|
271
271
|
rubric: {
|
|
272
272
|
intent: "Time-constrained apply decision → execution → impact check. Validates end-to-end apply workflow.",
|
|
273
|
-
goodResponse: "Identifies highest-priority Low-effort job from
|
|
273
|
+
goodResponse: "Identifies highest-priority Low-effort job from tracker_rank_priority. Marks it Applied with follow-up in turn 2. Shows updated apply rate and refreshed priority list in turn 3.",
|
|
274
274
|
badResponse: "Picks a medium/high-effort job, fails to set follow-up, or gives stale metrics without re-calling the tool.",
|
|
275
275
|
},
|
|
276
276
|
},
|
|
@@ -291,19 +291,19 @@ export const JOBS_AGENT_TESTS = [
|
|
|
291
291
|
},
|
|
292
292
|
{
|
|
293
293
|
prompt: "The top result looks interesting. Add it to my local tracker as a Tier 2 target with attention_score 8 and excitement 8.",
|
|
294
|
-
expectTools: ["
|
|
294
|
+
expectTools: ["tracker_add_job"],
|
|
295
295
|
expectedKeywords: ["added", "tier", "attention"],
|
|
296
296
|
},
|
|
297
297
|
{
|
|
298
298
|
prompt: "How does this new company rank in my pipeline? Show me where it fits in my priority score.",
|
|
299
|
-
expectTools: ["
|
|
299
|
+
expectTools: ["tracker_rank_priority"],
|
|
300
300
|
expectedKeywords: ["priority", "score"],
|
|
301
301
|
},
|
|
302
302
|
],
|
|
303
303
|
rubric: {
|
|
304
304
|
intent: "Full end-to-end workflow: search → discover → add to tracker → see priority ranking. Tests all major tools in sequence.",
|
|
305
|
-
goodResponse: "Uses resume context to search, adds the top result with specified scores, then shows
|
|
306
|
-
badResponse: "Adds a different company than the top search result, omits attention/excitement scores, or doesn't call
|
|
305
|
+
goodResponse: "Uses resume context to search, adds the top result with specified scores, then shows tracker_rank_priority where the new entry appears in the ranked list.",
|
|
306
|
+
badResponse: "Adds a different company than the top search result, omits attention/excitement scores, or doesn't call tracker_rank_priority to show placement.",
|
|
307
307
|
},
|
|
308
308
|
},
|
|
309
309
|
];
|