db-update

leopu00/job-hunter-team · 51★

Total 116Week 59Day 26
Scanned — cleanMIT

Update existing records in the JHT DB (positions / applications). Use it to promote positions to checked/excluded, write Critic score/verdict, mark applications as sent, update salary, last-checked, etc. Always after a `db-query` that confirms the current record state.

Install
npx skillforge add leopu00/job-hunter-team/agents/_skills/db-update

Full details

SKILL.md

---
name: db-update
description: Update existing records in the JHT DB (positions / applications). Use it to promote positions to checked/excluded, write Critic score/verdict, mark applications as sent, update salary, last-checked, etc. Always after a `db-query` that confirms the current record state.
allowed-tools: Bash(python3 *)
---

# db-update — record updates on the JHT DB

Wrapper at `/app/shared/skills/db_update.py`. Updates specific fields on existing records. **Does not create** records — for that, see `db-insert`.

## General pattern

```bash
python3 /app/shared/skills/db_update.py <table> <id> --<field> <value> [--<field> <value>...]
```

Tables: `position`, `application`.

## Positions

```bash
# Promote to checked / excluded (Analyst's job)
python3 /app/shared/skills/db_update.py position 42 --status checked
python3 /app/shared/skills/db_update.py position 42 --status excluded

# last-checked marker (link confirmed alive — also used as anti-collision claim)
python3 /app/shared/skills/db_update.py position 42 --last-checked now

# Liveness: --is-open / --last-open-check also advance last_checked on
# their own, so a rechecked position leaves the care queue (which gates on
# the most recent of the two dates). Pass --last-checked only to override it.
python3 /app/shared/skills/db_update.py position 42 --is-open false --last-open-check now

# Salary as declared in the JD
python3 /app/shared/skills/db_update.py position 42 --salary-declared-min 40000 --salary-declared-max 55000

# Estimated salary (glassdoor / levels.fyi / analyst's estimate)
python3 /app/shared/skills/db_update.py position 42 --salary-estimated-min 35000 --salary-estimated-max 50000 --salary-estimated-source glassdoor

# Role family (categoria semantica).
python3 /app/shared/skills/db_update.py position 42 --role-family "Technical Writing"

# Location strutturata (Analyst). Pieno esempio per "Dublin, Ireland" hybrid:
python3 /app/shared/skills/db_update.py position 42 \
  --loc-city "Dublin" --loc-region "Leinster" \
  --loc-country "Ireland" --loc-country-code "IE" \
  --loc-continent "Europe" \
  --work-mode "hybrid" \
  --work-country "Ireland" --work-country-code "IE" \
  --is-multi-location false

# Esempi casi speciali:
# A) "Europe Remote" → country=NULL, continent=EU, work_country dall'HQ azienda
python3 /app/shared/skills/db_update.py position 42 \
  --loc-continent "Europe" --work-mode "remote" \
  --work-country "United States" --work-country-code "US" \
  --location-notes "Remote within EU, US-based company"

# B) "Italy" + full_remote
python3 /app/shared/skills/db_update.py position 42 \
  --loc-country "Italy" --loc-country-code "IT" --loc-continent "Europe" \
  --work-mode "remote" --work-country "Italy" --work-country-code "IT"

# E) Multi-location stesso paese ("Barcelona / Malaga")
python3 /app/shared/skills/db_update.py position 42 \
  --loc-country "Spain" --loc-country-code "ES" --loc-continent "Europe" \
  --work-mode "hybrid" --work-country "Spain" --work-country-code "ES" \
  --is-multi-location true --location-notes "Barcelona or Málaga (candidato sceglie)"

# Per "ripulire" un campo (set NULL) passa stringa vuota:
python3 /app/shared/skills/db_update.py position 42 --loc-city ""
```

## Applications

```bash
# Critic verdict (per-round: NEEDS_WORK / PASS / REJECT) + score 0-10 + notes
python3 /app/shared/skills/db_update.py application 42 --critic-verdict NEEDS_WORK --critic-score 5.0 --critic-notes "needs more detail on project X"

# CV/cover letter committed (Writer marks as written)
python3 /app/shared/skills/db_update.py application 42 --written-at now

# Promote to ready after Critic PASS — Writer only, in application-flow Step 7
python3 /app/shared/skills/db_update.py application 42 --status ready

# User confirmed the application was sent
python3 /app/shared/skills/db_update.py application 42 --applied-at "2026-02-28" --applied-via linkedin
python3 /app/shared/skills/db_update.py application 42 --applied true

# Response received (`interview` / `rejected` / `ghosted`)
python3 /app/shared/skills/db_update.py application 42 --response "rejected" --response-at now
```

