Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .claude/skills/career-ops/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: career-ops
description: AI job search command center -- evaluate offers, generate CVs, scan portals, track applications
user_invocable: true
args: mode
argument-hint: "[scan | deep | pdf | oferta | ofertas | apply | batch | tracker | pipeline | contacto | training | project | interview-prep | update]"
argument-hint: "[scan | deep | pdf | oferta | ofertas | apply | batch | tracker | pipeline | contacto | training | project | interview-prep | update | visa-status]"
---

# career-ops -- Router
Expand All @@ -30,6 +30,7 @@ Determine the mode from `{{mode}}`:
| `batch` | `batch` |
| `patterns` | `patterns` |
| `followup` | `followup` |
| `visa-status` | `visa-status` |

**Auto-pipeline detection:** If `{{mode}}` is not a known sub-command AND contains JD text (keywords: "responsibilities", "requirements", "qualifications", "about the role", "we're looking for", company name + role) or a URL to a JD, execute `auto-pipeline`.

Expand Down Expand Up @@ -59,7 +60,8 @@ Available commands:
/career-ops scan → Scan portals and discover new offers
/career-ops batch → Batch processing with parallel workers
/career-ops patterns → Analyze rejection patterns and improve targeting
/career-ops followup → Follow-up cadence tracker: flag overdue, generate drafts
/career-ops followup → Follow-up cadence tracker: flag overdue, generate drafts
/career-ops visa-status → OPT timeline dashboard (expiration, unemployment, cap season)

Inbox: add URLs to data/pipeline.md → /career-ops pipeline
Or paste a JD directly to run the full pipeline.
Expand All @@ -79,7 +81,7 @@ Applies to: `auto-pipeline`, `oferta`, `ofertas`, `pdf`, `contacto`, `apply`, `p
### Standalone modes (only their mode file):
Read `modes/{mode}.md`

Applies to: `tracker`, `deep`, `training`, `project`, `patterns`, `followup`
Applies to: `tracker`, `deep`, `training`, `project`, `patterns`, `followup`, `visa-status`

### Modes delegated to subagent:
For `scan`, `apply` (with Playwright), and `pipeline` (3+ URLs): launch as Agent with the content of `_shared.md` + `modes/{mode}.md` injected into the subagent prompt.
Expand Down
22 changes: 22 additions & 0 deletions batch/batch-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,27 @@ Analyze posting signals to assess whether this is a real, active opening.

**Assessment:** Apply the same three tiers (High Confidence / Proceed with Caution / Suspicious), weighting available signals more heavily. If insufficient signals are available to make a determination, default to "Proceed with Caution" with a note about limited data.

#### OPT Timeline Status (solo si config/visa.yml tiene seccion opt:)

Si `config/visa.yml` tiene seccion `opt:` configurada, ejecutar:
`echo '{"jdText":"<full JD text>"}' | node opt-timeline.mjs --json`
Comment on lines +159 to +160
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the relevant files
git ls-files | grep -E "(batch-prompt|oferta|opt-timeline)" | head -20

Repository: santifer/career-ops

Length of output: 172


🏁 Script executed:

# Check batch/batch-prompt.md content around lines 159-160
if [ -f "batch/batch-prompt.md" ]; then
  echo "=== batch/batch-prompt.md (lines 150-175) ==="
  sed -n '150,175p' batch/batch-prompt.md
fi

Repository: santifer/career-ops

Length of output: 1969


🏁 Script executed:

# Check if opt-timeline.mjs exists and examine it
if [ -f "batch/opt-timeline.mjs" ]; then
  echo "=== opt-timeline.mjs (first 50 lines) ==="
  head -50 batch/opt-timeline.mjs
fi

Repository: santifer/career-ops

Length of output: 45


🏁 Script executed:

# Check modes/oferta.md for similar pattern
if [ -f "modes/oferta.md" ]; then
  echo "=== modes/oferta.md - searching for opt-timeline pattern ==="
  grep -n "opt-timeline" modes/oferta.md | head -10
fi

Repository: santifer/career-ops

Length of output: 195


🏁 Script executed:

# Check if opt-timeline.mjs exists and examine its full content
if [ -f "batch/opt-timeline.mjs" ]; then
  wc -l batch/opt-timeline.mjs
  echo "=== opt-timeline.mjs ==="
  cat -n batch/opt-timeline.mjs
elif [ -f "opt-timeline.mjs" ]; then
  wc -l opt-timeline.mjs
  echo "=== opt-timeline.mjs ==="
  cat -n opt-timeline.mjs
fi

Repository: santifer/career-ops

Length of output: 35699


🏁 Script executed:

# Search for opt-timeline.mjs in the repository
find . -name "opt-timeline.mjs" -type f 2>/dev/null

Repository: santifer/career-ops

Length of output: 81


🏁 Script executed:

# Search for references to how opt-timeline.mjs should be called
rg "opt-timeline" -A 3 -B 1 --type md

Repository: santifer/career-ops

Length of output: 1964


Use jq to safely construct the OPT JSON payload instead of echo with string interpolation.

The pattern echo '{"jdText":"<full JD text>"}' | node opt-timeline.mjs --json will fail on common JD content such as quoted requirements, newlines, and special characters. Use: jq -n --arg jd "$jdText" '{"jdText": $jd}' | node opt-timeline.mjs --json (or equivalent JSON-safe method) to properly escape all input.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@batch/batch-prompt.md` around lines 159 - 160, Reemplaza la instrucción
actual que usa echo con interpolación literal (la que termina en node
opt-timeline.mjs --json) por una construcción JSON segura usando jq: invoca jq
-n con --arg para pasar el texto completo del JD como una variable y construye
el objeto {"jdText": $jd} con jq, luego pipea la salida a node opt-timeline.mjs
--json; esto evitará fallos con comillas, saltos de línea y caracteres
especiales al procesar jdText.


