careervivid 1.12.27 → 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.
@@ -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,QA2BvB,CAAC;AAMT,eAAO,MAAM,eAAe,QAOpB,CAAC;AAMT,eAAO,MAAM,YAAY,QAoBjB,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,CA0BT"}
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 & Tracker
78
- - **search_jobs** — Search for jobs scored against the user's resume (call get_resume first).
79
- - **save_job** — Save a job to the online Kanban board.
80
- - **list_jobs** — Show the online Kanban board.
81
- - **update_job_status** Move a job through: To Apply → Applied → Interviewing → Offered/Rejected.
82
-
83
- ### Local Pipeline (CSV v2)
84
- - **list_local_jobs** Show the local pipeline (supports tier/status filters and sort_by).
85
- - **add_local_job** Add a new company to the tracker.
86
- - **update_local_job** — Update any field on a job entry.
87
- - **score_pipeline** Priority-ranked view; use for "what next?" questions.
88
- - **get_pipeline_metrics** — Full analytics: apply rate, avg scores, salary data, stale count.
89
- - **flag_stale_jobs** — Surface cold companies with next-action recommendations.
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** — Verify a single link is alive before sharing it.
93
- - **verify_search_results** — Verify all URLs returned by search_jobs before presenting them.
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 — Browser control (appended in --jobs mode)
106
+ // §5 — Autonomous execution harness (appended in --jobs mode)
98
107
  // ---------------------------------------------------------------------------
99
- export const BROWSER_SECTION = `
100
- ## Browser Control
108
+ export const JOBS_HARNESS = `
109
+ ## Autonomy Rules (Non-Negotiable)
101
110
 
102
- - **browser_use_agent** PRIMARY autonomous form-filling agent (pass URL + full resume context)
103
- - browser_navigate, browser_state, browser_click, browser_type, browser_select, browser_scroll, browser_screenshot
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
- NEVER submit a form without explicit user confirmation.
106
- `.trim();
107
- // ---------------------------------------------------------------------------
108
- // §6 Autonomous execution harness (appended in --jobs mode)
109
- // ---------------------------------------------------------------------------
110
- export const JOBS_HARNESS = `
111
- ## Autonomous Execution Directives (Non-Negotiable)
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
- 1. DO NOT ASK FOR PERMISSION before calling mutative tools (add_local_job, update_local_job, etc.) unless the action is destructive.
114
- 2. Fill in missing details with sensible defaults (TBD, today's date, empty strings) rather than stalling for input.
115
- 3. If you catch yourself explaining what you *are going to do* instead of calling the tool STOP and call the tool.
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 | list_local_jobs |
123
- | priority, what next, best ROI | score_pipeline |
124
- | stats, dashboard, apply rate, search health | get_pipeline_metrics |
125
- | stale, cold, neglecting, need attention | flag_stale_jobs |
126
- | adding a company, new job | add_local_job |
127
- | updating status, marking applied, follow-up | update_local_job |
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
- // §7 — Greeting protocol (shared across modes)
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
- // §8 — Assembled system prompts per mode (the public API)
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,IA0F5B,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"}
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"}
@@ -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 that match a given role and location.
16
- The search automatically scores results against the user's CareerVivid resume.
17
- Returns a list of scored job opportunities with AI summaries and missing skills.
18
- Use this when the user asks to "find jobs", "search for roles", "look for positions", etc.`,
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) to include in results. Use 60 for good matches, 80 for excellent matches only.",
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
- return (`Found ${result.total} jobs for "${args.role}"${args.location ? ` in ${args.location}` : ""}.\n` +
83
- `Showing top ${result.jobs.length} results:\n\n${jobList}`);
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: "save_job",
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: "list_jobs",
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: "update_job_status",
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: "apply_to_job",
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;AAwKlC,eAAO,MAAM,iBAAiB,EAAE,IA8H/B,CAAC;AAMF,eAAO,MAAM,kBAAkB,EAAE,IA2HhC,CAAC;AAMF,eAAO,MAAM,eAAe,EAAE,IAmG7B,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,uBAAuB,EAAE,IAAI,EAOzC,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: "list_local_jobs",
176
- description: `Read the local job pipeline from career-ops/data/jobs.csv.
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
- "check my local tracker", "what companies am I targeting?", etc.
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: "update_local_job",
300
- description: `Update any field on a row in career-ops/data/jobs.csv by job ID.
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: auto-set to ${today()}`);
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 + args.notes).replace(/,/g, ";");
398
- changes.push(`notes: appended "${args.notes.substring(0, 60)}"`);
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: "add_local_job",
423
- description: `Add a new company/role row to career-ops/data/jobs.csv.
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: args.salary_min ? String(args.salary_min) : "",
470
- salary_max: args.salary_max ? String(args.salary_max) : "",
580
+ salary_min: parseSalary(args.salary_min),
581
+ salary_max: parseSalary(args.salary_max),
471
582
  location: args.location ?? "Remote",
472
- notes: (args.notes ?? "").replace(/,/g, ";"),
583
+ notes: combinedNotes,
473
584
  fit_score: String(args.fit_score ?? 7),
474
585
  referral: "false",
475
- attention_score: String(Math.min(10, args.attention_score ?? 7)),
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: ${args.attention_score ?? 7}/10\n` +
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: "score_pipeline",
514
- description: `Return the job pipeline ranked by priority score — the attention-weighted formula:
515
- Priority = 40% attention_score + 30% excitement + 20% fit_score + 10% staleness_bonus
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: "get_pipeline_metrics",
600
- description: `Return comprehensive analytics about the job search pipeline.
601
- Includes: apply velocity, avg attention/excitement/fit, ATS breakdown, salary stats,
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: "flag_stale_jobs",
710
- description: `Identify jobs with no recent activity and suggest next actions.
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
- Returns a prioritized action list: Apply Now / Follow Up / Deprioritize?.`,
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: "verify_search_results",
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":"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,CA4ff"}
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"}
@@ -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
- list_local_jobs: "📊 Checking job pipeline...",
207
- add_local_job: "➕ Adding job to pipeline...",
208
- update_local_job: "🔄 Updating job record...",
209
- score_pipeline: "📈 Scoring pipeline...",
210
- get_pipeline_metrics: "📊 Fetching pipeline metrics...",
211
- flag_stale_jobs: "🚩 Checking stale jobs...",
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...",
@@ -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 (list_local_jobs, get_resume, search_jobs, etc.)
11
- * - WRITE tools (add_local_job, update_local_job): auto-denied by default.
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
  *
