Skip to content

feat: optional resume-ops mode for high-quality tailoring#640

Draft
Rat-S wants to merge 2 commits into
santifer:mainfrom
Rat-S:feat/resume-ops-integration
Draft

feat: optional resume-ops mode for high-quality tailoring#640
Rat-S wants to merge 2 commits into
santifer:mainfrom
Rat-S:feat/resume-ops-integration

Conversation

@Rat-S
Copy link
Copy Markdown

@Rat-S Rat-S commented May 13, 2026

What does this PR do?

Introduces an optional resume-ops mode that leverages an external high-quality tailoring service. It includes a robust startup script that automatically clones and configures the service, allowing users to opt-in for better resume quality without affecting the core agent-driven flow.

Related issue

Fixes #635 / Closes #557

Type of change

  • Bug fix
  • New feature
  • Documentation / translation
  • Refactor (no behavior change)

Checklist

  • I have read CONTRIBUTING.md
  • I linked a related issue above (required for features and architecture changes)
  • My PR does not include personal data (CV, email, real names)
  • I ran node test-all.mjs and all tests pass
  • My changes respect the Data Contract (no modifications to user-layer files)
  • My changes align with the project roadmap

Summary by CodeRabbit

  • New Features

    • Added a resume tailoring workflow with new npm scripts for resume operations
    • Generate tailored resumes in PDF and JSON formats based on job descriptions
    • New output options for theme customization and file formatting
  • Documentation

    • Added comprehensive guide for the resume-ops mode, including workflow pipeline and safety guidelines
  • Chores

    • Updated dependencies to support resume processing and PDF generation
    • Updated .gitignore to exclude generated files and service directories

Review Change Stack

@github-actions
Copy link
Copy Markdown
Contributor

Welcome to career-ops, @Rat-S! Thanks for your first PR.

A few things to know:

  • Tests will run automatically — check the status below
  • Make sure you've linked a related issue (required for features)
  • Read CONTRIBUTING.md if you haven't

We'll review your PR soon. Join our Discord if you have questions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

📝 Walkthrough

Walkthrough

This PR introduces a resume-ops workflow feature that enables high-quality resume tailoring against job descriptions. It adds service initialization, a CLI client for invoking tailoring, comprehensive documentation, supporting dependencies, and test fixtures to enable end-to-end resume generation and PDF output.

Changes

Resume-Ops Workflow Feature