Con el resultado JSON, mostrar este banner despues de la tabla de legitimacy:

> **OPT STATUS:** {remainingDays} days remaining (expires {endDate})
> Unemployment: {unemployment.used}/{unemployment.limit} days used ({unemployment.remaining} remaining)
> **Cap Season:** {capSeason.phase}. {capSeason.advice}
> **Time-to-Hire:** {tthEstimate.type} company, est. {tthEstimate.minDays}-{tthEstimate.maxDays} days. Your OPT window: {remainingDays} days. {tthEstimate.warning || "Within range."}
Comment on lines +162 to +167
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 | 🟠 Major

Render the Time-to-Hire line only when tthEstimate exists.

The JSON contract makes tthEstimate optional, so this template currently assumes fields that may be absent. Without a guard, batch output will degrade into undefined text for some postings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@batch/batch-prompt.md` around lines 162 - 167, The Time-to-Hire line in the
OPT banner currently assumes tthEstimate is present and can output "undefined"
when it's missing; update the banner rendering logic so the entire Time-to-Hire
line (the text using {tthEstimate.type}, {tthEstimate.minDays},
{tthEstimate.maxDays}, and {tthEstimate.warning}) is only included if
tthEstimate exists, e.g., wrap that line in a conditional check for tthEstimate
before interpolating; ensure other banner lines (OPT STATUS, Unemployment, Cap
Season) remain unchanged.


**Warning escalation:**
- Si unemployment.severity == 'urgent' (<=14 days): Prefijo `URGENT`, lenguaje fuerte: "CRITICAL: Only {remaining} unemployment days left. Immediate employment required."
- Si unemployment.severity == 'warning' (<=30 days): Prefijo `WARNING`, lenguaje firme: "WARNING: {remaining} unemployment days remaining. Accelerate job search."
- Si unemployment.severity == 'info' (<=60 days): Nota informativa: "Note: {remaining} unemployment days remaining. Monitor closely."

**Nota:** opt-timeline.mjs es pura computacion (no Playwright), seguro para batch mode.

Si `opt:` no esta configurada en visa.yml, omitir esta subseccion silenciosamente.

#### Score Global

| Dimensión | Score |
Expand Down Expand Up @@ -335,6 +356,7 @@ Al terminar, imprime por stdout un resumen JSON para que el orquestador lo parse
"role": "{rol}",
"score": {score_num},
"legitimacy": "{High Confidence|Proceed with Caution|Suspicious}",
"optStatus": { "remainingDays": null, "unemploymentRemaining": null, "capPhase": null },
"pdf": "{ruta_pdf}",
"report": "{ruta_report}",
"error": null
Expand Down
38 changes: 38 additions & 0 deletions config/visa.example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Career-Ops Visa Configuration
# Copy this file to config/visa.yml to activate visa features.
# This file is the template -- edit visa.yml with your actual data.
# Your visa.yml is user-layer data and will never be overwritten by updates.

# --- Sponsorship Mode (Phase 3) ---
# Controls how visa sponsorship signals affect evaluations:
# hard_filter: Auto-SKIP JDs with WONT_SPONSOR (saves tokens)
# score_penalty: Apply penalty to overall score
# info_only: Show visa info without affecting score (default)
sponsorship_mode: info_only

# Penalty values for score_penalty mode
penalties:
wont_sponsor: -0.7
unknown: -0.3

# --- OPT Timeline (Phase 4) ---
# Activate by filling in your OPT details below.
# type: regular (12 months) or stem (36 months from initial OPT start)
# start_date: Your OPT start date in YYYY-MM-DD format
# end_date: (optional) Override auto-calculated expiration
# unemployment_days_used: Manual counter -- update this yourself
# h1b_lottery_status: selected | not_selected | pending (optional, affects cap season advice)
opt:
type: stem
start_date: "2025-06-01"
# end_date: "2028-05-31"
unemployment_days_used: 0
# h1b_lottery_status: pending
Comment on lines +25 to +30
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 | 🟠 Major

Comment out the sample opt values so setup fails closed.

As written, copying this template verbatim already satisfies the opt: contract, so the module will start producing countdowns from bogus sample data if the user forgets to edit it. Please leave this block commented or use obviously invalid placeholders instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/visa.example.yml` around lines 25 - 30, The sample opt block currently
provides valid-looking values (opt.type, opt.start_date, unemployment_days_used)
which satisfy the config contract and can cause false countdowns; either comment
out the entire opt block or replace its fields (opt.type, opt.start_date,
unemployment_days_used, h1b_lottery_status) with clearly invalid placeholders
(e.g., "<REQUIRED>" or empty/null) so the config fails closed until the user
supplies real values; ensure the opt key remains absent or clearly disabled in
the example so the module cannot accidentally load sample data.


