Compare commits

12 Commits
v0.5 ... main

19 changed files with 799 additions and 3 deletions

47
input/AGENTS.md Normal file
View File

@@ -0,0 +1,47 @@
# Input Agent Guide
## Mission
Automate the upstream resume customization workflow. Monitor the job-description inbox, clean up messy recruiter-sourced text, combine the normalized description with the base resume and prompt templates, invoke the Codex CLI, and deposit the generated Markdown in a timestamped outbox for human review.
## Directory Responsibilities
- `resume/` contains exactly one Markdown resume. Any other count is a fatal configuration error.
- `ForCustomizing/inbox/` drop one plain-text or Markdown job description at a time to trigger processing.
- `ForCustomizing/outbox/YYYY/MM/DD/HHMM/` timestamped folders containing Codex output Markdown (`<company>-<jobtitle>.md`) along with the exact prompt and the cleaned job description that fed Codex.
- `ForCustomizing/processed/YYYY/MM/DD/HHMM/` archives of job descriptions that Codex processed successfully.
- `ForCustomizing/failed/` captures job descriptions when Codex errors or a recoverable issue occurs. Fatal configuration errors still exit the container.
- `Docker/JobDescriptionNormalizerPrompt.md` baked normalizer instructions (modify the container build to customize).
- `templates/ResumeCustomizerPrompt.md.example` default resume-customization instructions; copy to `ResumeCustomizerPrompt.md` to override.
## Running the Input Processor
Launch the stack with the wrapper so files inherit your UID/GID and your local Codex credentials mount in:
```bash
cd input/Docker
./run-input-processor.sh up -d
```
- Environment variables you can pass before the command:
- `POLL_INTERVAL_SECONDS` watcher polling cadence (default `5`).
- `CODEX_TIMEOUT_SECONDS` hard timeout for Codex calls (default `600`).
- `HOST_CODEX_DIR` optional override for the host directory that should mount into `/home/codex/.codex`.
The container bundles the Codex CLI and expects the caller to mount their `~/.codex` directory for credentials.
Stop or inspect the stack with:
```bash
cd input/Docker
./run-input-processor.sh down
./run-input-processor.sh logs -f
```
## Guardrails
- Ensure only one job description resides in `ForCustomizing/inbox/`. Multiple files cause the container to exit with a fatal error.
- Keep exactly one resume Markdown in `resume/`. Missing or multiple resumes also terminate the watcher.
- The container runs as a non-root user matching the callers UID/GID. Avoid changing permissions manually inside the mounted directories.
- Do not edit anything under `output/` from this agent session; treat the downstream pipeline as read-only reference material.
## Troubleshooting
- If Codex CLI fails, the job description moves to `ForCustomizing/failed/`. Check container logs, adjust the Markdown, then requeue it.
- Fatal errors (multiple resumes, multiple job descriptions, missing template, or missing Codex binary) stop the container. Resolve the issue and restart via the wrapper.
- The watcher logs the exact `codex exec - --output-last-message … --skip-git-repo-check --sandbox read-only` command before each invocation; inspect container logs if troubleshooting is needed.

31
input/Docker/Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
FROM debian:bookworm
ENV DEBIAN_FRONTEND=noninteractive \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
CODEX_HOME=/home/codex
RUN apt-get update \
&& apt-get install --yes --no-install-recommends \
ca-certificates \
curl \
nodejs \
npm \
python3 \
python3-venv \
tzdata \
&& npm install --location=global @openai/codex \
&& groupadd --gid 1000 codex \
&& useradd --uid 1000 --gid 1000 --create-home --shell /bin/bash codex \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY watch_and_customize.py entrypoint.sh JobDescriptionNormalizerPrompt.md ./
RUN chmod +x /app/watch_and_customize.py /app/entrypoint.sh
USER codex
ENTRYPOINT ["/app/entrypoint.sh"]

View File