@@ -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 (list_local_jobs, get_resume, search_jobs, etc.)
11
- * - WRITE tools (add_local_job, update_local_job): auto-denied by default.
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", "save_job", "list_jobs", "update_job_status"].includes(t.name));
54
- // All local tracker tools: list, update, add, score_pipeline, get_pipeline_metrics, flag_stale_jobs
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 = ["update_local_job", "add_local_job"].includes(toolName);
205
+ const isWriteTool = ["tracker_update_job", "tracker_add_job"].includes(toolName);
206
206
  if (isWriteTool && !isWriteOp)
207
207
  return false;
208
- // All read tools (list_local_jobs, score_pipeline, get_pipeline_metrics, flag_stale_jobs,
209
- // get_resume, search_jobs, list_jobs) are auto-approved
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: ["list_local_jobs"],
24
- forbidTools: ["update_local_job", "add_local_job"],
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 list_local_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.",
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: ["add_local_job"],
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 add_local_job with all provided details, reports auto-generated ID (LIN-001), and recalls it in turn 2.",
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: ["list_local_jobs"],
65
- forbidTools: ["update_local_job"],
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: ["update_local_job"],
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: ["update_local_job"],
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: ["list_local_jobs"],
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: ["add_local_job"],
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: ["add_local_job"],
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 (score_pipeline)",
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: ["score_pipeline"],
172
- forbidTools: ["update_local_job", "add_local_job"],
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 score_pipeline, presents the ranked list with priority scores, highlights Low-effort quick-apply opportunities separately, and makes a concrete recommendation.",
179
- badResponse: "Uses list_local_jobs without sorting, doesn't call score_pipeline, or fails to highlight quick-apply opportunities.",
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 (get_pipeline_metrics)",
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: ["get_pipeline_metrics"],
191
- forbidTools: ["update_local_job", "add_local_job"],
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 get_pipeline_metrics, surfaces at minimum: apply rate %, avg attention/excitement/fit scores, stale count, ATS breakdown, and a smart recommendation. Formats results clearly.",
198
- badResponse: "Uses list_local_jobs and manually counts, doesn't mention stale jobs or averages, or provides no recommendation.",
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 (flag_stale_jobs)",
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: ["flag_stale_jobs"],
210
- forbidTools: ["update_local_job", "add_local_job"],
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 flag_stale_jobs, presents stale companies with per-company actions (Apply Now / Follow Up / Deprioritize), and summarizes total counts. Offers next step.",
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: ["list_local_jobs"],
229
- forbidTools: ["update_local_job"],
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: ["update_local_job"],
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: ["score_pipeline"],
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 score_pipeline and shows Cursor at or near the top of the ranking.",
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: ["score_pipeline"],
257
- forbidTools: ["update_local_job", "add_local_job"],
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: ["update_local_job"],
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: ["get_pipeline_metrics"],
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 score_pipeline. Marks it Applied with follow-up in turn 2. Shows updated apply rate and refreshed priority list in turn 3.",
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: ["add_local_job"],
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: ["score_pipeline"],
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 score_pipeline 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 score_pipeline to show placement.",
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
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "careervivid",
3
- "version": "1.12.27",
3
+ "version": "1.12.31",
4
4
  "description": "Official CLI for CareerVivid — publish articles, diagrams, and portfolio updates from your terminal or AI agent",
5
5
  "type": "module",
6
6
  "bin": {