# --- Time-to-Hire Defaults (Phase 4, optional) ---
# Override heuristic estimates by company type [min_days, max_days]
# Defaults if omitted: startup [14,28], midsize [28,56], enterprise [56,112]
time_to_hire_defaults:
startup: [14, 28]
midsize: [28, 56]
enterprise: [56, 112]
96 changes: 96 additions & 0 deletions docs/VISA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# F-1 Visa Module

Career-ops extension for F-1 international students: visa sponsorship detection, H-1B sponsor history lookup, OPT timeline tracking, and proactive company discovery.

## Overview

The visa module transforms career-ops from "find good companies" into "predict and maximize hiring probability" for F-1 OPT/STEM OPT holders. It combines:

- **JD signal detection** -- Identify sponsorship language in job descriptions
- **H-1B filing history** -- Look up actual USCIS data for employer sponsorship track record
- **Visa score** -- Composite visa-friendliness rating (1-5)
- **OPT timeline tracking** -- Expiration countdown, unemployment day limits, cap season awareness

## Setup

1. Copy `config/visa.example.yml` to `config/visa.yml`
2. Fill in your details (sponsorship preferences, OPT dates, etc.)
3. The visa module activates automatically when `config/visa.yml` exists

## OPT Timeline Tracking

Track your F-1 OPT status, unemployment days, and H-1B cap season timing.

### OPT Setup

1. Copy `config/visa.example.yml` to `config/visa.yml` (if not done already)
2. Fill in the `opt:` section:

```yaml
opt:
type: stem # regular (12 months) or stem (36 months)
start_date: "2025-06-01" # Your OPT start date
unemployment_days_used: 0 # Update manually as needed
# h1b_lottery_status: pending # Optional: selected | not_selected | pending
```

### Quick Status

Run `/career-ops visa-status` to see your OPT dashboard:
- Days remaining until OPT expiration
- Unemployment days used vs limit (90 regular, 150 STEM)
- Current H-1B cap season phase with actionable advice
- Next key deadline

### Automatic Warnings

When configured, OPT warnings appear automatically in:
- **Evaluations** (Block G) -- includes time-to-hire estimate vs your remaining OPT window
- **Batch evaluations** -- same warnings, works in headless mode
- **Scan results** -- one-line OPT status summary at top

### Warning Thresholds

| Unemployment Days Remaining | Level |
|----------------------------|-------|
| <= 60 days | Info note |
| <= 30 days | Warning |
| <= 14 days | URGENT |

### Time-to-Hire Estimates

The system estimates hiring timelines by company type:
- **Startup** (Series A/B, <100 employees): 2-4 weeks
- **Mid-size**: 4-8 weeks
- **Enterprise** (Fortune 500, 5000+ employees): 8-16 weeks

Customize in `config/visa.yml` under `time_to_hire_defaults:`.

### CLI Usage

```bash
node opt-timeline.mjs # Human-readable dashboard
node opt-timeline.mjs --test # Run built-in tests
node opt-timeline.mjs --json # JSON output for pipeline integration
```