@@ -0,0 +1,28 @@
# Default instructions that clean messy job descriptions before resume customization.
# Customize only by modifying the container build.
You are an expert technical recruiter.
Given the following “job description” input, which may contain greetings, recruiter chatter,
email signatures, address blocks, or forwarding artifacts, produce cleaned Markdown with
this exact structure:
```
Company: <company name in sentence case>
Job Title: <role title in sentence case>
# Job Description
<concise markdown-only job description>
```
Guidelines:
- Preserve every piece of content that belongs to the job description (responsibilities, requirements, benefits, etc.). Do **not** summarize, condense, or delete substantive details.
- Only remove extraneous recruiter chatter such as greetings, signatures, contact blocks, forwarding notes, or metadata unrelated to the role itself.
- If multiple companies or roles appear, pick the primary one the candidate should target.
- Normalize whitespace, headings, and bullet lists in the Markdown output.
- Omit personal identifiers for recruiters or candidates unless they are essential job facts.
- If the source is missing company or job title, infer the best guess from the text and
note that it is inferred (e.g., “<Company> (inferred)”).
- Never add commentary outside the format shown above.
The raw content follows verbatim. Retain all job-related text.

View File

@@ -0,0 +1,23 @@
name: RCEO-AI-ResumeCustomizer-Input
services:
rceo-ai-resumecustomizer-inputprocessor:
build:
context: .
dockerfile: Dockerfile
container_name: RCEO-AI-ResumeCustomizer-InputProcessor
restart: "no"
environment:
POLL_INTERVAL_SECONDS: "${POLL_INTERVAL_SECONDS:-5}"
CODEX_TIMEOUT_SECONDS: "${CODEX_TIMEOUT_SECONDS:-600}"
volumes:
- ../ForCustomizing/inbox:/workspace/inbox
- ../ForCustomizing/outbox:/workspace/outbox
- ../ForCustomizing/processed:/workspace/processed
- ../ForCustomizing/failed:/workspace/failed
- ../resume:/workspace/resume:ro
- ../templates:/templates:ro
- ${HOST_CODEX_DIR:-${HOME}/.codex}:/home/codex/.codex
- /etc/localtime:/etc/localtime:ro
- /etc/machine-id:/etc/machine-id:ro
- /var/lib/dbus/machine-id:/var/lib/dbus/machine-id:ro

14
input/Docker/entrypoint.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
USER_NAME=${USER_NAME:-codex}
USER_HOME=$(eval echo "~${USER_NAME}")
mkdir -p /workspace/inbox /workspace/outbox /workspace/processed /workspace/failed
mkdir -p "${USER_HOME}/.codex"
export HOME="${USER_HOME}"
export XDG_CACHE_HOME="${USER_HOME}/.cache"
mkdir -p "${XDG_CACHE_HOME}"
exec python3 /app/watch_and_customize.py

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Wrapper to run docker compose with the caller's UID/GID so generated files stay writable.
set -euo pipefail
if ! command -v docker >/dev/null 2>&1; then
echo "Error: docker is not installed or not on PATH." >&2
exit 1
fi
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD=(docker compose)
elif command -dv docker-compose >/dev/null 2>&1; then
COMPOSE_CMD=(docker-compose)
else
echo "Error: docker compose plugin or docker-compose binary is required." >&2
exit 1
fi
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
HOST_CODEX_DIR=${HOST_CODEX_DIR:-${HOME}/.codex}
mkdir -p "${HOST_CODEX_DIR}"
(
cd "${SCRIPT_DIR}"
HOST_CODEX_DIR="${HOST_CODEX_DIR}" \
"${COMPOSE_CMD[@]}" "$@"
)

View File