Layer / File(s) Summary
Documentation and project setup
modes/resume-ops.md, package.json, .gitignore
Introduces the resume-ops mode workflow pipeline: JD preparation, invisible service setup (cloning/configuration), tailoring execution with theme support, and tracker updates. Adds npm scripts resume-ops and resume-ops:start, dependencies including puppeteer, resumed, and jsonresume-theme-stackoverflow, and ignores generated scratch/, resume-ops/, and resume-ops.log paths.
Service startup and bootstrap
start-resume-ops.mjs
Checks port 8000 availability, clones the resume-ops repository, initializes .env from .env.example with API keys, installs the resumed PDF engine, and spawns the Python service via uv with detached execution, log redirection, and readiness polling.
Resume tailoring CLI client
resume-ops.mjs
Parses CLI arguments (--resume, --jd, --output, --theme), loads resume JSON and resolves job description text or file path, sends a tailoring request to the local API, polls asynchronously for task completion when needed, and writes resulting PDF and optional JSON resume to disk.
Test data
scratch/test-jd.txt
Provides a sample Senior Product Manager job description targeting AI/automation, Python, and LLMs to test the resume tailoring workflow.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CLI as resume-ops.mjs
  participant API as Resume Ops API
  participant Storage as Filesystem
  
  User->>CLI: resume-ops --resume resume.json --jd job.txt --output output.pdf
  CLI->>CLI: Parse args, load resume and JD
  CLI->>API: POST /api/v1/tailor (resume + JD)
  API-->>CLI: {task_id} or {pdf_base64}
  alt async task
    CLI->>API: GET /api/v1/tasks/{taskId} (poll)
    API-->>CLI: {pdf_base64}
  end
  CLI->>Storage: Write PDF to output.pdf
  CLI->>Storage: Write JSON to output.json
  CLI-->>User: Success
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: optional resume-ops mode for high-quality tailoring' clearly and specifically summarizes the main change: introducing an optional resume-ops mode for improved resume tailoring functionality.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Rat-S Rat-S marked this pull request as ready for review May 13, 2026 09:06
@Rat-S Rat-S marked this pull request as draft May 13, 2026 09:07
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modes/resume-ops.md`:
- Around line 25-27: Add a blank line immediately before the opening fenced code
block (```bash) and another blank line immediately after the closing ``` so the
snippet containing "node resume-ops.mjs --resume
resume-ops/.local/master-resume.json --jd \"{JD_TEXT}\" --output
output/cv-{candidate}-{company}-{YYYY-MM-DD}.pdf" is visually separated from
surrounding text and conforms to Markdown linting; update the surrounding lines
accordingly and ensure any subsequent note about passing --theme (if present)
remains on its own paragraph after the blank line.

In `@resume-ops.mjs`:
- Around line 4-15: The pollTask function interpolates taskId into the URL
without validation; validate and sanitize taskId before constructing the URL by
(1) enforcing an allowed pattern (e.g., UUID/hex or a strict /^[A-Za-z0-9-]+$/
regex) and throwing an error for invalid values, and (2) using
encodeURIComponent on the validated taskId when building the URL; update
pollTask to run this check at the top (refer to function pollTask and the taskId
parameter) so malicious strings like "../" or special characters are rejected or
safely encoded before fetch is called.
- Around line 45-88: Add request timeouts and a max-poll limit: wrap the Resume
Ops POST fetch in an AbortController with a configurable timeout (e.g., 10–30s)
and abort the request if exceeded, ensuring the error bubbles up to the existing
catch; likewise update the pollTask function to accept a maxAttempts and/or
totalTimeout and stop polling after that limit (or when the total timeout
elapses), returning a clear error if the task never completes. Reference the
fetch call in the main block (the POST to '/api/v1/tailor') and the
pollTask(task_id) function; ensure both paths clean up the AbortController
timers and throw a descriptive Error so the existing catch logs and exits.
- Around line 77-81: The code uses a case-sensitive string replace on
params.output to produce the JSON path; change this to use Node's path utilities
so extensions are handled case-insensitively and robustly: import/require 'path'
and compute jsonPath from path.parse(params.output) (e.g., const p =
path.parse(params.output); const jsonPath = path.join(p.dir, `${p.name}.json`));
then continue to call fs.writeFileSync(jsonPath, JSON.stringify(data.resume,
null, 2)) and log the jsonPath; reference symbols: params.output, data.resume,
fs.writeFileSync.

In `@start-resume-ops.mjs`:
- Around line 54-61: The replacement uses process.env[key] directly in
localEnv.replace(new RegExp(`${key}=.*`), `${key}=${process.env[key]}`) which
can corrupt the .env when values contain regex-replacement metacharacters;
change the replacement to use a safe replacer that injects the raw value (e.g.,
pass a function as the second argument or escape the replacement string) so the
matched line is replaced with `${key}=` + the literal environment value; update
the keysToSync loop to compute const value = process.env[key] and perform
localEnv = localEnv.replace(new RegExp(`${key}=.*`), () => `${key}=${value}`) to
ensure special characters in process.env[key] are not interpreted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5cbdeee1-5ea9-4293-9f3d-c05ab4efcf8a

📥 Commits

Reviewing files that changed from the base of the PR and between d692647 and 152353a.

📒 Files selected for processing (6)
  • .gitignore
  • modes/resume-ops.md
  • package.json
  • resume-ops.mjs
  • scratch/test-jd.txt
  • start-resume-ops.mjs

Comment thread modes/resume-ops.md
Comment on lines +25 to +27
```bash
node resume-ops.mjs --resume resume-ops/.local/master-resume.json --jd "{JD_TEXT}" --output output/cv-{candidate}-{company}-{YYYY-MM-DD}.pdf
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add blank lines around the fenced code block.

The code block should be surrounded by blank lines for proper Markdown rendering and consistency with linting rules.

📝 Proposed fix
 3. **Execute Tailoring**:
    - Determine the output PDF path: `output/cv-{candidate}-{company}-{YYYY-MM-DD}.pdf`.
    - Use `config/profile.yml` to get `{candidate}` (normalized to kebab-case) and `{company}`.
    - Run the helper script:
+
      ```bash
      node resume-ops.mjs --resume resume-ops/.local/master-resume.json --jd "{JD_TEXT}" --output output/cv-{candidate}-{company}-{YYYY-MM-DD}.pdf
      ```