## H-1B Cap Season Phases

The system tracks 5 phases of the annual H-1B cap cycle:

| Phase | Months | What Happens |
|-------|--------|-------------|
| Pre-registration | Oct - Feb | Employers prepare petitions for upcoming fiscal year |
| Registration open | Mar | USCIS electronic registration window |
| Lottery results | Apr | Selections announced |
| Filing window | May - Jun | Selected petitions filed with USCIS |
| Post-cap | Jul - Sep | Focus on cap-exempt employers or next cycle |

## Related Commands

| Command | Description |
|---------|-------------|
| `/career-ops visa-status` | OPT timeline dashboard |
| `/career-ops oferta` | Full evaluation with visa analysis in Block G |
| `/career-ops scan` | Portal scan with sponsorship indicators |
| `/career-ops batch` | Batch evaluation with OPT-aware warnings |
20 changes: 20 additions & 0 deletions modes/oferta.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,26 @@ Analyze the job posting for signals that indicate whether this is a real, active

**Context Notes:** Any caveats (niche role, government job, evergreen position, etc.) that explain potentially concerning signals.

### OPT Timeline Status (solo si config/visa.yml tiene seccion opt:)

Si `config/visa.yml` tiene seccion `opt:` configurada, ejecutar:
`echo '{"jdText":"<full JD text>"}' | node opt-timeline.mjs --json`
Comment on lines +137 to +138
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, check the actual content of modes/oferta.md around lines 137-153
head -153 modes/oferta.md | tail -17 | cat -n

Repository: santifer/career-ops

Length of output: 1465


🏁 Script executed:

# Check if opt-timeline.mjs exists and examine its input handling
find . -name "opt-timeline.mjs" -type f

Repository: santifer/career-ops

Length of output: 81


🏁 Script executed:

# Look for the complete context in modes/oferta.md
sed -n '130,155p' modes/oferta.md | cat -n

Repository: santifer/career-ops

Length of output: 1860


🏁 Script executed:

# Now examine opt-timeline.mjs implementation
cat opt-timeline.mjs | head -100

Repository: santifer/career-ops

Length of output: 3627


🏁 Script executed:

# Check the input handling and JSON parsing in opt-timeline.mjs
cat opt-timeline.mjs | tail -150

Repository: santifer/career-ops

Length of output: 5490


🏁 Script executed:

# Get the full size of opt-timeline.mjs first
wc -l opt-timeline.mjs

Repository: santifer/career-ops

Length of output: 83


🏁 Script executed:

# Look for the runJsonMode function that handles --json stdin
sed -n '300,450p' opt-timeline.mjs | cat -n

Repository: santifer/career-ops

Length of output: 5488


🏁 Script executed:

# Search for the stdin handling logic
rg -A 20 "runJsonMode" opt-timeline.mjs | head -60

Repository: santifer/career-ops

Length of output: 790


🏁 Script executed:

# Get the complete runJsonMode function to see JSON output construction
sed -n '750,871p' opt-timeline.mjs | cat -n

Repository: santifer/career-ops

Length of output: 5217


🏁 Script executed:

# Search for where the JSON output is constructed
rg -B 5 -A 15 "JSON.stringify.*optStatus" opt-timeline.mjs

Repository: santifer/career-ops

Length of output: 45


🏁 Script executed:

# Search more broadly for JSON output in runJsonMode
sed -n '760,820p' opt-timeline.mjs | cat -n

Repository: santifer/career-ops

Length of output: 2750


🏁 Script executed:

# Find the complete runJsonMode function
rg -B 2 -A 40 "async function runJsonMode" opt-timeline.mjs

Repository: santifer/career-ops

Length of output: 1369


🏁 Script executed:

# Get the rest of runJsonMode to see if tthEstimate is included and how
sed -n '742,800p' opt-timeline.mjs | cat -n

Repository: santifer/career-ops

Length of output: 3174


🏁 Script executed:

# Get the complete rest of runJsonMode function
sed -n '702,760p' opt-timeline.mjs | cat -n

Repository: santifer/career-ops

Length of output: 2937


🏁 Script executed:

# Search for where tthEstimate is computed in runJsonMode
rg -A 50 "async function runJsonMode" opt-timeline.mjs | grep -A 30 "tthEstimate"

Repository: santifer/career-ops

Length of output: 162


🏁 Script executed:

# Get the full context around tthEstimate assignment in runJsonMode
rg -B 10 "result.tthEstimate" opt-timeline.mjs | head -50