### Position state transitions are auto-logged (bug #14)

Every call to `db_update.py position <id> --status <s>` that actually
changes `positions.status` inserts a row in `position_state_transitions`
with `from_state`, `to_state`, `ts`, `by_agent` (from `JHT_AGENT_NAME`),
and the `--notes` you passed (if any). Same goes for the initial
`db_insert.py position` (logged as `NULL → 'new'`).

You don't have to do anything — the wrapper handles it. Don't bypass it
with raw SQL: a `python3 -c "import sqlite3; UPDATE positions SET
status=..."` workaround skips the transition log and makes throughput /
funnel charts undercount.

### Maintenance checks leave a history (`--action` / `--outcome`)

`last_checked` and `last_open_check` hold only the **latest** date: every pass
overwrites the previous one, so nothing records how many times a position was
looked at, or how often a check failed to conclude anything.

Pass `--action` on maintenance work and the check is kept in
`maintenance_events`, one row per changed field — or one row anyway when
nothing changed, because knowing a position was looked at is half the point.

```sh
db_update.py position 412 --last-checked now \
  --action liveness_check --outcome confirmed_open --is-open true \
  --evidence-url "<url>" --evidence-code 200
```

`--action`: `liveness_check` · `geocode` · `logo_fetch` · `website_fetch` ·
`jd_refresh` · `exclude` · `rescore`

`--outcome`: `confirmed_open` · `confirmed_closed` · **`inconclusive`** ·
`updated` · `unchanged` · `unreachable` · `skipped` · `failed`. Omit it and it
is derived from whether anything changed.

`--evidence-url` / `--evidence-code` / `--evidence-hash` are optional context:
a 403 that keeps recurring tells you about an authwall, not about a dead
posting.

> ⛔ **An unresolved check may not close a position.** With `--outcome`
> `inconclusive`, `unreachable`, `skipped` or `failed`, writing
> `--is-open false` or `--status excluded|expired` is refused with exit 1.
> Not knowing is not the same as knowing it expired, and a position binned on
> a doubt is an opportunity lost in silence — leave it alive, the check stays
> in the history and it will be retried. Everything else still goes through:
> notes, coordinates, summaries. Reopening is never blocked.

Read it back with `db_query.py check-history <id>`, which also reports the
streak of consecutive unresolved checks — that is a **source** problem to
report, not a position to discard.

### Single-writer gate on `applications.status='ready'` (bug #21)

`applications.status='ready'` is **set exclusively by the Scrittore** in
`application-flow` Step 7, **only after** Critic PASS on the 3rd round.
This is the gate that makes the CV visible on the user's `/ready`
dashboard. Other agents:

- **Critic**: writes `critic_verdict` + `critic_score` only. Never `status`.
- **Capitano**: never writes `applications.status`. May read it.
- **Mentor / Assistente**: read-only on `applications`.

Without this gate, the Capitano can report "12 ready" verbally while the
DB still shows 0 — exactly the divergence that bug #21 fixed.

## Safety rules

1. **Read first.** Run `db-query position <id>` (or `application`) to see the current state before writing. Blind overwrites produce inconsistent records.
2. **Status flow is forward-only.** Legitimate transitions: `new → checked → scored → writing → ready → applied → response`. `excluded` is reachable from any step but no step ever moves backward. Don't reverse.
3. **`now` timestamp.** The wrapper converts the literal string `now` into the current timestamp. Don't pass `$(date)` — parsing is handled Python-side.
4. **Exclusion tags in `--notes`.** When marking a position `excluded`, prefix the notes with one of the canonical tags: `[LINK_MORTO]` · `[SCAM]` · `[GEO]` · `[LINGUA]` · `[SENIORITY]` · `[STACK]`. Same taxonomy used by the Analyst (see `agents/analista/analista.md` REGOLA-06).

## Don't use it for

- Reads: use **`db-query`**
- Creating records: use **`db-insert`** (only the Scout INSERTs positions)
- Schema changes: never run raw `sqlite3` against the tables — it bypasses foreign keys and Next.js's WAL journaling