@@ -0,0 +1,442 @@
#!/usr/bin/env python3
"""
Monitor the customization inbox, normalize messy job descriptions, and run the Codex CLI
to produce tailored resumes.
The watcher expects exactly one base resume Markdown file and processes one job file at a
time. After Codex succeeds, the generated resume is written into a timestamped outbox
folder using the pattern <company>-<jobtitle>.md, while the original job file is archived
under processed/. Failures move the job description into failed/.
"""
from __future__ import annotations
import logging
import os
import shutil
import subprocess
import time
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Sequence
INBOX = Path("/workspace/inbox")
OUTBOX = Path("/workspace/outbox")
PROCESSED = Path("/workspace/processed")
FAILED = Path("/workspace/failed")
RESUME_DIR = Path("/workspace/resume")
TEMPLATES_DIR = Path("/templates")
TEMPLATE_CACHE = Path("/tmp/templates")
PROMPT_TEMPLATE = TEMPLATES_DIR / "ResumeCustomizerPrompt.md"
PROMPT_TEMPLATE_EXAMPLE = TEMPLATES_DIR / "ResumeCustomizerPrompt.md.example"
NORMALIZER_TEMPLATE = Path("/app/JobDescriptionNormalizerPrompt.md")
POLL_INTERVAL_SECONDS = int(os.environ.get("POLL_INTERVAL_SECONDS", "5"))
CODEX_TIMEOUT_SECONDS = int(os.environ.get("CODEX_TIMEOUT_SECONDS", "600"))
RESOLVED_PROMPT_TEMPLATE: Path | None = None
RESOLVED_NORMALIZER_TEMPLATE: Path | None = None
class FatalConfigurationError(RuntimeError):
"""Raised when the watcher encounters a non-recoverable configuration problem."""
@dataclass(frozen=True)
class NormalizedJobDescription:
company: str
job_title: str
description_markdown: str
def ensure_environment() -> None:
"""Verify required directories and template assets exist."""
global RESOLVED_PROMPT_TEMPLATE, RESOLVED_NORMALIZER_TEMPLATE
missing = [
str(path)
for path in (
INBOX,
OUTBOX,
PROCESSED,
FAILED,
RESUME_DIR,
TEMPLATES_DIR,
)
if not path.exists()
]
if missing:
raise FatalConfigurationError(
"Input pipeline is missing required paths: " + ", ".join(missing)
)
RESOLVED_PROMPT_TEMPLATE = resolve_template(
PROMPT_TEMPLATE,
PROMPT_TEMPLATE_EXAMPLE,
TEMPLATE_CACHE,
"Resume customization prompt",
)
RESOLVED_NORMALIZER_TEMPLATE = NORMALIZER_TEMPLATE
if not RESOLVED_NORMALIZER_TEMPLATE.exists():
raise FatalConfigurationError(
f"Job description normalizer prompt missing at {RESOLVED_NORMALIZER_TEMPLATE}"
)
def resolve_template(
primary: Path,
example: Path,
cache_dir: Path,
description: str,
) -> Path:
"""Return the template path, copying the example if needed."""
if primary.exists():
return primary
if example.exists():
cache_dir.mkdir(parents=True, exist_ok=True)
cached = cache_dir / primary.name
shutil.copy(example, cached)
return cached
raise FatalConfigurationError(
f"{description} missing: {primary} (no example found at {example})"
)
def ensure_single_resume() -> Path:
"""Return the single resume markdown file or raise if conditions are not met."""
resumes = sorted(RESUME_DIR.glob("*.md"))
if len(resumes) == 0:
raise FatalConfigurationError(
f"No resume Markdown file found in {RESUME_DIR}. Exactly one is required."
)
if len(resumes) > 1:
raise FatalConfigurationError(
f"Multiple resume Markdown files found in {RESUME_DIR}: "
+ ", ".join(r.name for r in resumes)
)
return resumes[0]
def ensure_single_job(paths: Sequence[Path]) -> Path | None:
"""Validate there is at most one job description file (any extension)."""
visible = [path for path in paths if path.is_file() and not path.name.startswith(".")]
if not visible:
return None
if len(visible) > 1:
names = ", ".join(p.name for p in visible)
raise FatalConfigurationError(
f"Multiple job description files detected in inbox: {names} "
"— expected exactly one."
)
return visible[0]
def build_prompt_text(resume: Path, job_markdown: str, prompt_template: Path) -> str:
"""Return the combined prompt string fed to the Codex CLI."""
resume_text = resume.read_text(encoding="utf-8").strip()
instructions_text = prompt_template.read_text(encoding="utf-8").strip()
return (
"# Resume Customization Request\n\n"
"## Instructions\n"
f"{instructions_text}\n\n"
"---\n\n"
"## Job Description\n"
f"{job_markdown.strip()}\n\n"
"---\n\n"
"## Current Resume\n"
f"{resume_text}\n"
)
def build_timestamp_dir(base: Path, timestamp: datetime) -> Path:
"""Create (if missing) and return the timestamped directory path."""
path = (
base
/ timestamp.strftime("%Y")
/ timestamp.strftime("%m")
/ timestamp.strftime("%d")
/ timestamp.strftime("%H%M")
)
path.mkdir(parents=True, exist_ok=True)
return path
def sanitize_stem(stem: str) -> str:
"""Replace characters that could interfere with filesystem operations."""
return "".join(ch if ch.isalnum() else "_" for ch in stem) or "resume"
def slugify(component: str) -> str:
"""Turn a free-form string into a filesystem-friendly slug."""
normalized = "".join(
ch.lower() if ch.isalnum() else "-" for ch in component.strip()
)
parts = [part for part in normalized.split("-") if part]
return "-".join(parts)
def run_codex(prompt_text: str, output_path: Path) -> None:
"""Execute the Codex CLI with the provided prompt text."""
output_path.parent.mkdir(parents=True, exist_ok=True)
command = [
"codex",
"exec",
"-",
"--output-last-message",
str(output_path),
"--skip-git-repo-check",
"--sandbox",
"read-only",
]
logging.info("Running Codex CLI command: %s", " ".join(command))
try:
subprocess.run(
command,
input=prompt_text.encode("utf-8"),
check=True,
timeout=CODEX_TIMEOUT_SECONDS,
env=os.environ.copy(),
)
except FileNotFoundError as exc:
raise FatalConfigurationError(
"Codex CLI executable 'codex' not found in PATH"
) from exc
except subprocess.TimeoutExpired as exc:
raise RuntimeError("Codex CLI timed out") from exc
if not output_path.exists():
raise RuntimeError(
f"Codex CLI completed but expected output file {output_path} is missing."
)
def build_normalizer_prompt(raw_text: str) -> str:
"""Construct the prompt for normalizing the raw job description."""
if RESOLVED_NORMALIZER_TEMPLATE is None:
raise FatalConfigurationError("Normalizer template was not resolved during startup.")
instructions = RESOLVED_NORMALIZER_TEMPLATE.read_text(encoding="utf-8").strip()
return (
f"{instructions}\n\n"
"---\n\n"
"## Raw Job Description\n"
"```\n"
f"{raw_text.strip()}\n"
"```\n"
)
def parse_normalized_output(text: str) -> NormalizedJobDescription:
"""Parse the Codex-normalized output into structured pieces."""
lines = text.splitlines()
idx = 0
def next_non_empty(start: int) -> tuple[int, str]:
pos = start
while pos < len(lines):
content = lines[pos].strip()
if content:
return pos, content
pos += 1
raise RuntimeError("Normalized output is missing expected lines.")
idx, company_line = next_non_empty(idx)
if not company_line.lower().startswith("company:"):
raise RuntimeError(f"Expected 'Company:' line, found: {company_line!r}")
company = company_line[len("company:") :].strip()
idx, job_title_line = next_non_empty(idx + 1)
if not job_title_line.lower().startswith("job title:"):
raise RuntimeError(f"Expected 'Job Title:' line, found: {job_title_line!r}")
job_title = job_title_line[len("job title:") :].strip()
idx += 1
while idx < len(lines) and lines[idx].strip():
idx += 1
while idx < len(lines) and not lines[idx].strip():
idx += 1
description_lines = lines[idx:]
description = "\n".join(description_lines).strip()
if not description:
raise RuntimeError("Normalized output did not include a job description section.")
return NormalizedJobDescription(
company=company or "Company",
job_title=job_title or "Role",
description_markdown=description,
)
def normalize_job_description(job_file: Path) -> NormalizedJobDescription:
"""Use Codex to clean and extract metadata from the raw job description."""
raw_text = job_file.read_text(encoding="utf-8", errors="ignore").strip()
if not raw_text:
raise RuntimeError(f"Job description file {job_file.name} is empty after trimming.")
prompt_text = build_normalizer_prompt(raw_text)
with TemporaryDirectory() as tmp_dir_str:
tmp_dir = Path(tmp_dir_str)
output_path = tmp_dir / "normalize_output.md"
run_codex(prompt_text, output_path)
normalized_text = output_path.read_text(encoding="utf-8").strip()
return parse_normalized_output(normalized_text)
def move_with_unique_target(source: Path, destination_dir: Path) -> Path:
"""Move source into destination_dir, avoiding collisions with numeric suffixes."""
destination_dir.mkdir(parents=True, exist_ok=True)
target = destination_dir / source.name
stem = source.stem
suffix = source.suffix
counter = 1
while target.exists():
target = destination_dir / f"{stem}_{counter}{suffix}"
counter += 1
shutil.move(str(source), target)
return target
def process_job(job_file: Path) -> None:
"""Normalize the job description, run Codex, and archive outputs."""
timestamp = datetime.now().astimezone()
out_dir = build_timestamp_dir(OUTBOX, timestamp)
processed_dir = build_timestamp_dir(PROCESSED, timestamp)
resume_path = ensure_single_resume()
normalized = normalize_job_description(job_file)
if RESOLVED_PROMPT_TEMPLATE is None:
raise FatalConfigurationError("Prompt template was not resolved during startup.")
prompt_text = build_prompt_text(
resume_path,
normalized.description_markdown,
RESOLVED_PROMPT_TEMPLATE,
)
safe_company = slugify(normalized.company)
safe_title = slugify(normalized.job_title)
if safe_company and safe_title:
output_stem = f"{safe_company}-{safe_title}"
else:
output_stem = sanitize_stem(job_file.stem)
output_filename = f"{output_stem}.md"
with TemporaryDirectory() as tmp_dir_str:
tmp_dir = Path(tmp_dir_str)
output_path = tmp_dir / "codex_output.md"
run_codex(prompt_text, output_path)
generated_output = out_dir / output_filename
counter = 1
while generated_output.exists():
generated_output = out_dir / f"{output_stem}_{counter}.md"
counter += 1
shutil.move(str(output_path), generated_output)
logging.info(
"Generated customized resume for %s - %s at %s",
normalized.company,
normalized.job_title,
generated_output,
)
prompt_archive = out_dir / f"prompt-{generated_output.stem}.md"
prompt_archive.write_text(prompt_text, encoding="utf-8")
normalized_archive = out_dir / f"job-description-{generated_output.stem}.md"
normalized_archive.write_text(
(
f"Company: {normalized.company}\n"
f"Job Title: {normalized.job_title}\n\n"
"# Job Description\n"
f"{normalized.description_markdown}\n"
),
encoding="utf-8",
)
processed_target = move_with_unique_target(job_file, processed_dir)
logging.info(
"Archived job description %s to %s",
job_file.name,
processed_target,
)
def move_job_to_failed(job_file: Path) -> None:
"""Move the job description into the failed directory."""
if not job_file.exists():
return
failed_target = move_with_unique_target(job_file, FAILED)
logging.info(
"Moved job description %s into failed directory at %s",
job_file.name,
failed_target,
)
def main() -> None:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
try:
ensure_environment()
except FatalConfigurationError as exc:
logging.error("Fatal configuration error: %s", exc)
raise SystemExit(2) from exc
logging.info("Resume customizer watcher started")
while True:
job_paths = sorted(INBOX.iterdir())
try:
job_file = ensure_single_job(job_paths)
except FatalConfigurationError as exc:
logging.error("Fatal configuration error: %s", exc)
raise SystemExit(2) from exc
if job_file is None:
time.sleep(POLL_INTERVAL_SECONDS)
continue
logging.info("Processing job description %s", job_file.name)
try:
process_job(job_file)
except FatalConfigurationError as exc:
logging.error("Fatal configuration error: %s", exc)
move_job_to_failed(job_file)
raise SystemExit(2) from exc
except subprocess.CalledProcessError as exc:
logging.error("Codex CLI failed with non-zero exit status: %s", exc)
move_job_to_failed(job_file)
except Exception as exc: # noqa: BLE001
logging.exception("Unexpected error while processing %s: %s", job_file.name, exc)
move_job_to_failed(job_file)
time.sleep(POLL_INTERVAL_SECONDS)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,3 @@
*
!.gitignore
!.gitkeep