Repository: santifer/career-ops

Length of output: 545


🏁 Script executed:

# Verify the exact instruction text in modes/oferta.md to understand if this is a template or literal instruction
sed -n '6,11p' modes/oferta.md | cat -n

Repository: santifer/career-ops

Length of output: 357


🏁 Script executed:

# Check if there's any surrounding context that explains how to use this command
sed -n '1,20p' modes/oferta.md | cat -n

Repository: santifer/career-ops

Length of output: 873


🏁 Script executed:

# Test what happens if JD text contains quotes or special characters - simulate the brittle echo approach
cat > /tmp/test_json.sh << 'EOF'
#!/bin/bash
# Test 1: JD with single quotes
JD_WITH_QUOTES='JD says: "We need a developer"'
echo "Test 1 - With quotes in JD:"
echo "{\"jdText\":\"$JD_WITH_QUOTES\"}" 2>&1 | head -5

# Test 2: JD with newlines
JD_WITH_NEWLINE="Line 1
Line 2"
echo -e "\nTest 2 - With newlines in JD:"
echo "{\"jdText\":\"$JD_WITH_NEWLINE\"}" 2>&1 | head -5

# Test 3: Proper JSON construction with jq
echo -e "\nTest 3 - Proper jq approach:"
echo "$JD_WITH_QUOTES" | jq -Rs '{"jdText": .}' 2>&1 | head -5
EOF
bash /tmp/test_json.sh

Repository: santifer/career-ops

Length of output: 275


🏁 Script executed:

# Verify tthEstimate field is truly optional by checking the actual rendering template usage
sed -n '8,20p' modes/oferta.md | cat -n

Repository: santifer/career-ops

Length of output: 503


🏁 Script executed:

# Get the exact lines 137-145 as shown in the review snippet
sed -n '137,145p' modes/oferta.md | cat -n

Repository: santifer/career-ops

Length of output: 749


Do not inline raw JD text into this shell command — use proper JSON encoding instead.

Line 138's echo '{"jdText":"<full JD text>"}' will fail on job descriptions with quotes or newlines. Real postings will break the JSON structure. Use jq or a JSON encoder: jq -Rs '{"jdText": .}' instead, or switch to reading JD from a file.

Additionally, the banner template on line 142 unconditionally references {tthEstimate.type}, {tthEstimate.minDays}, and {tthEstimate.maxDays}, but tthEstimate is optional in the JSON output (only populated if jdText is provided). Add a conditional check before rendering this line, or ensure jdText is always passed when OPT status is computed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modes/oferta.md` around lines 137 - 138, The shell example using echo with
inline '{"jdText":"<full JD text>"}' is fragile for quotes/newlines; change the
docs to recommend proper JSON encoding (e.g., using jq -Rs '{"jdText": .}' or
reading JD from a file) and update the example in opt-timeline.mjs usage to show
that approach instead of raw echo; additionally, guard rendering of the banner
line that references tthEstimate.type, tthEstimate.minDays, and
tthEstimate.maxDays by checking that tthEstimate (or jdText) exists before
interpolating those fields, or require jdText be provided when computing OPT so
the template never dereferences an undefined tthEstimate.


Con el resultado JSON, mostrar este banner ANTES de la tabla de sponsorship:

> **OPT STATUS:** {remainingDays} days remaining (expires {endDate})
> Unemployment: {unemployment.used}/{unemployment.limit} days used ({unemployment.remaining} remaining)
> **Cap Season:** {capSeason.phase}. {capSeason.advice}
> **Time-to-Hire:** {tthEstimate.type} company, est. {tthEstimate.minDays}-{tthEstimate.maxDays} days. Your OPT window: {remainingDays} days. {tthEstimate.warning || "Within range."}
Comment on lines +140 to +145
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 | 🟠 Major

Guard the Time-to-Hire row behind tthEstimate.

The opt-timeline.mjs contract marks tthEstimate as optional, so this banner will emit undefined fields whenever the estimator does not return that object. Render that line only when tthEstimate is present, or provide a fallback without it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modes/oferta.md` around lines 140 - 145, The Time-to-Hire line in the banner
uses the optional tthEstimate and can emit undefined fields; update the banner
rendering so the entire "Time-to-Hire" row is only emitted when tthEstimate is
present (or provide a clear fallback), e.g., check tthEstimate before
interpolating tthEstimate.type/minDays/maxDays/warning and otherwise omit or
render a fallback message; reference the tthEstimate contract from
opt-timeline.mjs and the banner template where the OPT STATUS block is
assembled.


