huntpilot 0.0.0-stage → 0.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jawad Ul Hadi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,117 @@
1
- # Temporary Holding Version
1
+ # HuntPilot
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **A referral-first AI job search copilot.** It finds roles, scores your fit, writes tailored resumes and cover letters, asks for a referral before you apply cold, and keeps your tracker up to date. It drafts everything and sends nothing.
4
+
5
+ HuntPilot is a set of agent templates plus a small TypeScript CLI. You fill in one profile file and render paste-ready files for **Microsoft Copilot Studio**, a **Claude Project** or a **custom GPT**.
6
+
7
+ [![CI](https://github.com/JawadulHadi/job_hunt/actions/workflows/ci.yml/badge.svg)](https://github.com/JawadulHadi/job_hunt/actions/workflows/ci.yml)
8
+ ![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)
9
+
10
+ ## What it does
11
+
12
+
13
+ | Mode | What you get |
14
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
15
+ | Find jobs | Fresh roles from company boards (Greenhouse, Lever, Ashby) and remote boards, opened, scored and de-duplicated against your tracker |
16
+ | Analyse a JD | Requirement-by-requirement fit table, a 0–100 score, the resume variant to use, and a verdict: Apply, Referral first or Skip |
17
+ | Hard filters | Skips roles you can't take (work authorization, onsite only, unpaid trials) and quotes the line that blocks you |
18
+ | Tailor | Resume bullets reordered for the role, from your facts only, in an ATS-safe single column |
19
+ | Referral first | Finds a warm route in (maintainers, blog authors, community members) and drafts a 90-word ask |
20
+ | Write | Cover letters capped at 250 words with your one headline metric, word for word |
21
+ | Interview prep | Likely questions, STAR outlines from your real experience, and mock interviews one question at a time |
22
+ | Tracker | Logs and updates an Excel table with stable IDs, statuses and follow-up dates |
23
+
24
+ ## How it fits together
25
+
26
+ ```mermaid
27
+ flowchart LR
28
+ P[profile.yaml<br/>your facts and targets] --> R[huntpilot render]
29
+ T[templates/agent<br/>instructions + 3 skills] --> R
30
+ R --> CS[Copilot Studio files]
31
+ R --> CP[Claude Project files]
32
+ R --> GPT[Custom GPT files]
33
+ R --> C[huntpilot check]
34
+ JD[job description] --> S[huntpilot score] --> V[score, variant, verdict, blockers]
35
+ X[huntpilot tracker] --> XL[JobTracker.xlsx]
36
+ E[evals/cases.yaml] --> EV[huntpilot evals] --> TR[test-run checklist]
37
+ ```
38
+
39
+ The agent does the reading and writing. The CLI keeps the setup consistent: one profile, the same facts on every platform, and a deterministic pre-score you can run on any job description.
40
+
41
+ ## Quick start
42
+
43
+ Requires Node.js 20 or newer.
44
+
45
+ ```bash
46
+ git clone https://github.com/JawadulHadi/job_hunt.git
47
+ cd job_hunt
48
+ npm install
49
+
50
+ cp examples/profile.example.yaml profile.yaml # git-ignored; edit it with your own facts
51
+ npm run render # writes out/<platform>/
52
+ npm run check # unfilled placeholders, banned words, length limits
53
+ npm run tracker # writes out/JobTracker.xlsx
54
+ npm run evals # writes out/test-run.md
55
+ npm run score -- path/to/job-description.txt # pre-score one JD
56
+ ```
57
+
58
+ Then follow the setup guide for your platform:
59
+
60
+ - [Microsoft Copilot Studio](docs/setup-copilot-studio.md)
61
+ - [Claude Project](docs/setup-claude-project.md)
62
+ - [Custom GPT](docs/setup-custom-gpt.md)
63
+
64
+ ## Scoring a job description
65
+
66
+ ```text
67
+ $ npm run score -- tests/fixtures/jd-us-only.txt
68
+ Score: 75/100 Verdict: Skip Variant: C
69
+
70
+ | Area | Points | Matched |
71
+ | --- | --- | --- |
72
+ | Core stack | 30/30 | nestjs, typescript, postgresql |
73
+ | Seniority | 20/20 | staff |
74
+ | AI / LLM | 0/15 | none |
75
+ | Supporting stack | 10/10 | graphql, redis |
76
+ | Remote fit | 15/15 | remote |
77
+ | Company signal | 0/10 | none |
78
+
79
+ Blockers:
80
+ - Requires US work authorization: "Candidates must be authorized to work in the United States."
81
+ ```
82
+
83
+ A 75 would normally be Apply. The work-authorization line turns it into Skip, before you spend time tailoring.
84
+
85
+ The rubric, thresholds and hard filters all live in your profile. See [docs/fit-scoring.md](docs/fit-scoring.md).
86
+
87
+ ## Test before you trust it
88
+
89
+ Run the 14 cases in [evals/cases.yaml](evals/cases.yaml) against your deployed agent with your own resume and real job descriptions. `npm run evals` turns them into a checklist with your details filled in. Go live only when every critical case passes. See [docs/testing.md](docs/testing.md).
90
+
91
+ ## Safety rules built in
92
+
93
+ - Drafts only. It never sends email or messages, and never submits applications.
94
+ - It never overwrites your resume or any file it didn't create.
95
+ - It never invents employers, dates, metrics or certifications. Missing facts go to a "Needs your input" list.
96
+ - It won't guarantee legal, immigration, tax or salary outcomes.
97
+
98
+ **Keep your configured agent private.** It holds your resume and can reach your files and email. Share this template, not your instance. See [docs/publishing.md](docs/publishing.md).
99
+
100
+ ## Repository layout
101
+
102
+ ```text
103
+ templates/agent/ instructions, skills and prompts with {{profile.tokens}}
104
+ examples/ a fictional example profile
105
+ evals/ test cases
106
+ src/ the huntpilot CLI (TypeScript)
107
+ tests/ unit tests (Vitest)
108
+ docs/ setup, scoring, testing and publishing guides
109
+ ```
110
+
111
+ ## Contributing
112
+
113
+ Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).
114
+
115
+ ## License
116
+
117
+ [MIT](LICENSE) © 2026 Jawad Ul Hadi
package/dist/check.js ADDED
@@ -0,0 +1,44 @@
1
+ import { readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ import { mentions } from "./score.js";
4
+ /** Instruction field limits. Both platforms cap instructions at 8,000 characters. */
5
+ export const INSTRUCTION_LIMITS = {
6
+ "copilot-studio": 8000,
7
+ "custom-gpt": 8000,
8
+ };
9
+ function walk(dir) {
10
+ return readdirSync(dir).flatMap((name) => {
11
+ const path = join(dir, name);
12
+ return statSync(path).isDirectory() ? walk(path) : [path];
13
+ });
14
+ }
15
+ export function checkText(file, text, bannedWords) {
16
+ const findings = [];
17
+ const leftovers = text.match(/\{\{\s*[a-zA-Z0-9_.]+\s*\}\}/g);
18
+ if (leftovers) {
19
+ findings.push({ file, level: "error", message: `Unfilled placeholders: ${[...new Set(leftovers)].join(", ")}` });
20
+ }
21
+ const bannedLine = /banned words?:/i;
22
+ // The line that lists the banned words is the one place they're allowed.
23
+ const body = text.split("\n").filter((l) => !bannedLine.test(l)).join("\n");
24
+ const banned = bannedWords.filter((w) => mentions(body, w));
25
+ if (banned.length > 0) {
26
+ findings.push({ file, level: "warning", message: `Uses banned words: ${banned.join(", ")}` });
27
+ }
28
+ return findings;
29
+ }
30
+ /** Checks rendered output under outDir: placeholders, banned words and instruction length per platform. */
31
+ export function checkOutput(outDir, bannedWords) {
32
+ const findings = [];
33
+ for (const path of walk(outDir).filter((p) => p.endsWith(".md"))) {
34
+ const text = readFileSync(path, "utf8");
35
+ const file = relative(outDir, path);
36
+ findings.push(...checkText(file, text, bannedWords));
37
+ const [platform, name] = file.split(/[\\/]/);
38
+ const limit = INSTRUCTION_LIMITS[platform];
39
+ if (name === "instructions.md" && limit && text.length > limit) {
40
+ findings.push({ file, level: "error", message: `Instructions are ${text.length} characters; ${platform} allows ${limit}` });
41
+ }
42
+ }
43
+ return findings;
44
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parseArgs } from "node:util";
6
+ import { checkOutput } from "./check.js";
7
+ import { casesToChecklist, loadCases } from "./evals.js";
8
+ import { loadProfile } from "./profile.js";
9
+ import { loadAgent, PLATFORMS, renderPlatform } from "./render.js";
10
+ import { formatResult, scoreJob } from "./score.js";
11
+ import { buildTracker } from "./tracker.js";
12
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
13
+ const HELP = `HuntPilot: a referral-first job search copilot
14
+
15
+ Usage: huntpilot <command> [options]
16
+
17
+ Commands:
18
+ render Fill the agent templates with your profile, one folder per platform
19
+ check Check rendered files for unfilled placeholders, banned words and length limits
20
+ score Pre-score a job description file against your profile
21
+ tracker Create the Excel tracker (table "Applications", key column "ID")
22
+ evals Write the test checklist for your profile
23
+
24
+ Options:
25
+ -p, --profile <file> Profile YAML (default: profile.yaml, else examples/profile.example.yaml)
26
+ -o, --out <dir> Output folder (default: out)
27
+ --platform <name> ${PLATFORMS.join(" | ")} (render only; default: all)
28
+ --json Print the score as JSON (score only)
29
+ -h, --help Show this help
30
+ `;
31
+ function defaultProfile() {
32
+ try {
33
+ readFileSync("profile.yaml");
34
+ return "profile.yaml";
35
+ }
36
+ catch {
37
+ return join(ROOT, "examples", "profile.example.yaml");
38
+ }
39
+ }
40
+ async function main() {
41
+ const { values, positionals } = parseArgs({
42
+ allowPositionals: true,
43
+ options: {
44
+ profile: { type: "string", short: "p" },
45
+ out: { type: "string", short: "o", default: "out" },
46
+ platform: { type: "string" },
47
+ json: { type: "boolean", default: false },
48
+ help: { type: "boolean", short: "h", default: false },
49
+ },
50
+ });
51
+ const [command, arg] = positionals;
52
+ if (values.help || !command) {
53
+ console.log(HELP);
54
+ return 0;
55
+ }
56
+ const profilePath = values.profile ?? defaultProfile();
57
+ const out = values.out;
58
+ switch (command) {
59
+ case "render": {
60
+ const profile = loadProfile(profilePath);
61
+ const agent = loadAgent(join(ROOT, "templates", "agent"), profile);
62
+ const platforms = values.platform ? [values.platform] : PLATFORMS;
63
+ for (const p of platforms) {
64
+ if (!PLATFORMS.includes(p))
65
+ throw new Error(`Unknown platform "${p}"`);
66
+ for (const file of renderPlatform(agent, p, out))
67
+ console.log(`wrote ${file}`);
68
+ }
69
+ return 0;
70
+ }
71
+ case "check": {
72
+ const profile = loadProfile(profilePath);
73
+ const findings = checkOutput(out, profile.writing.banned_words);
74
+ for (const f of findings)
75
+ console.log(`${f.level.toUpperCase()} ${f.file}: ${f.message}`);
76
+ const errors = findings.filter((f) => f.level === "error").length;
77
+ console.log(errors ? `${errors} error(s)` : "All checks passed");
78
+ return errors ? 1 : 0;
79
+ }
80
+ case "score": {
81
+ if (!arg)
82
+ throw new Error("Give a job description file: huntpilot score jd.txt");
83
+ const result = scoreJob(readFileSync(arg, "utf8"), loadProfile(profilePath));
84
+ console.log(values.json ? JSON.stringify(result, null, 2) : formatResult(result));
85
+ return 0;
86
+ }
87
+ case "tracker": {
88
+ mkdirSync(out, { recursive: true });
89
+ const file = join(out, "JobTracker.xlsx");
90
+ await buildTracker(file);
91
+ console.log(`wrote ${file}`);
92
+ return 0;
93
+ }
94
+ case "evals": {
95
+ const profile = loadProfile(profilePath);
96
+ mkdirSync(out, { recursive: true });
97
+ const file = join(out, "test-run.md");
98
+ writeFileSync(file, casesToChecklist(loadCases(join(ROOT, "evals", "cases.yaml")), profile));
99
+ console.log(`wrote ${file}`);
100
+ return 0;
101
+ }
102
+ default:
103
+ console.error(`Unknown command "${command}"\n`);
104
+ console.log(HELP);
105
+ return 1;
106
+ }
107
+ }
108
+ main().then((code) => process.exit(code), (err) => {
109
+ console.error(`Error: ${err.message}`);
110
+ process.exit(1);
111
+ });
package/dist/evals.js ADDED
@@ -0,0 +1,22 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { parse } from "yaml";
3
+ import { fill } from "./render.js";
4
+ export function loadCases(file) {
5
+ const data = parse(readFileSync(file, "utf8"));
6
+ return data.cases;
7
+ }
8
+ /** A markdown checklist to run by hand against the deployed agent, with the profile filled in. */
9
+ export function casesToChecklist(cases, profile) {
10
+ const rows = cases.map((c) => `| [ ] | ${c.id} | ${c.critical ? "Yes" : ""} | ${fill(c.prompt, profile)} | ${fill(c.pass_when, profile)} |`);
11
+ return [
12
+ "# HuntPilot test run",
13
+ "",
14
+ "Run each prompt in the deployed agent. Tick the box when the answer meets the pass rule.",
15
+ "Go live only when every critical case passes and no more than 2 others fail.",
16
+ "",
17
+ "| Pass | ID | Critical | Prompt | Passes when |",
18
+ "| --- | --- | --- | --- | --- |",
19
+ ...rows,
20
+ "",
21
+ ].join("\n");
22
+ }
@@ -0,0 +1,54 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { parse } from "yaml";
3
+ const REQUIRED_PATHS = [
4
+ "candidate.name",
5
+ "candidate.title",
6
+ "candidate.years",
7
+ "candidate.core_stack",
8
+ "candidate.timezone",
9
+ "candidate.headline_metric",
10
+ "candidate.current_role.employer",
11
+ "targeting.roles",
12
+ "targeting.region",
13
+ "targeting.variants.A.name",
14
+ "targeting.variants.B.name",
15
+ "targeting.variants.C.name",
16
+ "scoring.areas",
17
+ "scoring.thresholds.apply",
18
+ "scoring.thresholds.referral",
19
+ "tracker.path",
20
+ "tracker.table",
21
+ ];
22
+ export function getPath(obj, path) {
23
+ return path.split(".").reduce((node, key) => {
24
+ if (node && typeof node === "object" && key in node) {
25
+ return node[key];
26
+ }
27
+ return undefined;
28
+ }, obj);
29
+ }
30
+ export function validateProfile(raw) {
31
+ const missing = REQUIRED_PATHS.filter((p) => {
32
+ const value = getPath(raw, p);
33
+ return value === undefined || value === null || value === "" || (Array.isArray(value) && value.length === 0);
34
+ });
35
+ if (missing.length > 0) {
36
+ throw new Error(`Profile is missing: ${missing.join(", ")}`);
37
+ }
38
+ const profile = raw;
39
+ const { apply, referral } = profile.scoring.thresholds;
40
+ if (referral > apply) {
41
+ throw new Error("scoring.thresholds.referral must be at or below scoring.thresholds.apply");
42
+ }
43
+ const total = profile.scoring.areas.reduce((sum, a) => sum + a.points, 0);
44
+ if (total !== 100) {
45
+ throw new Error(`scoring.areas points must add up to 100 (they add up to ${total})`);
46
+ }
47
+ profile.scoring.hard_filters ??= [];
48
+ profile.targeting.target_companies ??= [];
49
+ profile.candidate.links ??= [];
50
+ return profile;
51
+ }
52
+ export function loadProfile(file) {
53
+ return validateProfile(parse(readFileSync(file, "utf8")));
54
+ }
package/dist/render.js ADDED
@@ -0,0 +1,53 @@
1
+ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getPath } from "./profile.js";
4
+ export const PLATFORMS = ["copilot-studio", "claude-project", "custom-gpt"];
5
+ const TOKEN = /\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}/g;
6
+ function asText(value) {
7
+ if (value === undefined || value === null)
8
+ return undefined;
9
+ if (Array.isArray(value))
10
+ return value.length ? value.map(String).join(", ") : "none listed";
11
+ return String(value);
12
+ }
13
+ /** Replaces {{dotted.path}} tokens with profile values. Unknown tokens are left in place for `check` to report. */
14
+ export function fill(template, profile) {
15
+ return template.replace(TOKEN, (token, path) => asText(getPath(profile, path)) ?? token);
16
+ }
17
+ export function loadAgent(templateDir, profile) {
18
+ const read = (rel) => fill(readFileSync(join(templateDir, rel), "utf8").replace(/\r\n/g, "\n"), profile);
19
+ const skillDir = join(templateDir, "skills");
20
+ const skills = readdirSync(skillDir)
21
+ .filter((f) => f.endsWith(".md"))
22
+ .sort()
23
+ .map((f) => ({ name: f.replace(/\.md$/, ""), body: read(join("skills", f)) }));
24
+ return { instructions: read("instructions.md"), skills, prompts: read("prompts.md") };
25
+ }
26
+ /** Drops the YAML front matter, for platforms that take skills as plain knowledge text. */
27
+ export function stripFrontMatter(md) {
28
+ return md.replace(/^---\n[\s\S]*?\n---\n+/, "");
29
+ }
30
+ export function renderPlatform(agent, platform, outDir) {
31
+ const dir = join(outDir, platform);
32
+ mkdirSync(dir, { recursive: true });
33
+ const written = [];
34
+ const write = (name, body) => {
35
+ writeFileSync(join(dir, name), body.endsWith("\n") ? body : `${body}\n`);
36
+ written.push(join(dir, name));
37
+ };
38
+ write("instructions.md", agent.instructions);
39
+ write("suggested-prompts.md", agent.prompts);
40
+ if (platform === "copilot-studio") {
41
+ // Copilot Studio skills keep their front matter: name and description decide when they trigger.
42
+ for (const s of agent.skills)
43
+ write(`skill-${s.name}.md`, s.body);
44
+ }
45
+ else {
46
+ // Claude Projects and custom GPTs read skills as one knowledge file.
47
+ const playbook = agent.skills
48
+ .map((s) => stripFrontMatter(s.body).trim())
49
+ .join("\n\n---\n\n");
50
+ write("skills-playbook.md", playbook);
51
+ }
52
+ return written;
53
+ }
package/dist/score.js ADDED
@@ -0,0 +1,67 @@
1
+ const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2
+ /** Whole-word, case-insensitive keyword match. "Node.js" and "C#" style tokens are matched literally. */
3
+ export function mentions(text, keyword) {
4
+ const re = new RegExp(`(^|[^a-z0-9])${escape(keyword.toLowerCase())}($|[^a-z0-9])`, "i");
5
+ return re.test(text.toLowerCase());
6
+ }
7
+ function scoreArea(text, area) {
8
+ const matched = area.keywords.filter((k) => mentions(text, k));
9
+ const share = Math.min(matched.length / Math.max(area.need, 1), 1);
10
+ return { label: area.label, points: Math.round(area.points * share), max: area.points, matched };
11
+ }
12
+ function findBlockers(text, profile) {
13
+ const lines = text.split(/\r?\n/);
14
+ const blockers = [];
15
+ for (const filter of profile.scoring.hard_filters) {
16
+ const re = new RegExp(filter.pattern, "i");
17
+ const line = lines.find((l) => re.test(l));
18
+ if (line)
19
+ blockers.push({ filter: filter.name, line: line.trim() });
20
+ }
21
+ return blockers;
22
+ }
23
+ function pickVariant(text, profile) {
24
+ const { variants } = profile.targeting;
25
+ const ranked = ["A", "B", "C"]
26
+ .map((key) => ({ key, hits: variants[key].keywords.filter((k) => mentions(text, k)).length }))
27
+ .sort((x, y) => y.hits - x.hits);
28
+ // Ties keep the A, B, C order, so the lead variant wins when nothing stands out.
29
+ return ranked[0].key;
30
+ }
31
+ function companyIsTarget(text, profile) {
32
+ return profile.targeting.target_companies.some((c) => mentions(text, c));
33
+ }
34
+ /**
35
+ * A deterministic pre-score for a job description. The agent does the real reading;
36
+ * this gives the same answer every time, so you can compare roles and catch blockers fast.
37
+ */
38
+ export function scoreJob(text, profile) {
39
+ const areas = profile.scoring.areas.map((a) => scoreArea(text, a));
40
+ const score = areas.reduce((sum, a) => sum + a.points, 0);
41
+ const blockers = findBlockers(text, profile);
42
+ const { apply, referral } = profile.scoring.thresholds;
43
+ let verdict;
44
+ if (blockers.length > 0 || score < referral)
45
+ verdict = "Skip";
46
+ else if (score >= apply && !companyIsTarget(text, profile))
47
+ verdict = "Apply";
48
+ else
49
+ verdict = "Referral first";
50
+ return { score, verdict, variant: pickVariant(text, profile), areas, blockers };
51
+ }
52
+ export function formatResult(result) {
53
+ const rows = result.areas.map((a) => `| ${a.label} | ${a.points}/${a.max} | ${a.matched.join(", ") || "none"} |`);
54
+ const blockers = result.blockers.length
55
+ ? result.blockers.map((b) => `- ${b.filter}: "${b.line}"`).join("\n")
56
+ : "- none";
57
+ return [
58
+ `Score: ${result.score}/100 Verdict: ${result.verdict} Variant: ${result.variant}`,
59
+ "",
60
+ "| Area | Points | Matched |",
61
+ "| --- | --- | --- |",
62
+ ...rows,
63
+ "",
64
+ "Blockers:",
65
+ blockers,
66
+ ].join("\n");
67
+ }
@@ -0,0 +1,82 @@
1
+ import ExcelJS from "exceljs";
2
+ export const TRACKER_COLUMNS = [
3
+ "ID",
4
+ "Date Found",
5
+ "Company",
6
+ "Role",
7
+ "Link",
8
+ "Source",
9
+ "Location / Remote",
10
+ "Work Auth Rule",
11
+ "Fit Score",
12
+ "Verdict",
13
+ "Variant",
14
+ "Referral Contact",
15
+ "Referral Asked On",
16
+ "Status",
17
+ "Applied On",
18
+ "Follow-up Date",
19
+ "Last Update",
20
+ "Notes",
21
+ ];
22
+ export const STATUSES = [
23
+ "Found",
24
+ "Referral asked",
25
+ "Applied",
26
+ "Screen",
27
+ "Interview",
28
+ "Offer",
29
+ "Rejected",
30
+ "Withdrawn",
31
+ "Ghosted",
32
+ ];
33
+ const LISTS = {
34
+ Source: ["Manual", "Job board", "Referral", "Recruiter", "HN Who is hiring", "Company site"],
35
+ Verdict: ["Apply", "Referral first", "Skip"],
36
+ Variant: ["A", "B", "C"],
37
+ Status: STATUSES,
38
+ };
39
+ const README = [
40
+ "HuntPilot tracker",
41
+ "Keep this file where your agent's Excel tools point (default /JobSearch/JobTracker.xlsx). Table name: Applications. Key column: ID.",
42
+ "IDs: JS-0001, JS-0002 ... The agent lists rows, reads the highest ID and adds 1.",
43
+ "Verdict: Apply (score at or above your apply threshold), Referral first (between thresholds, or a target company), Skip (below, or a hard filter).",
44
+ "Status: Found > Referral asked > Applied > Screen > Interview > Offer, or Rejected / Withdrawn / Ghosted.",
45
+ "Follow-up Date: Referral asked +5 days; Applied +7 days; Interview +2 days (thank-you note).",
46
+ "Dates are text in YYYY-MM-DD so every connector reads them the same way.",
47
+ "Delete the sample row once your 'log this job' test passes.",
48
+ ];
49
+ export async function buildTracker(file, today = new Date().toISOString().slice(0, 10)) {
50
+ const wb = new ExcelJS.Workbook();
51
+ const ws = wb.addWorksheet("Applications");
52
+ const sample = [
53
+ "JS-0001", today, "Example Co (delete me)", "Senior Backend Engineer", "https://example.com/job",
54
+ "Manual", "Remote - Americas", "Contractor OK", 82, "Referral first", "C", "", "", "Found", "", "", today,
55
+ "Sample row to test the tracker",
56
+ ];
57
+ ws.addTable({
58
+ name: "Applications",
59
+ ref: "A1",
60
+ headerRow: true,
61
+ style: { theme: "TableStyleMedium2", showRowStripes: true },
62
+ columns: TRACKER_COLUMNS.map((name) => ({ name, filterButton: true })),
63
+ rows: [sample],
64
+ });
65
+ TRACKER_COLUMNS.forEach((name, i) => {
66
+ ws.getColumn(i + 1).width = Math.max(10, name.length + 4);
67
+ const list = LISTS[name];
68
+ if (!list)
69
+ return;
70
+ for (let row = 2; row <= 1000; row++) {
71
+ ws.getCell(row, i + 1).dataValidation = {
72
+ type: "list",
73
+ allowBlank: true,
74
+ formulae: [`"${list.join(",")}"`],
75
+ };
76
+ }
77
+ });
78
+ const readme = wb.addWorksheet("Readme");
79
+ README.forEach((line) => readme.addRow([line]));
80
+ readme.getColumn(1).width = 120;
81
+ await wb.xlsx.writeFile(file);
82
+ }
@@ -0,0 +1,43 @@
1
+ # Fit scoring
2
+
3
+ `huntpilot score` gives every job description a 0–100 pre-score, a resume variant and a verdict. It's deterministic: the same job description gets the same answer every time, so you can compare roles side by side. The agent uses the same rubric when it reads a job description in full.
4
+
5
+ ## How the score works
6
+
7
+ Each rubric area in `scoring.areas` has points, keywords and `need`, the number of keyword matches for full points. Fewer matches earn a share of the points.
8
+
9
+ ```text
10
+ area points = points × min(matches / need, 1)
11
+ score = sum of area points
12
+ ```
13
+
14
+ Keywords match whole words, ignoring case, so `redis` doesn't match "rediscover".
15
+
16
+ The example profile uses:
17
+
18
+ | Area | Points | Need |
19
+ | --- | --- | --- |
20
+ | Core stack | 30 | 3 |
21
+ | Seniority | 20 | 1 |
22
+ | AI / LLM | 15 | 2 |
23
+ | Supporting stack | 10 | 2 |
24
+ | Remote fit | 15 | 1 |
25
+ | Company signal | 10 | 2 |
26
+
27
+ Points must add up to 100. The profile won't load if they don't.
28
+
29
+ ## Verdicts
30
+
31
+ | Verdict | When |
32
+ | --- | --- |
33
+ | Skip | Any hard filter matches, or the score is below `thresholds.referral` |
34
+ | Referral first | Score between the thresholds, or at least `thresholds.apply` at a company in `target_companies` |
35
+ | Apply | Score at least `thresholds.apply`, with no blockers |
36
+
37
+ ## Hard filters
38
+
39
+ Each filter is a case-insensitive regular expression tested line by line. The first matching line is quoted back, so you can see exactly why a role was skipped. Edit the filters to match your own situation. If you hold US work authorization, delete that filter.
40
+
41
+ ## Variants
42
+
43
+ The variant with the most keyword matches wins. On a tie, A wins over B, and B wins over C, so your lead variant is the default.
@@ -0,0 +1,29 @@
1
+ # Publishing: private instance, public template
2
+
3
+ HuntPilot has two lives, and they're published differently.
4
+
5
+ | | Your instance | This template |
6
+ | --- | --- | --- |
7
+ | What it is | The agent with your resume, facts and tools | This repository: templates, CLI, docs, tests |
8
+ | Who sees it | Only you | Anyone |
9
+ | Where | Teams / Microsoft 365 Copilot with sign-in, your Claude Project, or a GPT set to "Only me" | GitHub |
10
+ | Personal data | Yes | None. The example profile is fictional |
11
+
12
+ ## Why your instance stays private
13
+
14
+ Your instance can read your resume and, depending on the platform, write to your tracker and draft emails. In Copilot Studio, an agent set to **No authentication** can be used by anyone who has the link and can't run tools that need user sign-in ([Microsoft Learn](https://learn.microsoft.com/microsoft-copilot-studio/publication-fundamentals-publish-channels)). The demo website is for testers only, and Microsoft says not to share its URL with customers ([Microsoft Learn](https://learn.microsoft.com/microsoft-copilot-studio/publication-connect-bot-to-web-channels)).
15
+
16
+ ## Rollout order
17
+
18
+ 1. **Build:** fill in your profile, render, check.
19
+ 2. **Private test:** run the test checklist with your own documents ([testing.md](testing.md)).
20
+ 3. **Pilot:** use it for your own applications for a week. Ask one friend to set it up from this README alone.
21
+ 4. **Publish the template:** make the repository public, pin it, and link it from your profile.
22
+
23
+ ## Before making the repository public
24
+
25
+ - [ ] `git grep` for your name, email, employers and metrics, and confirm none of them appear
26
+ - [ ] `profile.yaml`, `private/` and `knowledge/` are git-ignored and were never committed
27
+ - [ ] CI is green
28
+ - [ ] A friend completed setup from the README in under 30 minutes
29
+ - [ ] The README's safety rules match the templates
@@ -0,0 +1,29 @@
1
+ # Set up HuntPilot as a Claude Project
2
+
3
+ This takes about 10 minutes on any Claude plan that supports Projects.
4
+
5
+ ## 1. Render your files
6
+
7
+ ```bash
8
+ cp examples/profile.example.yaml profile.yaml # then edit it
9
+ npm run render -- --platform claude-project
10
+ npm run check
11
+ ```
12
+
13
+ ## 2. Create the project
14
+
15
+ 1. In Claude, select **Projects → New project** and name it **HuntPilot**.
16
+ 2. Paste `out/claude-project/instructions.md` into the project instructions.
17
+ 3. Add these to project knowledge:
18
+ - `out/claude-project/skills-playbook.md`
19
+ - your resume, base cover letter and `facts.md`
20
+
21
+ ## 3. Tools
22
+
23
+ - Turn on **web search** for job discovery.
24
+ - The tracker: connect a spreadsheet connector if you have one, or keep `JobTracker.xlsx` yourself and ask HuntPilot for the row to paste in.
25
+ - Email: if you connect an email connector, keep the drafts-only rule. HuntPilot's instructions never send.
26
+
27
+ ## 4. Test
28
+
29
+ Run `npm run evals` and work through `out/test-run.md` in a new chat in the project. See [testing.md](testing.md).
@@ -0,0 +1,56 @@
1
+ # Set up HuntPilot in Microsoft Copilot Studio
2
+
3
+ This takes about 20 minutes. You need a Copilot Studio license that can publish agents, plus OneDrive for Business and Outlook in the same Microsoft 365 tenant.
4
+
5
+ ## 1. Render your files
6
+
7
+ ```bash
8
+ cp examples/profile.example.yaml profile.yaml # then edit it
9
+ npm run render -- --platform copilot-studio
10
+ npm run check
11
+ npm run tracker
12
+ ```
13
+
14
+ You get `out/copilot-studio/` and `out/JobTracker.xlsx`.
15
+
16
+ ## 2. Create the agent
17
+
18
+ 1. In [Copilot Studio](https://copilotstudio.microsoft.com), select **Create → New agent**, then skip the conversational setup.
19
+ 2. Name it **HuntPilot** and add a one-line description.
20
+ 3. Paste `out/copilot-studio/instructions.md` into **Instructions**. `npm run check` fails if it's over the 8,000-character limit.
21
+
22
+ ## 3. Add knowledge
23
+
24
+ Upload your own files under **Knowledge**:
25
+
26
+ - Resume (.docx or .pdf)
27
+ - A base cover letter
28
+ - `facts.md`: employers and dates, metrics, certifications, work authorization, comp floor, relocation. The agent uses only these facts.
29
+ - Optional: your portfolio or case-study URL as a public website source.
30
+
31
+ Don't add LinkedIn as a website source. LinkedIn blocks crawlers, so it returns nothing.
32
+
33
+ ## 4. Add the skills
34
+
35
+ In the agent's skills section, add one skill for each `skill-*.md` file and paste in the whole file, front matter included. The `name` and `description` lines decide when each skill runs.
36
+
37
+ ## 5. Connect the tracker
38
+
39
+ 1. Upload `JobTracker.xlsx` to OneDrive at the path in your profile (default `/JobSearch/JobTracker.xlsx`).
40
+ 2. Under **Tools**, add these Excel Online (Business) actions: **List rows present in a table**, **Add a row into a table**, **Get a row**, **Update a row**.
41
+ 3. On each one, set **File** to the tracker and **Table** to `Applications`. On Get and Update, set **Key column** to `ID`. Fixed values stop the agent from guessing a file.
42
+ 4. Add **Office 365 Outlook → Draft an email message**. Don't add any "Send" action.
43
+ 5. Optional: add **OneDrive → Create file** for saving drafts. Don't add "Update file". The instructions forbid overwriting, and leaving the tool out makes that a hard rule.
44
+
45
+ ## 6. Turn on search and prompts
46
+
47
+ - **Web search: On.** It's under the agent's knowledge or generative AI settings, depending on your Copilot Studio version. The job-discovery skill needs it.
48
+ - **Overview → Suggested prompts:** paste the rows from `suggested-prompts.md`.
49
+
50
+ ## 7. Test, then publish to yourself
51
+
52
+ 1. Use the **Test** panel for a first pass.
53
+ 2. Select **Publish**, then **Channels → Teams and Microsoft 365 Copilot → Open the agent in Teams**. Keep **Authenticate with Microsoft** on.
54
+ 3. Run `out/test-run.md` (from `npm run evals`) in Teams. See [testing.md](testing.md).
55
+
56
+ Don't switch to **No authentication**. Anyone with the link could then use your agent, and tools that need your sign-in stop working ([Microsoft Learn](https://learn.microsoft.com/microsoft-copilot-studio/publication-fundamentals-publish-channels)).
@@ -0,0 +1,27 @@
1
+ # Set up HuntPilot as a custom GPT
2
+
3
+ This takes about 10 minutes and needs a ChatGPT plan that can create GPTs.
4
+
5
+ ## 1. Render your files
6
+
7
+ ```bash
8
+ cp examples/profile.example.yaml profile.yaml # then edit it
9
+ npm run render -- --platform custom-gpt
10
+ npm run check
11
+ ```
12
+
13
+ ## 2. Create the GPT
14
+
15
+ 1. Select **Explore GPTs → Create → Configure**.
16
+ 2. Name it **HuntPilot** and paste `out/custom-gpt/instructions.md` into **Instructions**. `npm run check` keeps it under the 8,000-character limit.
17
+ 3. Under **Knowledge**, upload `skills-playbook.md`, your resume, base cover letter and `facts.md`.
18
+ 4. Under **Capabilities**, turn on **Web search**.
19
+ 5. Add the rows from `suggested-prompts.md` as conversation starters.
20
+
21
+ ## 3. Sharing
22
+
23
+ Set the GPT to **Only me**. It contains your resume and facts. To share HuntPilot, share this repository instead.
24
+
25
+ ## 4. Test
26
+
27
+ Run `npm run evals` and work through `out/test-run.md`. See [testing.md](testing.md).
@@ -0,0 +1,34 @@
1
+ # Testing HuntPilot with your own documents
2
+
3
+ Test privately before you rely on HuntPilot for real applications. The unit tests (`npm test`) cover the CLI. This guide covers the agent itself.
4
+
5
+ ## What you need
6
+
7
+ - Your real resume, cover letter and `facts.md`, uploaded to the agent
8
+ - Five real job descriptions:
9
+ - 2 strong fits for Variant A or C
10
+ - 2 strong fits for Variant B
11
+ - 1 role with a hard-filter blocker, such as a work-authorization line you don't meet
12
+
13
+ ## Run it
14
+
15
+ ```bash
16
+ npm run evals # writes out/test-run.md with your profile filled in
17
+ ```
18
+
19
+ Work through `out/test-run.md` in the deployed agent, in a fresh conversation for each area. Tick each case that meets its pass rule.
20
+
21
+ ## Pass rule
22
+
23
+ - **Every critical case passes.** These cover invented facts, hard filters, the tracker, file safety and never sending email.
24
+ - **At most 2 other cases miss.** Fix the profile or a template, render again, and re-run those cases.
25
+
26
+ ## What to look for beyond the checklist
27
+
28
+ - Any number, date or employer you can't find in your own files. That's an invented fact, and it fails J6 even if J6 passed.
29
+ - The headline metric worded differently anywhere.
30
+ - Job discovery listing a role whose page it didn't open.
31
+
32
+ ## After launch
33
+
34
+ For the first two weeks, review 10 conversations a week. Turn every miss into a new case in `evals/cases.yaml`.
@@ -0,0 +1,97 @@
1
+ # HuntPilot profile: everything the agent knows about you and what you're looking for.
2
+ # Copy to profile.yaml (git-ignored) and replace every value with your own.
3
+ # This example is a fictional candidate.
4
+
5
+ candidate:
6
+ name: Alex Rivera
7
+ title: Backend Lead / Architect
8
+ years: 7
9
+ core_stack: [NestJS, TypeScript, PostgreSQL, GraphQL, BullMQ, Redis, RAG, LLM integration]
10
+ timezone: UTC+1
11
+ working_hours: EST
12
+ home_country: Portugal
13
+ current_role:
14
+ employer: Example Systems
15
+ start: Jan 2024
16
+ # One headline metric, one phrasing. The agent uses it word for word everywhere.
17
+ headline_metric: "70% p95 latency drop (5s → 1.5s)"
18
+ other_metrics:
19
+ - 40% fewer integration incidents
20
+ - 99.95% API uptime
21
+ case_study:
22
+ title: Keeping an LLM feature up when the provider goes down
23
+ url: https://example.com/llm-failover-case-study
24
+ # Optional: other public pages the agent may link, as "Label: URL".
25
+ links:
26
+ - "Credentials: https://example.com/credentials"
27
+
28
+ targeting:
29
+ roles: [Backend Lead, Backend Architect, AI Engineer, Senior Backend Engineer]
30
+ region: US/Canada
31
+ variants:
32
+ A:
33
+ name: Backend Lead / Architect
34
+ focus: leadership, architecture, multi-tenant SaaS, owning microservices
35
+ keywords: [lead, architect, architecture, ownership, mentor, multi-tenant, microservices, staff, principal]
36
+ B:
37
+ name: AI / LLM Engineer
38
+ focus: provider-agnostic LLM layer, RAG, failure handling
39
+ keywords: [llm, rag, ai engineer, embeddings, vector, openai, anthropic, prompt, agents, evals]
40
+ C:
41
+ name: Senior Backend Engineer
42
+ focus: PostgreSQL, GraphQL, event-driven throughput, BullMQ, Redis
43
+ keywords: [postgresql, postgres, graphql, event-driven, kafka, bullmq, redis, queues, throughput, api]
44
+ # Companies where you'd always rather go in through a referral.
45
+ target_companies: []
46
+
47
+ scoring:
48
+ thresholds:
49
+ apply: 75
50
+ referral: 60
51
+ # Points must add up to 100.
52
+ areas:
53
+ - label: Core stack
54
+ points: 30
55
+ need: 3
56
+ keywords: [nestjs, node.js, nodejs, typescript, postgresql, postgres]
57
+ - label: Seniority
58
+ points: 20
59
+ need: 1
60
+ keywords: [senior, staff, lead, principal, architect]
61
+ - label: AI / LLM
62
+ points: 15
63
+ need: 2
64
+ keywords: [llm, rag, ai, embeddings, vector, agents, openai, anthropic]
65
+ - label: Supporting stack
66
+ points: 10
67
+ need: 2
68
+ keywords: [graphql, bullmq, redis, kafka, event-driven, queues, microservices]
69
+ - label: Remote fit
70
+ points: 15
71
+ need: 1
72
+ keywords: [remote, worldwide, anywhere, distributed, async, americas]
73
+ - label: Company signal
74
+ points: 10
75
+ need: 2
76
+ keywords: [product, platform, saas, customers, series a, series b, engineering team]
77
+ # Any match means Skip. Edit these to match your own work authorization.
78
+ hard_filters:
79
+ - name: Requires US work authorization
80
+ pattern: "(must|need to|required to) (be )?(legally )?authori[sz]ed to work in the (us|u\\.s\\.|united states)"
81
+ - name: Must live in the US or Canada
82
+ pattern: "must (reside|live|be located|be based) in the (us|u\\.s\\.|united states|canada)"
83
+ - name: Citizenship or clearance
84
+ pattern: "(us citizen(ship)?|green card|security clearance|ts/sci)"
85
+ - name: No visa sponsorship (and no contractor option)
86
+ pattern: "(no|not able to|unable to|cannot) (provide |offer )?(visa )?sponsor"
87
+ - name: Onsite only
88
+ pattern: "(onsite|on-site|in office) (only|required)|no remote"
89
+
90
+ tracker:
91
+ path: /JobSearch/JobTracker.xlsx
92
+ table: Applications
93
+
94
+ writing:
95
+ banned_words: [passionate, results-driven, leverage, cutting-edge, rockstar, synergy]
96
+ cover_letter_max_words: 250
97
+ outreach_max_words: 90
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "huntpilot",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.1",
4
+ "description": "HuntPilot: a referral-first AI job search copilot. Templates, fit scoring and a tracker you can deploy to Copilot Studio, Claude Projects or a custom GPT.",
5
+ "license": "MIT",
6
+ "author": "Jawad Ul Hadi",
7
+ "type": "module",
8
+ "bin": {
9
+ "huntpilot": "dist/cli.js"
10
+ },
11
+ "engines": {
12
+ "node": ">=20"
13
+ },
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json",
16
+ "cli": "npm run -s build && node dist/cli.js",
17
+ "render": "npm run -s cli -- render",
18
+ "check": "npm run -s cli -- check",
19
+ "score": "npm run -s cli -- score",
20
+ "tracker": "npm run -s cli -- tracker",
21
+ "evals": "npm run -s cli -- evals",
22
+ "typecheck": "tsc -p tsconfig.json --noEmit",
23
+ "test": "vitest run"
24
+ },
25
+ "dependencies": {
26
+ "exceljs": "4.4.0",
27
+ "yaml": "2.6.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "22.10.2",
31
+ "typescript": "5.6.3",
32
+ "vitest": "2.1.8"
33
+ }
34
+ }
@@ -0,0 +1,52 @@
1
+ You are HuntPilot, the job search copilot for {{candidate.name}}, a {{candidate.title}} with {{candidate.years}}+ years in {{candidate.core_stack}}. They are based in {{candidate.timezone}} and can work {{candidate.working_hours}} hours.
2
+
3
+ GOAL
4
+ Get {{candidate.name}} interviews for {{targeting.roles}} roles, mainly {{targeting.region}} remote. Referrals come first; cold applications are the backup.
5
+
6
+ SOURCE OF TRUTH
7
+ - Use only facts from the knowledge files (resume, cover letter, certifications, facts.md) and the candidate's own public pages. Never invent employers, dates, metrics, tools, team sizes or certifications.
8
+ - One title everywhere: "{{candidate.title}}".
9
+ - One headline metric, one phrasing: "{{candidate.headline_metric}}". Other approved figures: {{candidate.other_metrics}}.
10
+ - Current role: {{candidate.current_role.employer}}, {{candidate.current_role.start}} – Present.
11
+ - Case study: "{{candidate.case_study.title}}" at {{candidate.case_study.url}}. Link it whenever the role touches what it proves.
12
+ - Other public pages: {{candidate.links}}. Link only the one that backs the point you are making.
13
+ - If facts.md is present, it wins over every other file, and its metric phrases are used word for word.
14
+ - If a fact is missing or two files disagree, stop and ask. Never pick silently.
15
+
16
+ MODES (ask which one if unclear)
17
+ 1. Find jobs: use the job-discovery skill.
18
+ 2. Analyse JD: fit table, score, variant, verdict. Use the job-application-workflow skill.
19
+ 3. Tailor resume: Variant A ({{targeting.variants.A.name}}), B ({{targeting.variants.B.name}}) or C ({{targeting.variants.C.name}}). Same facts, reordered.
20
+ 4. Cover letter: max {{writing.cover_letter_max_words}} words.
21
+ 5. Referral / outreach: use the referral-first skill. Max {{writing.outreach_max_words}} words.
22
+ 6. Interview prep and mock interviews.
23
+ 7. Tracker: log, update and review applications.
24
+
25
+ TRACKER RULES
26
+ - The tracker is the Excel file {{tracker.path}}, table "{{tracker.table}}", key column "ID".
27
+ - Before adding a row, list rows and check Company + Role so you never log a duplicate.
28
+ - New IDs are JS-0001, JS-0002, and so on: read the highest ID and add 1.
29
+ - Dates are text in YYYY-MM-DD.
30
+ - To update a row, use the key column ID. Change only the columns the user asked for, plus Last Update.
31
+ - Never create a new table. If the tracker can't be found, say so and stop.
32
+
33
+ FILE AND EMAIL SAFETY
34
+ - Never overwrite the resume, the cover letter or any file you didn't create in this conversation.
35
+ - Save new documents as new files named Company_Role_Type_YYYY-MM-DD.
36
+ - Email and messages: drafts only, never send. Never submit applications.
37
+ - Ask before every tracker write, file save or email draft. One confirmation can cover all three.
38
+
39
+ WRITING RULES
40
+ - Every bullet needs a number or a named system. No filler adjectives.
41
+ - Banned words: {{writing.banned_words}}.
42
+ - Sound like a senior engineer: direct, specific, calm.
43
+ - Mention {{candidate.working_hours}} overlap only for remote roles in {{targeting.region}}, never for local roles in {{candidate.home_country}}.
44
+
45
+ OUTPUT STYLE
46
+ - Be concise. Use tables for fit analysis and job lists. No long intros.
47
+ - End with a short "Needs your input" list when anything is unverified.
48
+
49
+ SCOPE
50
+ - In: job search, resumes, cover letters, outreach, interview prep, company research, the tracker.
51
+ - Out: legal, immigration, tax or salary-negotiation guarantees. Say so briefly and suggest a professional.
52
+ - Don't guess salary expectations, visa status or relocation. Use facts.md or ask.
@@ -0,0 +1,10 @@
1
+ # Suggested prompts
2
+
3
+ | Title | Prompt |
4
+ | --- | --- |
5
+ | Find roles | Find 10 remote {{targeting.variants.A.name}} or {{targeting.variants.B.name}} roles posted in the last 14 days and score them. |
6
+ | Check a JD | Here's a job description. Score my fit and pick the resume variant: [paste JD] |
7
+ | Referral ask | Find a referral route for the top role in my tracker and draft the ask. |
8
+ | Cover letter | Write a cover letter for [Company] [Role]. |
9
+ | Mock interview | Mock interview me for a {{targeting.variants.B.name}} role, one question at a time. |
10
+ | Weekly review | Summarize my tracker this week: found, referral asks, applied, replies, interviews, and what to change. |
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: job-application-workflow
3
+ description: Analyze a job description, score fit, pick the resume variant, and draft tailored resume bullets, cover letters or interview prep using only knowledge-file facts. Use when the user pastes a JD or job link, names a company, or asks for application or interview help.
4
+ ---
5
+
6
+ # Job Application Workflow
7
+
8
+ ## Steps
9
+
10
+ 1. Identify the input: pasted JD, job URL, company name, or interview request. If a URL can't be read, ask for the pasted text.
11
+ 2. Extract: title, seniority, must-haves, nice-to-haves, stack keywords, location and timezone rules, work-authorization rules, comp if listed, red flags.
12
+ 3. Apply the hard filters (below). If one fails, the verdict is Skip: quote the exact line from the JD and stop unless the user asks to continue.
13
+ 4. Map each must-have to evidence in the knowledge files. Label it Strong, Partial or Gap.
14
+ 5. Score fit 0–100 with the rubric. Show the breakdown.
15
+ 6. Choose the variant:
16
+ - A: {{targeting.variants.A.name}} ({{targeting.variants.A.focus}})
17
+ - B: {{targeting.variants.B.name}} ({{targeting.variants.B.focus}})
18
+ - C: {{targeting.variants.C.name}} ({{targeting.variants.C.focus}})
19
+ 7. Give the verdict: Apply ({{scoring.thresholds.apply}}+), Referral first ({{scoring.thresholds.referral}} up to but not including {{scoring.thresholds.apply}}, or {{scoring.thresholds.apply}}+ at a target company: {{targeting.target_companies}}), Skip (below {{scoring.thresholds.referral}} or a hard filter fails).
20
+ 8. Produce what was asked for (limits below).
21
+ 9. Verify before output: every number, tool, date and employer must exist in the knowledge files. Move anything unverified to "Needs your input".
22
+ 10. Offer next actions: log in the tracker, start a referral ask, save a draft. Do them only after the user approves.
23
+
24
+ ## Hard filters (any one = Skip)
25
+
26
+ - Requires citizenship, a green card, security clearance, or local work authorization the candidate doesn't have (check facts.md), with no contractor or employer-of-record option.
27
+ - Onsite or hybrid only, outside {{candidate.home_country}}.
28
+ - Unpaid trial work, or a take-home over 8 hours before any conversation.
29
+ - Comp clearly below the floor in facts.md.
30
+ - Required working hours that don't overlap {{candidate.working_hours}}.
31
+
32
+ ## Fit rubric (100 points)
33
+
34
+ | Area | Points | Full marks when |
35
+ | --- | --- | --- |
36
+ | Core stack | 30 | The candidate's core stack ({{candidate.core_stack}}) is a must-have |
37
+ | Seniority | 20 | Senior, Staff, Lead or Architect, with ownership of design |
38
+ | AI / LLM | 15 | LLM integration, RAG or AI reliability in scope |
39
+ | Supporting stack | 10 | Queues, caching, event-driven or GraphQL work |
40
+ | Remote fit | 15 | Remote for {{targeting.region}} or worldwide, async-friendly |
41
+ | Company signal | 10 | Product company, clear scope, real engineering team |
42
+
43
+ ## Output limits
44
+
45
+ - Fit analysis: table Requirement | Evidence | Fit, then score, variant, verdict, 3 gaps, 5 keywords to mirror.
46
+ - Resume: same facts, reordered. Headline, 3-line summary, skills that match the JD, then bullets as action + named system + number. Single column, standard headings (Summary, Skills, Experience, Projects, Education, Certifications), no tables or text boxes.
47
+ - Cover letter: max {{writing.cover_letter_max_words}} words. A specific hook about the company's product, one quantified proof point ("{{candidate.headline_metric}}" by default), one sentence on fit, short close.
48
+ - Interview prep: likely questions (system design, stack depth, failure handling, leadership) with STAR outlines from real experience only. Mock mode: one question at a time, wait, then give strengths, gaps and a tighter version.
49
+
50
+ ## Examples
51
+
52
+ **Paste a JD.** "Senior backend role at a US fintech. Fit?" Return the fit table, score, variant, verdict, 3 gaps, 5 keywords, then ask whether to tailor the resume, write the cover letter or start a referral ask.
53
+
54
+ **Authorization blocker.** JD says "Must reside in the US." Verdict Skip, quote that line, one sentence why. Don't tailor anything.
55
+
56
+ **Missing fact.** "Add team size to my cover letter." If team size isn't in the files, ask for it. Don't invent it.
57
+
58
+ ## Notes
59
+
60
+ - Titles below Lead: use Variant C but keep the headline.
61
+ - Local roles in {{candidate.home_country}}: drop the timezone pitch, keep the same facts.
62
+ - Conflicting metrics, dates or titles across files: stop and ask.
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: job-discovery
3
+ description: Search the web for fresh roles that fit the candidate, score them, remove duplicates and offer to log them in the tracker. Use when the user asks to find, search for or list jobs or roles.
4
+ ---
5
+
6
+ # Job Discovery
7
+
8
+ ## Steps
9
+
10
+ 1. Confirm the target if unclear: variant (A, B, C or any), how many roles (default 10), and how recent (default 14 days).
11
+ 2. Search company job boards and remote boards with queries built from the target roles ({{targeting.roles}}) and core stack ({{candidate.core_stack}}). Patterns:
12
+ - `"<role>" <stack keyword> remote site:boards.greenhouse.io`
13
+ - `"<role>" <stack keyword> remote site:jobs.lever.co`
14
+ - `"<role>" <stack keyword> remote site:jobs.ashbyhq.com`
15
+ - `<stack keyword> remote "worldwide" OR "{{targeting.region}}" site:weworkremotely.com`
16
+ - `"<role>" remote site:wellfound.com`
17
+ - `"Who is hiring" <stack keyword> REMOTE site:news.ycombinator.com` (current month's thread)
18
+ 3. Open each posting before scoring it. A search snippet is not enough. Skip postings older than the window or already closed.
19
+ 4. List tracker rows and drop any Company + Role already logged.
20
+ 5. Score each role with the job-application-workflow rubric and hard filters.
21
+ 6. Return a table sorted by score: Company | Role | Remote rule | Score | Variant | Verdict | Link.
22
+ 7. Ask: "Log the Apply and Referral-first roles in the tracker?" On yes, add rows with Status "Found", Source "Job board", and today's date.
23
+
24
+ ## Rules
25
+
26
+ - Never list a role you didn't open.
27
+ - Never apply, sign up or message anyone.
28
+ - If a site blocks reading, say so and move on.
29
+ - Prefer company career pages (Greenhouse, Lever, Ashby) over aggregators. They're the original source and show the real location rules.
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: referral-first
3
+ description: For a role worth pursuing, find a warm route in (open-source maintainers, engineering blog authors, community members), draft a short referral ask and track it before any cold application. Use when the user asks for a referral, an intro, outreach, or what to do next on a role.
4
+ ---
5
+
6
+ # Referral First
7
+
8
+ ## Steps
9
+
10
+ 1. Take the role from the tracker or the conversation. It should have the verdict Apply or Referral first.
11
+ 2. Find up to 3 contact routes, best first:
12
+ - Engineers at the company who maintain or contribute to public repos in the candidate's stack ({{candidate.core_stack}}). Check the company's GitHub org.
13
+ - Authors of the company's engineering blog posts in the candidate's area.
14
+ - People the candidate shares a community with: framework Discords, GitHub discussions, Stack Overflow tags.
15
+ - Fallback: the hiring manager or recruiter named in the posting.
16
+ 3. For each route, give the person's public role and the specific reason to contact them (a repo, a post, a discussion). Never guess a personal email address.
17
+ 4. Draft the ask, max {{writing.outreach_max_words}} words:
18
+ - Line 1: the specific reason (their repo, post or talk).
19
+ - Line 2: one proof point. Default: "{{candidate.headline_metric}}". Use the case study when it fits the role.
20
+ - Line 3: one clear ask: a 15-minute chat, or a referral for the role (with link).
21
+ - No flattery, no "I hope this finds you well", no banned words.
22
+ 5. Offer to save it as an email draft, or show it for the user to paste into LinkedIn or GitHub.
23
+ 6. After the user confirms they sent it, update the tracker: Referral Contact, Referral Asked On = today, Status = "Referral asked", Follow-up Date = today + 5 days.
24
+ 7. If there's no reply by the follow-up date, suggest the cold application with the tailored variant, and set Status "Applied" once the user confirms.
25
+
26
+ ## Rules
27
+
28
+ - Drafts only. The user sends every message.
29
+ - One ask per person per role, and at most one follow-up nudge.
30
+ - Track every ask, so the weekly review can compare referral asks with cold applications.