View File

@@ -0,0 +1 @@

3
input/ForCustomizing/inbox/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*
!.gitignore
!.gitkeep

View File

@@ -0,0 +1 @@

119
input/ForCustomizing/jd-test.txt Executable file
View File

@@ -0,0 +1,119 @@
Subject:
URGENT Job Opportunity in Senior IT Engineer in Richardson TX
From:
<mandeep@infotreeglobal.com>
Date:
10/15/2025, 9:04 AM
To:
<reachableceo@reachableceo.com>
Hi,
This side Mandeep from Infotree Global. I have a job opening for you as a Senior IT Engineer in Richardson TX. If you are looking for job change, please share your resume @ mandeep@infotreeglobal.com.
Job Title: Senior IT Engineer
Job Location: Richardson TX
Duration :- 6 months
Responsible for architecting, deploying, and supporting information (IT) and operational technology (OT) solutions for one of our most mission critical manufacturing sites and growing businesses in Richardson, Texas.
Will work closely with cross-functional teams to design, develop, and deploy innovative infrastructure and software solutions to digitize and optimize semiconductor related manufacturing and business operations.
The Full Stack Engineer will be responsible for operational support of Server Infrastructure both virtual and physical. You will work in VMware, Windows & Linux Server Architecture & Administration. You will be helping to deploy and support our initial Kubernetes Container-based application environments and to grow that technology base.
This site will be leveraging increasing AI based semiconductor centric automation, equipment integration, fault detection (FDC), and advanced process control (APC) solutions within manufacturing that will have this role deploying and supporting bleeding edge infrastructure.
Key Responsibilities
• Operational support of on-premises server stacks and technology. Working with Corporate IT teams to maintain as much to standards as possible but focused on the tier 1 24 X 7 mission critical needs of the site.
• Work with Hyper-Converged Infrastructure technology platforms such as Dell VXRail and newer.
• Troubleshoot performance issues related to virtualized and physical server workloads.
• Facilitate implementation of new technology stacks and migration of workloads from physical servers to virtualized instances wherever possible.
• Work with semiconductor process and equipment vendors to continually evolve infrastructure for the best performance and fault tolerance.
• Participate in calls / meetings with site and corporate team members to address and troubleshoot application and or infrastructure related issues.
• Participate in daily tier meetings with factory systems and manufacturing operations teams.
• Work with Corporate IT and Honeywell Global Security to maintain all infrastructure to corporate and semiconductor cyber security standards.
MUST HAVE
• Bachelors Degree
• 5+ years of experience in operational support of enterprise server ecosystems
• 5+ years of experience with Windows Server OS and RHEL LINUX 5-9.X
• 3+ years of experience with ServiceNow or another similar formal Service Management platform and/or methodology
• 3+ years of experience with virtualization platforms such as Vmware,, vSAN, Vsphere, and VCF/NSX
• 1+ years of experience with Docker and Kubernetes for container orchestration.
• Experience with one or more of the following: Server automation activities or Infrastructure as code - Tanium, CHEF, JSON, Powershell, Ruby, or other scripting languages.
• Experience with network fundamentals such as - Firewall configuration, VPN setup, VLANs, subnetting, IP addressing, DNS, DHCP, NAT, routing, and switching.
• Experience with backup, disaster recovery, and technology resilience strategies / processes.
• Must demonstrate proficiency in documenting infrastructure, automation scripts, and system configurations to ensure clarity, maintainability, and operational continuity.
VALUED
• Advanced Degree preferably in Information Technology, Computer Science, or Computer Engineering.
• Experience with Windows patching tools such as Tanium and Windows security standards & OS Hardening concepts
• Experience in all aspects of the software development lifecycle including requirements gathering, design, coding, testing and production support
• Experience with support of Network infrastructure such as Cisco Nexus & Catalyst switches
• Experience with support of contemporary data recovery products such as Rubrik
• Experience with support of IT/OT systems and applications, including network implementation & segmentation, equipment integration and automation, and manufacturing execution systems (MES)
• Understanding of supervisory control and data acquisition (SCADA), manufacturing focused Windows-based PCs and servers, and industrial network architectures and how to effectively support these technologies.
• The ability to be a change agent and promote positive change in the organization
• Excellent oral, written and collaborative communication skills.
• Skilled in partnering with internal customers at all levels to define problems, identify solutions, and facilitate change.
• A self-starting, self-driven individual, working to remain motivated and efficient with minimum oversight..
Kind Regards,
Mandeep Singh
Phone: 734-228-4349 ext. 1105 FAX: 734-927-9390
EMAIL: Mandeep@infotreeglobal.com globalwww.infotreeglobal.com
Mailing Address: 215 Ann Arbor Rd. Suite 304 Plymouth, MI 48170
logo_3
Follow Us On:
Please consider the environment before printing this e-mail.

View File

@@ -0,0 +1,3 @@
*
!.gitignore
!.gitkeep

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,3 @@
*
!.gitignore
!.gitkeep

View File

@@ -0,0 +1 @@

45
input/README.md Normal file
View File

@@ -0,0 +1,45 @@
# Input Pipeline Overview
The input side of ResumeCustomizer prepares job-specific Markdown resumes by stitching together the base resume, a job-description Markdown file, and the shared instruction prompt, then invoking the Codex CLI inside a containerized watcher.
## Workflow Recap
1. Ensure `input/resume/` contains exactly one Markdown resume.
2. Drop a single job-description file (plain text or Markdown) into `input/ForCustomizing/inbox/`.
3. Start the watcher stack (`input/Docker/run-input-processor.sh up -d`).
4. The watcher normalizes the messy job description via Codex (using the baked-in normalizer prompt), stripping recruiter chatter while preserving every job-related detail, then combines the cleaned Markdown, the base resume, and the resolved customization prompt into a second Codex run that writes the generated resume to `ForCustomizing/outbox/YYYY/MM/DD/HHMM/<company>-<jobtitle>.md`.
5. Successful runs archive the job description under `ForCustomizing/processed/`, copy both the prompt and the cleaned job description into the same outbox folder, and leave the Codex output for human review. Failures move the job description into `ForCustomizing/failed/`.
The human operator reviews the Codex output Markdown, makes any edits, and then manually hands it off to the output pipeline for document rendering.
## Container Stack
The watcher lives in `input/Docker/`:
- `Dockerfile` builds a Node/Python base image, installs the Codex CLI, and prepares a non-root `codex` user.
- `watch_and_customize.py` polls the inbox, validates preconditions, resolves both templates (normalizer and customization), cleans the job description, builds prompts, runs Codex twice, and routes files.
- `entrypoint.sh` maps the container user to the callers UID/GID and ensures shared directories exist.
- `run-input-processor.sh` wrapper around `docker compose` that resolves your Codex config directory (`${HOST_CODEX_DIR:-${HOME}/.codex}`) and forwards CLI arguments.
- `docker-compose.yml` defines the container, volumes (including machine-id bindings required by the Codex CLI), environment variables, and restart policy (`no` so fatal errors halt the stack).
### Templates
- baked normalizer prompt (`input/Docker/JobDescriptionNormalizerPrompt.md`) ships with the container; customize by editing the image.
- `templates/ResumeCustomizerPrompt.md.example` ships with default resume-customization instructions. Copy it to `ResumeCustomizerPrompt.md` to override.
- The `.gitignore` in `templates/` keeps local overrides out of version control.
### Codex CLI
- The container image installs the official Codex CLI and expects credentials/configuration from the mounted `~/.codex` directory on the host.
- The watcher pipes prompts directly to `codex exec - --output-last-message <path> --skip-git-repo-check --sandbox read-only`, so no additional command-line customization is required from operators.
### Key Environment Variables
- `POLL_INTERVAL_SECONDS` watch loop delay (defaults to 5).
- `CODEX_TIMEOUT_SECONDS` wall-clock timeout for each Codex call (defaults to 600).
- `HOST_CODEX_DIR` host path that mounts into `/home/codex/.codex` (defaults to `${HOME}/.codex` via the wrapper).
### Prerequisites
- Docker Engine with the Compose plugin (`docker compose`) or the standalone `docker-compose` binary.
- A working Codex CLI and credentials in `~/.codex`. The Docker build attempts `npm install --location=global codex-cli`; override or update as needed if packages change.
## Failure Modes
- **Multiple resumes or job descriptions** watcher exits immediately with a fatal configuration error. Fix the files and restart.
- **Codex CLI missing or failing** job description moves to `ForCustomizing/failed/`; inspect container logs, resolve, and requeue.
- **Timeouts** treat as failures; adjust `CODEX_TIMEOUT_SECONDS` if Codex regularly needs more time.
For day-to-day operating guidelines, see `input/AGENTS.md`.

2
input/templates/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
ResumeCustomizerPrompt.md
!ResumeCustomizerPrompt.md.example

View File

@@ -1,3 +1,7 @@
# Default instructions for Codex-driven resume customization. Copy this file to
# `ResumeCustomizerPrompt.md` (without the `.example` suffix) and edit it to
# override the defaults for your workflow.
I need you to customize my resume for this job description:
. “Act like a recruiter in information technology. Whats missing from this résumé that would stop you from reaching out?”
@@ -7,6 +11,3 @@ I need you to customize my resume for this job description:
. “Add ATS keywords from this job post without sounding robotic.”
. “Format this résumé so its easy to scan, clean to read, and works on any system.”
. “Write a short, punchy message I can DM a hiring manager. No desperation, just value.”