**Warning escalation (per D-06):**
- Si unemployment.severity == 'urgent' (<=14 days): Prefijo `URGENT` en rojo, lenguaje fuerte: "CRITICAL: Only {remaining} unemployment days left. Immediate employment required."
- Si unemployment.severity == 'warning' (<=30 days): Prefijo `WARNING`, lenguaje firme: "WARNING: {remaining} unemployment days remaining. Accelerate job search."
- Si unemployment.severity == 'info' (<=60 days): Nota informativa: "Note: {remaining} unemployment days remaining. Monitor closely."
- Similar escalation for optStatus.remainingDays approaching 0.

Si `opt:` no esta configurada en visa.yml, omitir esta subseccion silenciosamente.

### Edge case handling:
- **Government/academic postings:** Longer timelines are standard. Adjust thresholds (60-90 days is normal).
- **Evergreen/continuous hire postings:** If the JD explicitly says "ongoing" or "rolling," note it as context -- this is not a ghost job, it is a pipeline role.
Expand Down
69 changes: 29 additions & 40 deletions modes/scan.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,9 @@ Leer `portals.yml` que contiene:

**Cada empresa DEBE tener `careers_url` en portals.yml.** Si no la tiene, buscarla una vez, guardarla, y usar en futuros scans.

### Nivel 2 — ATS APIs / Feeds (COMPLEMENTARIO)

Para empresas con API pública o feed estructurado, usar la respuesta JSON/XML como complemento rápido de Nivel 1. Es más rápido que Playwright y reduce errores de scraping visual.

**Soporte actual (variables entre `{}`):**
- **Greenhouse**: `https://boards-api.greenhouse.io/v1/boards/{company}/jobs`
- **Ashby**: `https://jobs.ashbyhq.com/api/non-user-graphql?op=ApiJobBoardWithTeams`
- **BambooHR**: lista `https://{company}.bamboohr.com/careers/list`; detalle de una oferta `https://{company}.bamboohr.com/careers/{id}/detail`
- **Lever**: `https://api.lever.co/v0/postings/{company}?mode=json`
- **Teamtailor**: `https://{company}.teamtailor.com/jobs.rss`
- **Workday**: `https://{company}.{shard}.myworkdayjobs.com/wday/cxs/{company}/{site}/jobs`

**Convención de parsing por provider:**
- `greenhouse`: `jobs[]` → `title`, `absolute_url`
- `ashby`: GraphQL `ApiJobBoardWithTeams` con `organizationHostedJobsPageName={company}` → `jobBoard.jobPostings[]` (`title`, `id`; construir URL pública si no viene en payload)
- `bamboohr`: lista `result[]` → `jobOpeningName`, `id`; construir URL de detalle `https://{company}.bamboohr.com/careers/{id}/detail`; para leer el JD completo, hacer GET del detalle y usar `result.jobOpening` (`jobOpeningName`, `description`, `datePosted`, `minimumExperience`, `compensation`, `jobOpeningShareUrl`)
- `lever`: array raíz `[]` → `text`, `hostedUrl` (fallback: `applyUrl`)
- `teamtailor`: RSS items → `title`, `link`
- `workday`: `jobPostings[]`/`jobPostings` (según tenant) → `title`, `externalPath` o URL construida desde el host
### Nivel 2 — Greenhouse API (COMPLEMENTARIO)

Para empresas con Greenhouse, la API JSON (`boards-api.greenhouse.io/v1/boards/{slug}/jobs`) devuelve datos estructurados limpios. Usar como complemento rápido de Nivel 1 — es más rápido que Playwright pero solo funciona con Greenhouse.

### Nivel 3 — WebSearch queries (DESCUBRIMIENTO AMPLIO)

Expand Down Expand Up @@ -80,18 +64,11 @@ Los niveles son aditivos — se ejecutan todos, los resultados se mezclan y dedu
f. Acumular en lista de candidatos
g. Si `careers_url` falla (404, redirect), intentar `scan_query` como fallback y anotar para actualizar la URL

