| name | scan |
| description | Run the Job Scanner end-to-end pipeline -- discover jobs from all enabled portals, extract descriptions, evaluate against CV+preferences, write a pass/fail Markdown report. All stages are DB-driven via hooks in .claude/settings.json. Run manually via /scan. |
| disable-model-invocation | true |
| allowed-tools | ["Agent","Read"] |
| shell | bash |
META rule: Non-fixing orchestrator (locked)
You are a narrator, not a debugger. You explain what happened; you do not fix what happened.
PERMITTED:
- Use
Read on any file in this project to investigate a failure mode and reason about probable cause.
- Narrate degraded states (missing hook_reports, zero-count stages, unreadable artifacts) using the locked line templates defined below.
- Emit the locked OBSERVED/LIKELY/CHECK explanation block on any
SCAN: halt (...) path.
- Echo agent summary lines and hook_report JSON content verbatim into each stage's narration block.
FORBIDDEN:
- Do NOT use
Edit, Write, or NotebookEdit on any file. You have no write permission in this skill.
- Do NOT run any
Bash command. Your allowed-tools are Agent and Read only.
- Do NOT re-dispatch an agent that has already returned. One dispatch per stage per run.
- Do NOT modify, infer writes to, or reason about mutating
config/*, scripts/*, .claude/*, or data/jobs.db.json. These are the user's surfaces, not yours.
- Do NOT attempt to recover from a failed stage by retrying, substituting defaults, or routing to another agent. Halt and explain.
If you feel the urge to "fix" something — that is exactly the signal to HALT and EXPLAIN instead. The user fixes; /scan narrates.
Pre-flight: Config validation
!python scripts/preflight_cleanup.py 2>&1
HALT INSTRUCTION (locked): If the output above contains the substring PREFLIGHT FAILURE: STOP immediately. Do not attempt any more actions and return control to the user:
SCAN: halt (Data Pipeline Preparation: Found error(s) from preflight_cleanup.py -- delete all_tmp files or attempt to re-run /scan)
Where error(s) is the exact substring from the output
if the output shows success line PREFLIGHT: Pipeline Ready for Scanning. proceed to the next validation step and emit:
SCAN: PREFLIGHT -> checks passed
!python scripts/validate_config.py 2>&1
HALT INSTRUCTION (locked): If the output above contains the substring Found N error(s): (for any positive integer N), OR contains any line starting with two spaces followed by a hyphen ( -, which is the validator's error bullet format), STOP immediately. Do not attempt any more actions and return control to the user:
SCAN: halt (config: <N> error(s) from validate_config.py -- fix config files and re-run /scan)
Then stop. The user fixes config; /scan does not.
where <N> is the integer from Found N error(s):. If you cannot parse N, use the literal string unknown in its place.
If the output shows the success lines (portals.yml: ... OK, filters.yml: ... OK, preferences.yml: ... OK, cv.md: ... OK, ending in All configs valid.), emit:
SCAN: CONFIG -> check passed
Then proceed to Stage 0 below
Stage 0: Pipeline start
Emit exactly this single line to stdout:
SCAN: start (pipeline: discovery -> extraction -> evaluation)
Stage 1: Discovery
Use the Agent tool to dispatch the discovery subagent (subagent_type: "discovery") and wait for it to return. Do NOT batch it with any other tool call.
The SubagentStop hook (post_discovery.py) runs automatically after the discovery agent stops. It validates raw discoveries, deduplicates by (title, company), upserts new jobs to the DB (data/jobs.db.json), and applies keyword filtering to transition new jobs to qualified or disqualified. DO NOT invoke hooks yourself.
After the Agent tool call returns, the discovery agent has emitted a final summary block with two locked lines (DISCOVERY: <summary> and DISCOVERY: artifact <path>) and the post-discovery hook has written data/_tmp/hook_reports/post_discovery.json with flat counts {new, known, qualified, disqualified, validation_failed}.
If the discovery agent failed (the Agent tool returned an error, or the agent stopped without emitting its locked DISCOVERY: <summary> and DISCOVERY: artifact <path> final-block lines):
Emit: SCAN: halt (discovery: agent failed)
Then stop immediately. Do NOT proceed to the hook_report Read or to Stage 2.
If the discovery agent succeed use the Read tool to read data/_tmp/hook_reports/post_discovery.json. Parse it as JSON. Then emit EXACTLY this 4-line block to stdout, in this order:
STAGE 1 DISCOVERY:
DISCOVERY: <total_listings> listings across <M> of <K> portals (<api> via API, <pw> via playwright fallback[, Z portal(s) failed: <names>])
DISCOVERY: artifact data/raw-discoveries.json
POST-DISCOVERY: hook_report data/_tmp/hook_reports/post_discovery.json {new=<N>, known=<K2>, qualified=<Q>, disqualified=<D>, validation_failed=<V>}
Where:
- Line 1: literal header
STAGE 1 DISCOVERY:.
- Line 2: VERBATIM from the agent's
DISCOVERY: <summary> summary line. Do NOT recompute or rephrase.
- Line 3: VERBATIM from the agent's
DISCOVERY: artifact data/raw-discoveries.json summary line. This is a historical record — the file has been consumed and unlinked by post_discovery.py. Do NOT attempt to Read it.
- Line 4: parsed from
data/_tmp/hook_reports/post_discovery.json. Format the five keys (new, known, qualified, disqualified, validation_failed) as =-separated integers inline. Do NOT compute or transform the counts.
If data/_tmp/hook_reports/post_discovery.json does not exist OR cannot be parsed as JSON, replace the {new=..., ...} content of line 4 with one of these locked tags (no halting — the pipeline continues to Stage 2 regardless):
- If the agent summary line WAS present (the agent ran normally) but the hook_report is missing or unreadable:
[not found -- hook may have failed, work file preserved for inspection]
- If the parse succeeded but the file content is malformed (e.g. not a dict, missing keys):
[unreadable -- <short error summary>]
After emitting the 4-line block, proceed to Stage 2. Do NOT halt on a missing or malformed hook_report — the count report is informational only, never control-flow.
Stage 2: Extraction
Use the Agent tool to dispatch the extraction subagent (subagent_type: "extraction") and wait for it to return. Do NOT batch with any other tool call.
The SubagentStart hook (pre_extraction.py) runs automatically BEFORE the extraction agent starts. It queries the DB for jobs with status: qualified|retry and description IS NULL, writes data/_tmp/extraction_work.json, and either:
- Continues (exit 0)
- Blocks (exit 2)
After the Agent tool call returns (or if the agent was blocked):
If the agent was blocked with exit code 2 (you will see a system message containing PRE-EXTRACTION: 0 jobs need extraction):
Emit EXACTLY this 2-line block to stdout, in this order:
STAGE 2 EXTRACTION: skipped (0 jobs need extraction)
POST-EXTRACTION: hook_report data/_tmp/hook_reports/post_extraction.json [not found -- pre-hook blocked, no agent dispatched]
Where:
- Line 1: literal header with fixed reason
STAGE 2 EXTRACTION: skipped (0 jobs need extraction).
- Line 2: locked tag
[not found -- pre-hook blocked, no agent dispatched]. The hook_report is absent by design — do NOT Read data/_tmp/hook_reports/post_extraction.json.
Then proceed to Stage 3. Do NOT halt on a blocked agent — a skip is a valid outcome.
If the agent ran normally:
The extraction agent has emitted a final summary block with two locked lines (EXTRACTION: <summary> and EXTRACTION: artifact <path>) and the post-extraction hook has written data/_tmp/hook_reports/post_extraction.json with flat counts {successful, failed, retried, disqualified}.
Use the Read tool to read data/_tmp/hook_reports/post_extraction.json. Parse it as JSON. Then emit EXACTLY this 4-line block to stdout, in this order:
STAGE 2 EXTRACTION:
EXTRACTION: <total> leads processed (<playwright_extracted> playwright extracted, <uncached_api_null> uncached api null, <failed> failed)
EXTRACTION: artifact data/_tmp/extraction_output.json
POST-EXTRACTION: hook_report data/_tmp/hook_reports/post_extraction.json {successful=<S>, failed=<F>, retried=<R>, disqualified=<D>}
Where:
- Line 1: literal header
STAGE 2 EXTRACTION:.
- Line 2: VERBATIM from the agent's
EXTRACTION: <summary> summary line. Do NOT recompute or rephrase.
- Line 3: VERBATIM from the agent's
EXTRACTION: artifact data/_tmp/extraction_output.json summary line. File has been consumed and unlinked — do NOT Read it.
- Line 4: parsed from
data/_tmp/hook_reports/post_extraction.json. Format the four keys (successful, failed, retried, disqualified) as =-separated integers inline.
Then proceed to Stage 3.
If data/_tmp/hook_reports/post_extraction.json does not exist OR cannot be parsed as JSON, replace the {successful=..., ...} content of line 4 with [not found -- hook may have failed]. Do NOT halt on a missing or malformed hook_report.
Then proceed to Stage 3. Do NOT halt on a blocked agent — a missing hook report is a valid outcome.
If the agent failed (the Agent tool returned an error, or the agent stopped without emitting its locked EXTRACTION: <summary> and EXTRACTION: artifact <path> final-block lines):
Emit:
SCAN: halt (extraction: agent failed)
Stop immediately. Do NOT proceed to Stage 3.
Stage 3: Evaluation
Use the Agent tool to dispatch the evaluation subagent (subagent_type: "evaluation") and wait for it to return. Do NOT batch with any other tool call.
The SubagentStart hook (pre_evaluation.py) runs automatically BEFORE the evaluation agent starts. It queries the DB for jobs with status: qualified and description IS NOT NULL, writes data/_tmp/evaluation_work.json, and either:
- Continues (exit 0)
- Blocks (exit 2)
After the Agent tool call returns (or if the agent was blocked):
If the agent was blocked (you will see a system message containing PRE-EVALUATION: 0 qualified jobs):
Emit EXACTLY this 2-line block to stdout, in this order:
STAGE 3 EVALUATION: skipped (0 qualified jobs to evaluate)
POST-EVALUATION: hook_report data/_tmp/hook_reports/post_evaluation.json [not found -- pre-hook blocked, no agent dispatched]
Then emit the final terminal line:
SCAN: ok (pipeline complete -- no new jobs to evaluate)
This is the final line. Stop.
Where:
- Line 1: literal header
STAGE 3 EVALUATION: skipped (0 qualified jobs to evaluate).
- Line 2: locked tag
[not found -- pre-hook blocked, no agent dispatched]. Hook_report absent by design.
- Line 3: terminal success marker
SCAN: ok (pipeline complete -- no new jobs to evaluate).
If the evaluation agent failed (the Agent tool returned an error, or the agent stopped without emitting its locked Total: <N> jobs evaluated (<P> pass, <F> fail) and Report written: reports/<YYYY-MM-DD>-evaluation.md final-block lines):
Emit:
SCAN: halt (evaluation: agent failed)
Stop immediately.
If the agent ran normally:
The evaluation agent has emitted a final summary block with two locked lines (asymmetric per the agent's existing communication contract — the first line preserves the historical Total: prefix, the second uses Report written:):
Total: <total> jobs evaluated (<passed> pass, <failed> fail)
Report written: reports/<YYYY-MM-DD>-evaluation.md
The post-evaluation hook has written data/_tmp/hook_reports/post_evaluation.json with flat counts {pass, fail, invalid, not_found}.
Use the Read tool to read data/_tmp/hook_reports/post_evaluation.json. Parse it as JSON. Then emit EXACTLY this 4-line block to stdout, in this order:
STAGE 3 EVALUATION:
Total: <total> jobs evaluated (<passed> pass, <failed> fail)
Report written: reports/<YYYY-MM-DD>-evaluation.md
POST-EVALUATION: hook_report data/_tmp/hook_reports/post_evaluation.json {pass=<P>, fail=<F>, invalid=<I>, not_found=<NF>}
Where:
- Line 1: literal header
STAGE 3 EVALUATION:.
- Line 2: VERBATIM from the agent's
Total: <N> jobs evaluated (<P> pass, <F> fail) summary line. No EVALUATION: prefix — asymmetry is intentional. Do NOT add one.
- Line 3: VERBATIM from the agent's
Report written: reports/<YYYY-MM-DD>-evaluation.md summary line. The agent's summary is the sole source of the report path — do NOT derive the date yourself.
- Line 4: parsed from
data/_tmp/hook_reports/post_evaluation.json. Format the four keys (pass, fail, invalid, not_found) as =-separated integers inline.
If data/_tmp/hook_reports/post_evaluation.json does not exist OR cannot be parsed as JSON, replace the {pass=..., ...} content of line 4 with [not found -- hook may have failed, work file preserved for inspection]. Do NOT halt on a missing or malformed hook_report.
After emitting the 4-line block, read reports/<YYYY-MM-DD>-evaluation.md (the path the agent reported, NOT a clock-derived date)
If the report file does not exist
Emit:
SCAN: halt (evaluation: reports/<YYYY-MM-DD>-evaluation.md not found despite agent reporting success)
You can attemp to diagnose with your allowed tools or stop immediately.
If the report file exists (the Read succeeds):
You do NOT need to parse the report file content — you already have the pass/fail counts and the report path from lines 2 and 3 of the 4-line block above. Parse the <P> and <F> integers directly from line 2 (the agent's Total: <N> jobs evaluated (<P> pass, <F> fail) line). Use the report path from line 3 (the agent's Report written: reports/<YYYY-MM-DD>-evaluation.md line) verbatim.
Emit:
SCAN: ok (<P> pass / <F> fail; report at reports/<YYYY-MM-DD>-evaluation.md)
This is the final line. Stop.