diff --git a/input/AGENTS.md b/input/AGENTS.md new file mode 100644 index 0000000..77cc08c --- /dev/null +++ b/input/AGENTS.md @@ -0,0 +1,45 @@ +# Input Agent Guide + +## Mission +Automate the upstream resume customization workflow. Monitor the job-description inbox, combine the base resume and prompt template into a Codex-friendly request, 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 job-description Markdown at a time to trigger processing. +- `ForCustomizing/outbox/YYYY/MM/DD/HHMM/` – timestamped folders containing Codex output Markdown (and a copy of the prompt used). +- `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. +- `templates/ResumeCustomizerPrompt.md.example` – default instruction block; copy to `ResumeCustomizerPrompt.md` (same folder) to override locally. + +## 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: +- `CODEX_COMMAND_TEMPLATE` – override the Codex CLI invocation (defaults to `codex prompt --input {prompt} --output {output} --format markdown`). +- `POLL_INTERVAL_SECONDS` – watcher polling cadence (default `5`). +- `CODEX_TIMEOUT_SECONDS` – hard timeout for Codex calls (default `600`). +- `CODEX_CONFIG_DIR` – optional override for the host directory that should mount into `/home/codex/.codex`. + +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 caller’s 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. +- To change the Codex command format, pass a quoted template (e.g., `CODEX_COMMAND_TEMPLATE='codex run --input {prompt} --output {output}' ./run-input-processor.sh up -d'`). The template must include `{prompt}` and `{output}` placeholders. diff --git a/input/Docker/Dockerfile b/input/Docker/Dockerfile new file mode 100644 index 0000000..8898250 --- /dev/null +++ b/input/Docker/Dockerfile @@ -0,0 +1,29 @@ +FROM node:20-bookworm + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PUID=1000 \ + PGID=1000 \ + CODEX_HOME=/home/codex + +RUN apt-get update \ + && apt-get install --yes --no-install-recommends \ + python3 \ + python3-venv \ + gosu \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +RUN npm install --location=global codex-cli || true + +RUN groupadd -r codex && \ + useradd -r -m -g codex -s /bin/bash codex + +WORKDIR /app + +COPY watch_and_customize.py entrypoint.sh ./ + +RUN chmod +x /app/watch_and_customize.py /app/entrypoint.sh + +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/input/Docker/docker-compose.yml b/input/Docker/docker-compose.yml new file mode 100644 index 0000000..01b3576 --- /dev/null +++ b/input/Docker/docker-compose.yml @@ -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: + PUID: "${LOCAL_UID:-1000}" + PGID: "${LOCAL_GID:-1000}" + POLL_INTERVAL_SECONDS: "${POLL_INTERVAL_SECONDS:-5}" + CODEX_TIMEOUT_SECONDS: "${CODEX_TIMEOUT_SECONDS:-600}" + CODEX_COMMAND_TEMPLATE: "${CODEX_COMMAND_TEMPLATE:-codex prompt --input {prompt} --output {output} --format markdown}" + volumes: + - ../ForCustomizing/inbox:/workspace/inbox + - ../ForCustomizing/outbox:/workspace/outbox + - ../ForCustomizing/processed:/workspace/processed + - ../ForCustomizing/failed:/workspace/failed + - ../resume:/workspace/resume:ro + - ../templates:/templates:ro + - ${CODEX_CONFIG_DIR:-/workspace/.codex}:/home/codex/.codex diff --git a/input/Docker/entrypoint.sh b/input/Docker/entrypoint.sh new file mode 100755 index 0000000..6e4bc77 --- /dev/null +++ b/input/Docker/entrypoint.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +USER_NAME=codex +PUID=${PUID:-1000} +PGID=${PGID:-1000} + +ensure_group() { + local desired_gid=$1 + local group_name + + if getent group "${desired_gid}" >/dev/null 2>&1; then + group_name=$(getent group "${desired_gid}" | cut -d: -f1) + echo "${group_name}" + return 0 + fi + + if getent group "${USER_NAME}" >/dev/null 2>&1; then + groupmod -o -g "${desired_gid}" "${USER_NAME}" + echo "${USER_NAME}" + return 0 + fi + + groupadd -o -g "${desired_gid}" "${USER_NAME}" + echo "${USER_NAME}" +} + +ensure_user() { + local desired_uid=$1 + local primary_group=$2 + + if getent passwd "${USER_NAME}" >/dev/null 2>&1; then + usermod -o -u "${desired_uid}" -g "${primary_group}" -d "/home/${USER_NAME}" -s /bin/bash "${USER_NAME}" + else + useradd -o -m -u "${desired_uid}" -g "${primary_group}" -s /bin/bash "${USER_NAME}" + fi +} + +GROUP_NAME=$(ensure_group "${PGID}") +ensure_user "${PUID}" "${GROUP_NAME}" + +USER_HOME=$(eval echo "~${USER_NAME}") + +mkdir -p /workspace/inbox /workspace/outbox /workspace/processed /workspace/failed +mkdir -p "${USER_HOME}/.codex" + +for path in /workspace/inbox /workspace/outbox /workspace/processed /workspace/failed "${USER_HOME}" "${USER_HOME}/.codex"; do + if [ -e "${path}" ]; then + chown -R "${PUID}:${PGID}" "${path}" + fi +done + +export HOME="${USER_HOME}" +export XDG_CACHE_HOME="${USER_HOME}/.cache" +mkdir -p "${XDG_CACHE_HOME}" +chown -R "${PUID}:${PGID}" "${XDG_CACHE_HOME}" + +exec gosu "${PUID}:${PGID}" python3 /app/watch_and_customize.py diff --git a/input/Docker/run-input-processor.sh b/input/Docker/run-input-processor.sh new file mode 100755 index 0000000..a6e06d2 --- /dev/null +++ b/input/Docker/run-input-processor.sh @@ -0,0 +1,35 @@ +#!/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 -v 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 + +CALLER_UID=$(id -u) +CALLER_GID=$(id -g) + +DEFAULT_CODEX_CONFIG="${HOME}/.codex" +CODEX_CONFIG_DIR=${CODEX_CONFIG_DIR:-${DEFAULT_CODEX_CONFIG}} + +mkdir -p "${CODEX_CONFIG_DIR}" + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +( + cd "${SCRIPT_DIR}" + LOCAL_UID="${CALLER_UID}" \ + LOCAL_GID="${CALLER_GID}" \ + CODEX_CONFIG_DIR="${CODEX_CONFIG_DIR}" \ + "${COMPOSE_CMD[@]}" "$@" +) diff --git a/input/Docker/watch_and_customize.py b/input/Docker/watch_and_customize.py new file mode 100755 index 0000000..c960555 --- /dev/null +++ b/input/Docker/watch_and_customize.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 +""" +Monitor the customization inbox for job description Markdown files and run the Codex CLI +to produce tailored resumes. + +The script expects exactly one base resume Markdown file and processes one job file at a +time. After a successful Codex run, the generated resume is written into a timestamped +outbox folder and the job description is archived under processed/. Failures move the +job description into failed/. +""" + +from __future__ import annotations + +import logging +import os +import shlex +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" + +POLL_INTERVAL_SECONDS = int(os.environ.get("POLL_INTERVAL_SECONDS", "5")) +CODEX_COMMAND_TEMPLATE = os.environ.get( + "CODEX_COMMAND_TEMPLATE", + "codex prompt --input {prompt} --output {output} --format markdown", +) +CODEX_TIMEOUT_SECONDS = int(os.environ.get("CODEX_TIMEOUT_SECONDS", "600")) + +RESOLVED_PROMPT_TEMPLATE: Path | None = None + + +class FatalConfigurationError(RuntimeError): + """Raised when the watcher encounters a non-recoverable configuration problem.""" + + +@dataclass(frozen=True) +class MarkdownInputs: + resume: Path + job_description: Path + prompt_template: Path + + +def ensure_environment() -> None: + """Verify required directories and template assets exist.""" + global RESOLVED_PROMPT_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_prompt_template( + PROMPT_TEMPLATE, + PROMPT_TEMPLATE_EXAMPLE, + TEMPLATE_CACHE, + ) + + +def resolve_prompt_template(primary: Path, example: Path, cache_dir: Path) -> Path: + """Return the prompt 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"Prompt template 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(md_files: Sequence[Path]) -> Path | None: + """Validate there is at most one job description file.""" + if not md_files: + return None + + if len(md_files) > 1: + names = ", ".join(p.name for p in md_files) + raise FatalConfigurationError( + f"Multiple job description files detected in inbox: {names} " + "— expected exactly one." + ) + + return md_files[0] + + +def read_inputs(job_file: Path) -> MarkdownInputs: + """Gather and return all markdown inputs required for the prompt.""" + resume = ensure_single_resume() + + missing = [str(path) for path in (job_file,) if not path.exists()] + if missing: + raise FatalConfigurationError( + "Required files disappeared before processing: " + ", ".join(missing) + ) + + if RESOLVED_PROMPT_TEMPLATE is None: + raise FatalConfigurationError("Prompt template was not resolved during startup.") + + return MarkdownInputs( + resume=resume, + job_description=job_file, + prompt_template=RESOLVED_PROMPT_TEMPLATE, + ) + + +def build_prompt_text(inputs: MarkdownInputs) -> str: + """Return the combined prompt string fed to the Codex CLI.""" + resume_text = inputs.resume.read_text(encoding="utf-8").strip() + jd_text = inputs.job_description.read_text(encoding="utf-8").strip() + instructions_text = inputs.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"{jd_text}\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 run_codex(prompt_path: Path, output_path: Path) -> None: + """Execute the Codex CLI using the configured command template.""" + command_text = CODEX_COMMAND_TEMPLATE.format( + prompt=str(prompt_path), + output=str(output_path), + ) + logging.info("Running Codex CLI command: %s", command_text) + + try: + command = shlex.split(command_text) + except ValueError as exc: + raise FatalConfigurationError( + f"Unable to parse CODEX_COMMAND_TEMPLATE into arguments: {exc}" + ) from exc + + try: + subprocess.run( + command, + check=True, + timeout=CODEX_TIMEOUT_SECONDS, + env=os.environ.copy(), + ) + except FileNotFoundError as exc: + raise FatalConfigurationError( + f"Executable not found while running Codex CLI command: {command[0]}" + ) 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 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: + """Combine inputs, run Codex, and archive outputs.""" + timestamp = datetime.now().astimezone() + out_dir = build_timestamp_dir(OUTBOX, timestamp) + processed_dir = build_timestamp_dir(PROCESSED, timestamp) + + inputs = read_inputs(job_file) + prompt_text = build_prompt_text(inputs) + + safe_resume_stem = sanitize_stem(inputs.resume.stem) + safe_job_stem = sanitize_stem(job_file.stem) + output_filename = f"{safe_resume_stem}-for-{safe_job_stem}.md" + + with TemporaryDirectory() as tmp_dir_str: + tmp_dir = Path(tmp_dir_str) + prompt_path = tmp_dir / "prompt.md" + prompt_path.write_text(prompt_text, encoding="utf-8") + + output_path = tmp_dir / "codex_output.md" + + run_codex(prompt_path, output_path) + + generated_output = out_dir / output_filename + counter = 1 + while generated_output.exists(): + generated_output = out_dir / f"{safe_resume_stem}-for-{safe_job_stem}_{counter}.md" + counter += 1 + + shutil.move(str(output_path), generated_output) + logging.info("Generated customized resume at %s", generated_output) + + prompt_archive = out_dir / f"prompt-{safe_job_stem}.md" + prompt_archive.write_text(prompt_text, 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_files = sorted(INBOX.glob("*.md")) + + try: + job_file = ensure_single_job(job_files) + 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() diff --git a/input/README.md b/input/README.md new file mode 100644 index 0000000..3406c32 --- /dev/null +++ b/input/README.md @@ -0,0 +1,41 @@ +# 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 Markdown into `input/ForCustomizing/inbox/`. +3. Start the watcher stack (`input/Docker/run-input-processor.sh up -d`). +4. The watcher combines the resume, job description, and the resolved instruction prompt (defaulting to `templates/ResumeCustomizerPrompt.md.example`) into a prompt, runs the Codex CLI, and writes the generated resume to `ForCustomizing/outbox/YYYY/MM/DD/HHMM/`. +5. Successful runs archive the job description under `ForCustomizing/processed/` and copy the prompt used into the same outbox folder. Failures move the job description into `ForCustomizing/failed/` for review. + +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 gosu, and prepares a non-root `codex` user. +- `watch_and_customize.py` – polls the inbox, validates preconditions, resolves the prompt template (`ResumeCustomizerPrompt.md` or its `.example` fallback), constructs prompts, runs Codex, and routes files. +- `entrypoint.sh` – maps the container user to the caller’s UID/GID and ensures shared directories exist. +- `run-input-processor.sh` – wrapper around `docker compose` that mounts your `~/.codex` directory and forwards CLI arguments. +- `docker-compose.yml` – defines the container, volumes, environment variables, and restart policy (`no` so fatal errors halt the stack). + +### Templates +- `templates/ResumeCustomizerPrompt.md.example` ships with default Codex instructions. +- To customize, copy the `.example` file to `templates/ResumeCustomizerPrompt.md` (the `.gitignore` keeps your local overrides out of version control). + +### Key Environment Variables +- `CODEX_COMMAND_TEMPLATE` – format string for invoking Codex (placeholders: `{prompt}`, `{output}`). +- `POLL_INTERVAL_SECONDS` – watch loop delay (defaults to 5). +- `CODEX_TIMEOUT_SECONDS` – wall-clock timeout for each Codex call (defaults to 600). +- `CODEX_CONFIG_DIR` – host path to mount as `/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`. diff --git a/input/questionsForHuman.md b/input/questionsForHuman.md new file mode 100644 index 0000000..abebbc2 --- /dev/null +++ b/input/questionsForHuman.md @@ -0,0 +1,46 @@ +# Input Pipeline Clarifications + +Please add answers inline after each question. + +1. **Codex execution:** Should the input-side container actually run the `codex` CLI to generate the customized resume, or simply assemble the combined prompt file for manual execution? If automated, what exact command, flags, and expected output path should it use? + +Yes please invoke the codex cli . + +I want it to run non interactively and in an automated fashion. I am open to you building a cli / process todo that . I am sure we will need to iterate a bit. + +It should output the returned markdown file to ForCustomizing/outbox/YYYY/MM/DD/HHMM (using local system time) and move the .md file from ForCustomizing/inbox to ForCustomizing/processed/YYYY/MMD/DD/HHMM (if succesful) + +On failure it should put the .md file into failed + + +2. **Resume selection:** How should the watcher choose the base resume when multiple Markdown files live in `input/resume/`? Will there always be exactly one current resume, or should job-description filenames reference a specific variant? + +Only one .md file should exist. Any other condition is a fatal error and the input stack should halt and inform the user of multiple input resumes. +Same for if multiple .md files ever exist in ForCustomizing/inbox . + + +3. **Post-processing job descriptions:** After a job description is handled, where should its source Markdown move? Should successes land in `input/ForCustomizing/processed/` and failures in `input/ForCustomizing/failed/`, mirroring the output pipeline? + +Yes exactly + +4. **Parallel jobs:** Do you want the watcher to enforce “exactly one job file at a time” in `input/ForCustomizing/inbox`, or process multiple sequentially if several appear? + +Only one file and one job at a time. Anything else is a fatal error and should halt the line. + + +5. **Customized resume location:** Where should the generated Codex output (or combined prompt) be saved for human review—timestamped folders under `input/ForCustomizing/outbox/`, a flat directory, or another structure? + + +See my answer to question 1. + +To recap outbox/YYYY/MM/DD/HHMM (using local system time) + +6. **Container environment:** Is there a preferred base image or existing Codex CLI installation to assume for the input container? When mounting `~/.codex`, should it be attached to `/root/.codex` or a different path the CLI expects? + +Great question. + +I guess a generic debian image that can npm install codex however the official codex docs say to install it? It will need to be able to update the cli as well, as the cli is frequently updated. I'm fine with this being a "fat" container. + +I do not want any root user anywhere. Look at the output stack for how it has a little bash wrapper script to handle uid/gid mapping? + +I guess create a dedicated non root user in the container to run the watcher and invoke codex? Open to suggestions here? diff --git a/input/templates/.gitignore b/input/templates/.gitignore new file mode 100644 index 0000000..7b5c605 --- /dev/null +++ b/input/templates/.gitignore @@ -0,0 +1,2 @@ +ResumeCustomizerPrompt.md +!ResumeCustomizerPrompt.md.example diff --git a/input/codex-prompt.md b/input/templates/ResumeCustomizerPrompt.md.example similarity index 78% rename from input/codex-prompt.md rename to input/templates/ResumeCustomizerPrompt.md.example index ee4b945..001ace8 100644 --- a/input/codex-prompt.md +++ b/input/templates/ResumeCustomizerPrompt.md.example @@ -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. What’s 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 it’s 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.” - - -