5. **Nivel 2 — ATS APIs / feeds** (paralelo):
5. **Nivel 2 — Greenhouse APIs** (paralelo):
Para cada empresa en `tracked_companies` con `api:` definida y `enabled: true`:
a. WebFetch de la URL de API/feed
b. Si `api_provider` está definido, usar su parser; si no está definido, inferir por dominio (`boards-api.greenhouse.io`, `jobs.ashbyhq.com`, `api.lever.co`, `*.bamboohr.com`, `*.teamtailor.com`, `*.myworkdayjobs.com`)
c. Para **Ashby**, enviar POST con:
- `operationName: ApiJobBoardWithTeams`
- `variables.organizationHostedJobsPageName: {company}`
- query GraphQL de `jobBoardWithTeams` + `jobPostings { id title locationName employmentType compensationTierSummary }`
d. Para **BambooHR**, la lista solo trae metadatos básicos. Para cada item relevante, leer `id`, hacer GET a `https://{company}.bamboohr.com/careers/{id}/detail`, y extraer el JD completo desde `result.jobOpening`. Usar `jobOpeningShareUrl` como URL pública si viene; si no, usar la URL de detalle.
e. Para **Workday**, enviar POST JSON con al menos `{"appliedFacets":{},"limit":20,"offset":0,"searchText":""}` y paginar por `offset` hasta agotar resultados
f. Para cada job extraer y normalizar: `{title, url, company}`
g. Acumular en lista de candidatos (dedup con Nivel 1)
a. WebFetch de la URL de API → JSON con lista de jobs
b. Para cada job extraer: `{title, url, company}`
c. Acumular en lista de candidatos (dedup con Nivel 1)

6. **Nivel 3 — WebSearch queries** (paralelo si posible):
Para cada query en `search_queries` con `enabled: true`:
Expand Down Expand Up @@ -167,6 +144,28 @@ https://... 2026-02-10 Ashby — AI PM SA AI OldCo skipped_dup
https://... 2026-02-10 WebSearch — AI PM PM AI ClosedCo skipped_expired
```

## OPT Timeline Notice (si config/visa.yml tiene seccion opt:)

Al inicio del scan output, si `config/visa.yml` tiene seccion `opt:` configurada, ejecutar `node opt-timeline.mjs` (sin args, human-readable) y mostrar un one-line summary:

> OPT: {remainingDays}d remaining | Unemployment: {used}/{limit}d | Cap: {phase}

Si unemployment.severity o OPT expiration severity es 'warning' o 'urgent', highlight the line con prefijo `WARNING:` o `URGENT:`.
Comment on lines +149 to +153
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 | 🟠 Major

Use --json here, or display the dashboard verbatim.

node opt-timeline.mjs returns formatted text, but this section needs structured fields like {remainingDays}, {used}, {limit}, {phase}, and severity. That one-line summary cannot be generated reliably from the human-readable output, so scan should either switch to --json or print the dashboard as-is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modes/scan.md` around lines 149 - 153, When printing the OPT summary at scan
start, call "node opt-timeline.mjs --json" and parse its structured output to
extract remainingDays, used, limit, phase and the severity fields
(unemployment.severity and OPT expiration severity) and then render the one-line
summary "OPT: {remainingDays}d remaining | Unemployment: {used}/{limit}d | Cap:
{phase}" with a "WARNING:" or "URGENT:" prefix when either severity is
warning/urgent; if the tool does not support --json or parsing fails, fall back
to printing the opt-timeline.mjs dashboard verbatim instead of attempting to
generate the one-line summary.


Si `opt:` no esta configurada, omitir silenciosamente.

## Sponsorship Indicator (solo si config/visa.yml existe)

Si visa features estan activas (config/visa.yml existe), agregar columna de sponsorship al output de scan:
- Para cada oferta encontrada, si el titulo o snippet del job posting contiene keywords de sponsorship (from `config/sponsorship-keywords.yml`):
- Positive keywords found (e.g., "visa sponsorship", "will sponsor"): mostrar `[SPONSOR]`
- Negative keywords found (e.g., "will not sponsor", "security clearance required"): mostrar `[NO-SPNS]`
- No keywords found: mostrar `[?]`
- Esta columna es SOLO indicativa -- NO filtrar ofertas. Todas se muestran al usuario.
- El usuario decide cuales evaluar.

Nota: el scan solo tiene acceso al titulo y snippet, no al JD completo. La deteccion completa de sponsorship ocurre durante la evaluacion (oferta/batch mode) cuando se obtiene el JD completo.

## Resumen de salida

```
Expand All @@ -193,18 +192,8 @@ Cada empresa en `tracked_companies` debe tener `careers_url` — la URL directa
- **Ashby:** `https://jobs.ashbyhq.com/{slug}`
- **Greenhouse:** `https://job-boards.greenhouse.io/{slug}` o `https://job-boards.eu.greenhouse.io/{slug}`
- **Lever:** `https://jobs.lever.co/{slug}`
- **BambooHR:** lista `https://{company}.bamboohr.com/careers/list`; detalle `https://{company}.bamboohr.com/careers/{id}/detail`
- **Teamtailor:** `https://{company}.teamtailor.com/jobs`
- **Workday:** `https://{company}.{shard}.myworkdayjobs.com/{site}`
- **Custom:** La URL propia de la empresa (ej: `https://openai.com/careers`)

**Patrones de API/feed por plataforma:**
- **Ashby API:** `https://jobs.ashbyhq.com/api/non-user-graphql?op=ApiJobBoardWithTeams`
- **BambooHR API:** lista `https://{company}.bamboohr.com/careers/list`; detalle `https://{company}.bamboohr.com/careers/{id}/detail` (`result.jobOpening`)
- **Lever API:** `https://api.lever.co/v0/postings/{company}?mode=json`
- **Teamtailor RSS:** `https://{company}.teamtailor.com/jobs.rss`
- **Workday API:** `https://{company}.{shard}.myworkdayjobs.com/wday/cxs/{company}/{site}/jobs`

**Si `careers_url` no existe** para una empresa:
1. Intentar el patrón de su plataforma conocida
2. Si falla, hacer un WebSearch rápido: `"{company}" careers jobs`
Expand Down
50 changes: 50 additions & 0 deletions modes/visa-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# visa-status -- OPT Timeline Dashboard

Quick-glance view of your F-1 OPT status, unemployment day counter, H-1B cap season phase, and upcoming deadlines.

## Prerequisites

- `config/visa.yml` must exist with `opt:` section configured
- If missing, tell user: "OPT tracking not configured. Copy config/visa.example.yml to config/visa.yml and fill in your OPT details."

## Steps

1. Run `node opt-timeline.mjs` (no args -- human-readable dashboard mode)
2. Display the output directly to the user
3. If any warnings are present (unemployment severity or OPT expiration approaching), highlight them prominently

## Output Format

The script produces a formatted dashboard. Display it as-is. Example:

```
=== F-1 OPT Status ===

Type: STEM OPT
Start Date: 2025-06-01
Expiration: 2028-06-01
Remaining: 792 days

--- Unemployment Counter ---
Used: 45 / 150 days
Remaining: 105 days
Status: OK

--- H-1B Cap Season ---
Current Phase: Filing window (Apr - Jun)
Advice: Selected petitions being filed. If not selected, focus on cap-exempt employers.
Lottery Status: pending

--- Next Key Deadline ---
Oct 1, 2026: H-1B employment start date for FY2027 selectees
```
Comment on lines +18 to +40
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

Keep this example synchronized with the real dashboard output.

The current sample shows a different header/section layout than formatDashboard() and includes a “Next Key Deadline” block that the formatter does not print. That conflicts with the instruction to “display it as-is.” While updating it, also add a fence language to clear the MD040 lint warning.

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 20-20: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modes/visa-status.md` around lines 18 - 40, Update the example block in
modes/visa-status.md so it exactly matches the output produced by
formatDashboard(): remove the “Next Key Deadline” section, adjust the header and
section ordering/labels to mirror formatDashboard() (e.g., ensure the top
header, "Unemployment Counter" and "H-1B Cap Season" blocks match), and add a
fenced code block language (e.g., ```text) to silence MD040; reference
formatDashboard() when confirming exact field names and layout.


## Warnings

If unemployment remaining <= 60 days or OPT remaining is within warning thresholds, the script output includes warning banners. Make sure these are visible and not buried.

Comment on lines +42 to +45
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 | 🟠 Major

Do not promise OPT-expiration warning banners unless the script emits them.

The provided formatDashboard() implementation only adds severity banners for unemployment days and prints EXPIRED for elapsed OPT; it does not emit warning/urgent labels for a soon-expiring OPT window. This mode will otherwise wait for warnings that never appear.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modes/visa-status.md` around lines 42 - 45, The README promises
OPT-expiration warning banners but formatDashboard() currently only handles
unemployment severity banners and prints "EXPIRED" for elapsed OPT; update
formatDashboard() to compute optRemaining (or the existing OPT remaining
variable) and apply the same threshold logic used for unemploymentRemaining to
emit warning/urgent severity banners (e.g., by calling the same
printSeverityBanner/addSeverityBanner helper used for unemployment) when
optRemaining falls within the warning thresholds, or alternatively remove the
promise of OPT warning banners from the mode text if you prefer not to implement
them.

## Tips

After displaying status, suggest:
- "Update your unemployment days: edit `opt.unemployment_days_used` in config/visa.yml"
- "Run `/career-ops oferta` to evaluate a job with OPT-aware timing analysis"
Loading
Loading