+
    - If `config/profile.yml` has a `cv.theme` value, pass it via `--theme`.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 25-25: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 27-27: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modes/resume-ops.md` around lines 25 - 27, Add a blank line immediately
before the opening fenced code block (```bash) and another blank line
immediately after the closing ``` so the snippet containing "node resume-ops.mjs
--resume resume-ops/.local/master-resume.json --jd \"{JD_TEXT}\" --output
output/cv-{candidate}-{company}-{YYYY-MM-DD}.pdf" is visually separated from
surrounding text and conforms to Markdown linting; update the surrounding lines
accordingly and ensure any subsequent note about passing --theme (if present)
remains on its own paragraph after the blank line.

Comment thread resume-ops.mjs
Comment on lines +4 to +15
async function pollTask(taskId) {
const url = `http://127.0.0.1:8000/api/v1/tasks/${taskId}`;
while (true) {
const response = await fetch(url);
if (!response.ok) throw new Error(`Polling failed: ${response.status}`);
const data = await response.json();
if (data.status === 'completed') return data;
if (data.status === 'failed') throw new Error(`Task failed: ${JSON.stringify(data.error)}`);
process.stdout.write('.');
await new Promise(r => setTimeout(r, 2000));
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate taskId format before URL construction.

Line 5 interpolates taskId directly into the URL without validation. If the Resume Ops API returns a malicious taskId (e.g., containing ../ or special characters), it could lead to unintended endpoint access. While the service is local, validating the format adds defense in depth.

🛡️ Proposed fix to validate taskId
 async function pollTask(taskId) {
+  // Validate taskId format (alphanumeric, hyphens, underscores only)
+  if (!/^[a-zA-Z0-9_-]+$/.test(taskId)) {
+    throw new Error(`Invalid task ID format: ${taskId}`);
+  }
   const url = `http://127.0.0.1:8000/api/v1/tasks/${taskId}`;
   while (true) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resume-ops.mjs` around lines 4 - 15, The pollTask function interpolates
taskId into the URL without validation; validate and sanitize taskId before
constructing the URL by (1) enforcing an allowed pattern (e.g., UUID/hex or a
strict /^[A-Za-z0-9-]+$/ regex) and throwing an error for invalid values, and
(2) using encodeURIComponent on the validated taskId when building the URL;
update pollTask to run this check at the top (refer to function pollTask and the
taskId parameter) so malicious strings like "../" or special characters are
rejected or safely encoded before fetch is called.

Comment thread resume-ops.mjs
Comment on lines +45 to +88
console.log('📡 Sending tailoring request to Resume Ops API...');

try {
const response = await fetch('http://127.0.0.1:8000/api/v1/tailor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
resume,
job_description: jd,
theme: params.theme || 'jsonresume-theme-stackoverflow'
})
});

if (!response.ok) {
const err = await response.text();
throw new Error(`API Error: ${response.status} ${err}`);
}

let data = await response.json();

if (data.task_id && !data.pdf_base64) {
console.log(`⏳ Task queued (ID: ${data.task_id}). Polling for completion...`);
data = await pollTask(data.task_id);
console.log('\n✅ Task completed.');
}

if (data.pdf_base64) {
fs.mkdirSync(path.dirname(params.output), { recursive: true });
fs.writeFileSync(params.output, Buffer.from(data.pdf_base64, 'base64'));
console.log(`📄 PDF successfully saved to ${params.output}`);

// Also save the tailored JSON if available
if (data.resume) {
const jsonPath = params.output.replace('.pdf', '.json');
fs.writeFileSync(jsonPath, JSON.stringify(data.resume, null, 2));
console.log(`📝 Tailored JSON saved to ${jsonPath}`);
}
} else {
throw new Error('No PDF content received from API');
}
} catch (err) {
console.error(`❌ Error: ${err.message}`);
process.exit(1);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider adding request timeout for external API calls.

The fetch call to the Resume Ops API (line 48) and polling loop (lines 6-14) have no timeout configuration. If the service hangs or becomes unresponsive, the script will wait indefinitely, leading to poor user experience.

⏱️ Proposed fix to add timeout handling
   console.log('📡 Sending tailoring request to Resume Ops API...');
   
   try {
-    const response = await fetch('http://127.0.0.1:8000/api/v1/tailor', {
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 60000); // 60s timeout
+    const response = await fetch('http://127.0.0.1:8000/api/v1/tailor', {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({
         resume,
         job_description: jd,
         theme: params.theme || 'jsonresume-theme-stackoverflow'
-      })
+      }),
+      signal: controller.signal
     });
+    clearTimeout(timeoutId);

     if (!response.ok) {

Also add a maximum iteration limit to pollTask:

 async function pollTask(taskId) {
   if (!/^[a-zA-Z0-9_-]+$/.test(taskId)) {
     throw new Error(`Invalid task ID format: ${taskId}`);
   }
   const url = `http://127.0.0.1:8000/api/v1/tasks/${taskId}`;
+  let attempts = 0;
+  const MAX_ATTEMPTS = 60; // 2 minutes with 2s intervals
   while (true) {
+    if (attempts++ >= MAX_ATTEMPTS) {
+      throw new Error('Polling timeout: task did not complete in time');
+    }
     const response = await fetch(url);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resume-ops.mjs` around lines 45 - 88, Add request timeouts and a max-poll
limit: wrap the Resume Ops POST fetch in an AbortController with a configurable
timeout (e.g., 10–30s) and abort the request if exceeded, ensuring the error
bubbles up to the existing catch; likewise update the pollTask function to
accept a maxAttempts and/or totalTimeout and stop polling after that limit (or
when the total timeout elapses), returning a clear error if the task never
completes. Reference the fetch call in the main block (the POST to
'/api/v1/tailor') and the pollTask(task_id) function; ensure both paths clean up
the AbortController timers and throw a descriptive Error so the existing catch
logs and exits.

Comment thread resume-ops.mjs
Comment on lines +77 to +81
if (data.resume) {
const jsonPath = params.output.replace('.pdf', '.json');
fs.writeFileSync(jsonPath, JSON.stringify(data.resume, null, 2));
console.log(`📝 Tailored JSON saved to ${jsonPath}`);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use case-insensitive path handling for JSON output.

Line 78 uses .replace('.pdf', '.json') which is case-sensitive. If --output specifies a path with uppercase .PDF, the JSON file will be named incorrectly (e.g., resume.PDF.json instead of resume.json).

🔧 Proposed fix using proper path manipulation
       // Also save the tailored JSON if available
       if (data.resume) {
-        const jsonPath = params.output.replace('.pdf', '.json');
+        const parsed = path.parse(params.output);
+        const jsonPath = path.join(parsed.dir, parsed.name + '.json');
         fs.writeFileSync(jsonPath, JSON.stringify(data.resume, null, 2));
         console.log(`📝 Tailored JSON saved to ${jsonPath}`);
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resume-ops.mjs` around lines 77 - 81, The code uses a case-sensitive string
replace on params.output to produce the JSON path; change this to use Node's
path utilities so extensions are handled case-insensitively and robustly:
import/require 'path' and compute jsonPath from path.parse(params.output) (e.g.,
const p = path.parse(params.output); const jsonPath = path.join(p.dir,
`${p.name}.json`)); then continue to call fs.writeFileSync(jsonPath,
JSON.stringify(data.resume, null, 2)) and log the jsonPath; reference symbols:
params.output, data.resume, fs.writeFileSync.

Comment thread start-resume-ops.mjs
Comment on lines +54 to +61
const keysToSync = ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GEMINI_API_KEY', 'OPENAI_BASE_URL'];
keysToSync.forEach(key => {
if (process.env[key]) {
console.log(`🔑 Syncing ${key} from environment...`);
// Replace the empty or placeholder value
localEnv = localEnv.replace(new RegExp(`${key}=.*`), `${key}=${process.env[key]}`);
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Escape special characters when syncing API keys to .env.

Line 59 uses process.env[key] directly in a regex replacement string. If an API key contains characters with special meaning in regex replacements (e.g., $, \), they will be misinterpreted, potentially corrupting the .env file.

🔒 Proposed fix to escape replacement string
        keysToSync.forEach(key => {
          if (process.env[key]) {
            console.log(`🔑 Syncing ${key} from environment...`);
-           // Replace the empty or placeholder value
-           localEnv = localEnv.replace(new RegExp(`${key}=.*`), `${key}=${process.env[key]}`);
+           // Replace the empty or placeholder value (escape special regex chars in value)
+           const escapedValue = process.env[key].replace(/\$/g, '$$$$');
+           localEnv = localEnv.replace(new RegExp(`${key}=.*`), `${key}=${escapedValue}`);
          }
        });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const keysToSync = ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GEMINI_API_KEY', 'OPENAI_BASE_URL'];
keysToSync.forEach(key => {
if (process.env[key]) {
console.log(`🔑 Syncing ${key} from environment...`);
// Replace the empty or placeholder value
localEnv = localEnv.replace(new RegExp(`${key}=.*`), `${key}=${process.env[key]}`);
}
});
const keysToSync = ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GEMINI_API_KEY', 'OPENAI_BASE_URL'];
keysToSync.forEach(key => {
if (process.env[key]) {
console.log(`🔑 Syncing ${key} from environment...`);
// Replace the empty or placeholder value (escape special regex chars in value)
const escapedValue = process.env[key].replace(/\$/g, '$$$$');
localEnv = localEnv.replace(new RegExp(`${key}=.*`), `${key}=${escapedValue}`);
}
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@start-resume-ops.mjs` around lines 54 - 61, The replacement uses
process.env[key] directly in localEnv.replace(new RegExp(`${key}=.*`),
`${key}=${process.env[key]}`) which can corrupt the .env when values contain
regex-replacement metacharacters; change the replacement to use a safe replacer
that injects the raw value (e.g., pass a function as the second argument or
escape the replacement string) so the matched line is replaced with `${key}=` +
the literal environment value; update the keysToSync loop to compute const value
= process.env[key] and perform localEnv = localEnv.replace(new
RegExp(`${key}=.*`), () => `${key}=${value}`) to ensure special characters in
process.env[key] are not interpreted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant