refactor: shared agent workflows (F01) and one application registry (F02)
Checkpoint for the 2026-09-11 project review remediation. Work by a previous agent that ran out of context; committed unchanged as a rollback point before the remaining findings land on top of it. F01 — the two agent directories had drifted into different processes - Six shared workflows under `resume_builder/reference/workflows/`; both `.agents/skills/` and `.claude/skills/` are now thin entrypoints that route to the same file instead of carrying full copies. - The `.claude` copies were the stale side: fixed bullet line variants and character budgets that the newer process had already dropped. Removing them is the point of the change, not collateral. - Parity/link regression test added; corrected the top-level file map, which pointed at a directory that does not exist. F02 — application status was stored in four places and disagreed - New registry `job_scout/state/applications.json` (26 records) with `application_records.py` (schema, validation, transactional write) and `application_tracker.py` (add/update, no quota). - AGENTS.md and CLAUDE.md no longer keep their own status tables; both point at `current_applications.md`, generated from the registry. - Interview-reached is now independent of outcome, so a withdrawal after an interview can record both facts. The old tables are preserved verbatim in `job_scout/state/history/application_status_before_2026-09-11.json`. Verification: `python -m unittest discover -s tests -v` — 18/18 pass. Spot-checked that the detailed SBB and Kdo Cy notes survived the migration into the registry rather than being flattened. Known unresolved, deliberately not invented (see FIX_PROGRESS.md): Aker BP's application date and its Adjacent-despite-79 classification. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
This commit is contained in:
1 parent
0d999ab21b
commit
afdef7eb94
33 files changed
+1558
-1768
No files matched your search
@@ -0,0 +1,140 @@
|
||||
"""Authoritative application state and generated, read-only projections.
|
||||
|
||||
Run with --check to detect stale views, or --render after editing the registry.
|
||||
Outcome events describe what happened; current outcome never erases an interview.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from contextlib import contextmanager
|
||||
from datetime import date
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
REGISTRY = ROOT / "job_scout/state/applications.json"
|
||||
VIEW = ROOT / "resume_builder/reference/current_applications.md"
|
||||
COHORT = ROOT / "job_scout/state/application_cohort.json"
|
||||
OUTCOMES = {"open", "applied", "paused", "not_proceeding", "role_closed", "closed_no_response", "withdrawn_by_candidate", "rejected_no_interview", "rejected_after_interview", "interviewing", "offer", "accepted"}
|
||||
|
||||
|
||||
def valid_date(value: str) -> str:
|
||||
if not isinstance(value, str) or date.fromisoformat(value).isoformat() != value:
|
||||
raise ValueError("expected ISO date YYYY-MM-DD")
|
||||
return value
|
||||
|
||||
|
||||
def validate(data: dict) -> None:
|
||||
ids = set()
|
||||
for record in data["applications"]:
|
||||
identifier = record.get("id")
|
||||
if not identifier or identifier in ids:
|
||||
raise ValueError("application IDs must be nonempty and unique")
|
||||
ids.add(identifier)
|
||||
if record.get("outcome") not in OUTCOMES:
|
||||
raise ValueError(f"{identifier}: invalid outcome")
|
||||
for key in ("application_date", "rejection_date", "withdrawn"):
|
||||
if record.get(key):
|
||||
valid_date(record[key])
|
||||
for event in record.get("events", []):
|
||||
if event.get("date"):
|
||||
valid_date(event["date"])
|
||||
score = record.get("evidence_fit")
|
||||
if score is not None and (isinstance(score, bool) or not isinstance(score, (float, int)) or not math.isfinite(score) or not 0 <= score <= 100):
|
||||
raise ValueError(f"{identifier}: evidence_fit must be finite and between 0 and 100")
|
||||
if record.get("hard_gate") == "fail" and record.get("fit_class") not in {"stretch", "no-go"}:
|
||||
raise ValueError(f"{identifier}: failed gate requires Stretch or No-go")
|
||||
if score is not None and record.get("hard_gate") == "pass" and record.get("fit_class") in {"core", "adjacent"}:
|
||||
expected = "core" if score >= 75 else "adjacent" if score >= 60 else "stretch"
|
||||
if record["fit_class"] != expected and not record.get("fit_override_reason"):
|
||||
raise ValueError(f"{identifier}: classification differs from score; record fit_override_reason")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def transaction():
|
||||
"""Serialize local writers; fail clearly rather than overwrite another update."""
|
||||
lock = REGISTRY.with_suffix(".lock")
|
||||
try:
|
||||
descriptor = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
||||
except FileExistsError as exc:
|
||||
raise ValueError(f"Application registry is locked: {lock}. Check for an active writer before removing a stale lock.") from exc
|
||||
try:
|
||||
with os.fdopen(descriptor, "w") as stream:
|
||||
stream.write(str(os.getpid()))
|
||||
data = load()
|
||||
yield data
|
||||
validate(data)
|
||||
atomic_write(REGISTRY, json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
||||
render(data)
|
||||
finally:
|
||||
lock.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def atomic_write(path: Path, text: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
descriptor, temporary = tempfile.mkstemp(dir=path.parent, prefix=path.name + ".", suffix=".tmp")
|
||||
try:
|
||||
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as stream:
|
||||
stream.write(text)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary, path)
|
||||
finally:
|
||||
Path(temporary).unlink(missing_ok=True)
|
||||
|
||||
|
||||
def load() -> dict:
|
||||
return json.loads(REGISTRY.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def interview_reached(record: dict) -> bool:
|
||||
return bool(record.get("interview")) or any(
|
||||
event.get("type") in {"interview_invited", "interview_held"}
|
||||
for event in record.get("events", [])
|
||||
)
|
||||
|
||||
|
||||
def projections(data: dict) -> dict[Path, str]:
|
||||
lines = ["# Current applications", "", "Generated from `job_scout/state/applications.json`; do not edit this view.",
|
||||
"Use current outcome and next action below. Imported legacy notes are historical, not current instructions.", "",
|
||||
"| Application | Current outcome | Interview reached | Next action |", "|---|---|---|---|"]
|
||||
for record in data["applications"]:
|
||||
cells = [record["label"], record["outcome"], "yes" if interview_reached(record) else "not recorded", record["next_action"]]
|
||||
lines.append("| " + " | ".join(str(cell).replace("|", " / ").replace("\n", " ") for cell in cells) + " |")
|
||||
lines.extend(["", "## Current targeting notes", "", *["- " + note for note in data.get("targeting_notes", [])], ""])
|
||||
cohort = dict(data["cohort_metadata"])
|
||||
cohort["generated_from"] = "job_scout/state/applications.json"
|
||||
cohort["applications"] = [record for record in data["applications"] if record.get("cohort_id") == cohort["cohort_id"]]
|
||||
return {VIEW: "\n".join(lines), COHORT: json.dumps(cohort, indent=2, ensure_ascii=False) + "\n"}
|
||||
|
||||
|
||||
def render(data: dict, check: bool = False) -> list[str]:
|
||||
validate(data)
|
||||
stale = []
|
||||
for path, expected in projections(data).items():
|
||||
if not path.exists() or path.read_text(encoding="utf-8") != expected:
|
||||
stale.append(str(path.relative_to(ROOT)))
|
||||
if not check:
|
||||
atomic_write(path, expected)
|
||||
return stale
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
mode = parser.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument("--check", action="store_true")
|
||||
mode.add_argument("--render", action="store_true")
|
||||
args = parser.parse_args()
|
||||
stale = render(load(), check=args.check)
|
||||
if args.check and stale:
|
||||
print("STALE: " + ", ".join(stale))
|
||||
return 1
|
||||
print("Application views are current.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Record applications without quotas; preserve outcome events and interview history."""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import hashlib
|
||||
import application_records as records
|
||||
|
||||
FIT_CLASSES = ("core", "adjacent", "stretch", "no-go")
|
||||
CHANNELS = ("strong", "moderate", "weak")
|
||||
|
||||
|
||||
def summary(data: dict, all_applications: bool = False) -> None:
|
||||
cohort_id = data["cohort_metadata"]["cohort_id"]
|
||||
applications = [row for row in data["applications"] if all_applications or row.get("cohort_id") == cohort_id]
|
||||
print(f"Application record: {'all' if all_applications else cohort_id} ({len(applications)} applications; no quota)")
|
||||
for field, values in (("fit_class", FIT_CLASSES), ("channel", CHANNELS)):
|
||||
for value in values:
|
||||
print(f" {field} {value}: {sum(row.get(field) == value for row in applications)}")
|
||||
unknown = sum(row.get(field) is None for row in applications)
|
||||
if unknown:
|
||||
print(f" {field} unrecorded: {unknown}")
|
||||
outcomes = sorted({row["outcome"] for row in applications})
|
||||
print(" outcomes: " + ", ".join(f"{value}={sum(row['outcome'] == value for row in applications)}" for value in outcomes))
|
||||
print(f" interview reached (including later withdrawal/rejection): {sum(records.interview_reached(row) for row in applications)}")
|
||||
|
||||
|
||||
def add_application(data: dict, args: argparse.Namespace) -> None:
|
||||
records.valid_date(args.date)
|
||||
identity = args.id or "app-" + hashlib.sha256(f"{args.company.casefold()}|{args.job_id or args.role.casefold()}|{args.date}".encode()).hexdigest()[:12]
|
||||
candidate = {
|
||||
"id": identity, "company": args.company, "role": args.role,
|
||||
"label": f"{args.company} — {args.role}", "application_date": args.date,
|
||||
"fit_class": args.fit_class, "evidence_fit": args.evidence_fit,
|
||||
"hard_gate": args.hard_gate, "channel": args.channel, "outcome": args.outcome,
|
||||
"cohort_id": data["cohort_metadata"]["cohort_id"], "job_id": args.job_id,
|
||||
"url": args.url, "fit_override_reason": args.fit_override_reason,
|
||||
"rubric_version": args.rubric_version,
|
||||
"next_action": args.next_action, "current_summary": args.outcome,
|
||||
"events": [{"type": args.outcome, "date": args.date}],
|
||||
}
|
||||
for existing in data["applications"]:
|
||||
same_job = args.job_id and existing.get("job_id") == args.job_id and existing["company"].casefold() == args.company.casefold()
|
||||
same_url = args.url and existing.get("url") == args.url
|
||||
same_application = existing["company"].casefold() == args.company.casefold() and existing["role"].casefold() == args.role.casefold() and existing.get("application_date") == args.date
|
||||
if existing["id"] == identity or same_job or same_url or same_application:
|
||||
if all(existing.get(key) == value for key, value in candidate.items() if key not in {"events", "id", "current_summary", "next_action", "outcome"}):
|
||||
return
|
||||
raise ValueError(f"Application already exists ({existing['id']}); use update instead")
|
||||
data["applications"].append(candidate)
|
||||
records.validate(data)
|
||||
|
||||
|
||||
def update_application(data: dict, args: argparse.Namespace) -> None:
|
||||
records.valid_date(args.date)
|
||||
matches = [row for row in data["applications"] if row["id"] == args.id]
|
||||
if not matches:
|
||||
raise ValueError(f"Unknown application ID: {args.id}")
|
||||
row = matches[0]
|
||||
if row.get("application_date") and args.date < row["application_date"]:
|
||||
raise ValueError("event date precedes application date")
|
||||
outcome_dates = [event["date"] for event in row.get("events", []) if event.get("outcome") and event.get("date")]
|
||||
if args.outcome and outcome_dates and args.date < max(outcome_dates):
|
||||
raise ValueError("older outcome cannot replace current state; record it as a historical note")
|
||||
event = {"type": args.event or args.outcome or "note", "date": args.date}
|
||||
if args.note:
|
||||
event["note"] = args.note
|
||||
if args.outcome:
|
||||
event["outcome"] = args.outcome
|
||||
if event not in row.setdefault("events", []):
|
||||
row["events"].append(event)
|
||||
if args.outcome:
|
||||
row["outcome"] = args.outcome
|
||||
row["current_summary"] = args.note or args.outcome
|
||||
row["next_action"] = args.next_action or ("Await response." if args.outcome in {"applied", "interviewing", "open"} else "Review recorded outcome before further action.")
|
||||
elif args.next_action:
|
||||
row["next_action"] = args.next_action
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
commands = parser.add_subparsers(dest="command", required=True)
|
||||
view = commands.add_parser("summary")
|
||||
view.add_argument("--all", action="store_true")
|
||||
add = commands.add_parser("add")
|
||||
for field in ("company", "role", "date"):
|
||||
add.add_argument("--" + field, required=True)
|
||||
add.add_argument("--id")
|
||||
add.add_argument("--job-id")
|
||||
add.add_argument("--url")
|
||||
add.add_argument("--fit-class", choices=FIT_CLASSES, required=True)
|
||||
add.add_argument("--evidence-fit", type=float, required=True)
|
||||
add.add_argument("--channel", choices=CHANNELS, required=True)
|
||||
add.add_argument("--hard-gate", choices=("pass", "fail", "unknown"), required=True)
|
||||
add.add_argument("--outcome", choices=sorted(records.OUTCOMES), default="applied")
|
||||
add.add_argument("--fit-override-reason")
|
||||
add.add_argument("--rubric-version", default="critique-framework-2026-09-11")
|
||||
add.add_argument("--next-action", default="Await response.")
|
||||
update = commands.add_parser("update")
|
||||
update.add_argument("--id", required=True)
|
||||
update.add_argument("--date", required=True, help="Date of this event")
|
||||
update.add_argument("--outcome", choices=sorted(records.OUTCOMES))
|
||||
update.add_argument("--event", choices=("interview_invited", "interview_held", "assessment_passed", "contact_attempted", "contact_conversation", "referral_received", "note"))
|
||||
update.add_argument("--note")
|
||||
update.add_argument("--next-action")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
if args.command == "summary":
|
||||
summary(records.load(), args.all)
|
||||
return
|
||||
with records.transaction() as data:
|
||||
(add_application if args.command == "add" else update_application)(data, args)
|
||||
summary(data)
|
||||
except (ValueError, OSError) as error:
|
||||
parser.exit(2, f"ERROR: {error}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,81 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Record and summarize the controlled ten-application cohort."""
|
||||
"""Compatibility entry point; application_tracker owns the uncapped registry workflow."""
|
||||
from application_tracker import main
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TRACKER = ROOT / "job_scout" / "state" / "application_cohort.json"
|
||||
FIT_CLASSES = ("core", "adjacent", "stretch")
|
||||
CHANNELS = ("strong", "moderate", "weak")
|
||||
|
||||
|
||||
def load() -> dict:
|
||||
return json.loads(TRACKER.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def save(data: dict) -> None:
|
||||
TRACKER.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def summary(data: dict) -> None:
|
||||
applications = data["applications"]
|
||||
# Slot caps were dropped 2026-09-05 (application_strategy.md). This is a record, not a
|
||||
# budget: report counts, never counts-against-a-target, so nothing reads as a remaining slot.
|
||||
print(f"Application record: {data['cohort_id']} ({len(applications)} applications)")
|
||||
for fit_class in FIT_CLASSES:
|
||||
count = sum(item["fit_class"] == fit_class for item in applications)
|
||||
print(f" {fit_class}: {count}")
|
||||
for channel in CHANNELS:
|
||||
count = sum(item["channel"] == channel for item in applications)
|
||||
print(f" channel {channel}: {count}")
|
||||
outcomes: dict[str, int] = {}
|
||||
for item in applications:
|
||||
outcome = item.get("outcome", "open")
|
||||
outcomes[outcome] = outcomes.get(outcome, 0) + 1
|
||||
print(" outcomes: " + ", ".join(f"{key}={value}" for key, value in sorted(outcomes.items())))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
subparsers.add_parser("summary")
|
||||
add = subparsers.add_parser("add")
|
||||
add.add_argument("--company", required=True)
|
||||
add.add_argument("--role", required=True)
|
||||
add.add_argument("--date", required=True)
|
||||
add.add_argument("--fit-class", choices=FIT_CLASSES, required=True)
|
||||
add.add_argument("--evidence-fit", type=float, required=True)
|
||||
add.add_argument("--channel", choices=CHANNELS, required=True)
|
||||
add.add_argument("--hard-gate", choices=("pass", "fail"), required=True)
|
||||
add.add_argument("--outcome", default="open")
|
||||
|
||||
args = parser.parse_args()
|
||||
data = load()
|
||||
if args.command == "add":
|
||||
if len(data["applications"]) >= 10:
|
||||
raise SystemExit("cohort already contains ten applications")
|
||||
if args.hard_gate == "fail" and args.fit_class != "stretch":
|
||||
raise SystemExit("a hard-gate failure must be recorded as stretch")
|
||||
data["applications"].append(
|
||||
{
|
||||
"company": args.company,
|
||||
"role": args.role,
|
||||
"application_date": args.date,
|
||||
"fit_class": args.fit_class,
|
||||
"evidence_fit": args.evidence_fit,
|
||||
"hard_gate": args.hard_gate,
|
||||
"channel": args.channel,
|
||||
"outcome": args.outcome,
|
||||
}
|
||||
)
|
||||
save(data)
|
||||
summary(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -105,7 +105,7 @@ def workflow_checks() -> list[str]:
|
||||
"<= 3 lines white space",
|
||||
"20-21 variable bullets",
|
||||
],
|
||||
ROOT / ".agents" / "skills" / "critique" / "SKILL.md": [
|
||||
ROOT / "resume_builder" / "reference" / "workflows" / "critique.md": [
|
||||
"Publications 10%",
|
||||
"Eight-Dimension Scoring",
|
||||
],
|
||||
|
||||
@@ -45,8 +45,8 @@ literals the session had flagged as risky.
|
||||
|
||||
**What is kept: the recording, not the rationing.** Every application still records fit class, hard
|
||||
gate, channel, date, stage and outcome. That record is what makes patterns visible instead of
|
||||
re-derived, and it produced every useful finding to date. `application_cohort.json` continues as a
|
||||
durable log; `cohort_tracker.py summary` remains a read-only view. There is no slot to consume and no
|
||||
re-derived. `job_scout/state/applications.json` is the durable registry; `application_cohort.json` is
|
||||
a generated historical-cohort projection. `cohort_tracker.py summary` is a read-only view. There is no slot to consume and no
|
||||
application is blocked for lack of one.
|
||||
|
||||
**Fit class is now a preparation signal, not a spend limit.** An Evidence Fit of 64 does not mean
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Current applications
|
||||
|
||||
Generated from `job_scout/state/applications.json`; do not edit this view.
|
||||
Use current outcome and next action below. Imported legacy notes are historical, not current instructions.
|
||||
|
||||
| Application | Current outcome | Interview reached | Next action |
|
||||
|---|---|---|---|
|
||||
| NVIDIA - Senior DevOps Engineer, Zürich (req JR2024256) | applied | not recorded | **Await response. Raise the working model at first contact** - it went in unresolved and a 5-day-onsite seat would be a late no. Note: `claims.json` has no shell-scripting entry, so **Bash is omitted** from Skills although the JD names it and ten years of Debian admin plus Ansible work plainly involve it - same shape as the Ansible/Linux gaps found 2026-08-27. Confirm with the user and record it. Groovy stays out regardless. |
|
||||
| RUAG C5I - AI Engineer C5I, Thun (application ID 18027) | applied | not recorded | Await response. Optional but still worthwhile: call **Marco Heinzen** about technical scope and level, and **Frank Haugwitz** about compensation and PSP/project eligibility. **NEW CONTACT 2026-09-07 — `Lars Kauz`, Head of ICT Infrastructure C5I, +41 79 293 93 82, lars.kauz@ruag.ch**, published on the System Engineer C5I posting. Channel is the rationed variable since the 09-05 cohort change, so a published, reachable name matters — **but he heads ICT Infrastructure, the function whose req was ruled out for VMware/VDI/Windows content, and that is NOT demonstrably the Kubernetes platform group Senior DevOps C5I sits in.** He is a warm entry into C5I, not a verified channel for rank 1; Haugwitz stays the recorded gate. Do not react to a single outcome with positioning changes (`application_strategy.md` §38/§74). |
|
||||
| Schweizer Armee - Kommando Cyber (Kdo Cy), DevOps Engineer III (Data Platform), Zimmerwald (Ref JRQ$540-19848) | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| Citadel Securities — Platform Engineer, Research Platform Engineering, Zürich (also NY/Miami) | applied | not recorded | **Await response** (submitted 2026-08-26). No document work pending. **Two questions travel with this application and must both be asked at first contact, before any interview effort:** (1) **is the application even against the Zürich seat?** — the apply form offered no Zürich, so it may sit against a US req, which is a hard NO on relocation; (2) **hybrid or 5-day onsite?** — unresolved at submission, and 5-day onsite is a likely NO-GO on the Bern constraint. Same shape as the NVIDIA working-model issue: it went out unresolved, so it is now a live screening topic rather than a pre-cleared one. Also unasked: the Zürich compensation figure — the posting's $175–350k is a US/NY pay-transparency disclosure. |
|
||||
| SBB - Data Engineer (m/w/d), Asset Management Infrastrukturanlagen, Bern (Job ID 103755) | withdrawn_by_candidate | yes | Closed. For future SBB roles assess Niveau L/Kader; do not generalize SBB bands to other employers. |
|
||||
| AWS (AWS EMEA SARL, Switzerland Branch) — Senior Forward Deployed Engineer, Zürich (job 10504263) | not_proceeding | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| Google — Forward Deployed Engineer III, Google Cloud GTM (French, German), Zürich (req 78350205438567110) | not_proceeding | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| SDU Center for Energy Informatics, Odense DK — PhD, Theme 2 Predictive Maintenance & Asset Management of Smart Energy Networks (job 4159) | not_proceeding | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| Aker BP ASA — Data Product Architect, AI-ready Data Products (FINN 469067315) | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| Google - Software Engineer III, Business Home, Zurich (req 93922217108087494) | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| Microsoft — Principal Forward Deployed Engineer, SWE (German Speaking), Zürich (req 200043897) | applied | not recorded | Done — await response |
|
||||
| BIS Basel — Senior Data & Analytics Engineer, AI (jr100429, 3-yr term) | applied | not recorded | Prep interview brief when screening lands |
|
||||
| NATO JWC Stavanger — Staff Officer, AI Engineer (2030 Digitalisation, G15, 3-yr PLN) | applied | not recorded | Await screening; no action until contact |
|
||||
| Microsoft — Senior SWE, Industry Solutions Engineering (ISE), Zürich (req 200040836) | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| Kraken (Payward) — SRE, AI Agents (remote, CH-eligible) | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| Google — Senior Data Engineer (Merchant Data Science), Zürich/MV | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| Snowflake — Sr SWE, Enterprise (Observe by Snowflake), Zürich | closed_no_response | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| Isovalent (Cisco) Sr Data Engineer, Observability | role_closed | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| Google Zürich Sr SWE Infrastructure (Data Pipeline) | not_proceeding | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| Kraken AI Infrastructure | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| Infineon Doctoral | withdrawn_by_candidate | yes | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| Infineon AI Engineer | not_proceeding | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| Apple Data Engineer (ISE, Zurich) | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| Google FDE GenAI (Zurich) | paused | not recorded | Likely abandon |
|
||||
| Equinor — Manager / AI Architect, Agentic systems (Stavanger/Oslo/Rotvoll/Sandsli; req JR106747) | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
| QuantCo Cloud Engineer (Europe/Zürich) | rejected_no_interview | not recorded | Closed; no pending application action. Read recorded history before reopening. |
|
||||
|
||||
## Current targeting notes
|
||||
|
||||
- SBB Niveau K/L is SBB-specific. The reported approximate CHF 145k K ceiling is not a salary anchor for other employers.
|
||||
- Assess headroom and total package: working time, holidays, office days, commute and verified benefits. Never decline a Thun-area role on salary band alone.
|
||||
- Childcare-cost coverage at the Swiss Army/Kdo Cy remains unverified; confirm before pricing it.
|
||||
- Norway remains open selectively; the Swiss compensation threshold does not automatically apply there.
|
||||
- An interview does not prove how an ATS or recruiter evaluated the application. No causal inference from a single conversion.
|
||||
@@ -5,7 +5,7 @@
|
||||
Before generation, editing or critique:
|
||||
|
||||
1. Read `resume_builder/canonical/claims.json` completely.
|
||||
2. Read `config.md` and `AGENTS.md` corrections/status.
|
||||
2. Read `config.md`, `AGENTS.md` corrections and `resume_builder/reference/current_applications.md`. Application status is authoritative in `job_scout/state/applications.json`; the Markdown view and old cohort JSON are generated projections. Historical events are not current instructions.
|
||||
3. Run `python resume_builder/helpers/validate_resume_system.py`.
|
||||
4. Treat canonical claims as higher authority than extractions, experience files, bundles, sessions or outputs.
|
||||
5. Never use a file under `output/` as source content for a new application. Consult `resume_builder/canonical/historical_outputs.json`.
|
||||
@@ -88,4 +88,4 @@ After explicit approval:
|
||||
|
||||
## Progress and Recovery
|
||||
|
||||
Give short progress updates at major steps. Before ending or clearing context, update session Status and the Active Sessions pointer. If interrupted, add a Resume Point describing completed work and the exact next action.
|
||||
Give short progress updates at major steps. Before ending or clearing context, update session Status and the application registry through the tracker; regenerate its views. Do not duplicate status in AGENTS or CLAUDE. If interrupted, add a Resume Point describing completed work and the exact next action.
|
||||
@@ -0,0 +1,59 @@
|
||||
<!-- Shared workflow version: 2026-09-11 -->
|
||||
|
||||
# Critique an Application Package
|
||||
|
||||
## Startup
|
||||
|
||||
1. Read `resume_builder/reference/shared_ops.md`.
|
||||
2. Read `resume_builder/canonical/claims.json` completely.
|
||||
3. Read `config.md`, the session file, and the real verbatim JD.
|
||||
4. Stop if the JD is reconstructed, incomplete or inferred.
|
||||
5. Read `resume_builder/reference/critique_framework.md` and `resume_builder/support/ai_fingerprint_rules.md`.
|
||||
6. Read the resume and optional cover letter. Never use either as evidence; verify against canonical claims and experience files.
|
||||
|
||||
## Preflight
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
python resume_builder/helpers/validate_resume_system.py --document <resume.tex>
|
||||
```
|
||||
|
||||
Add `--document <cover_letter.tex>` when a letter exists. Any failure is Tier 1 and prevents approval.
|
||||
|
||||
Compile the documents and inspect the PDFs. Extract resume text with `pdftotext` and confirm that employer, formal title and dates appear in the expected order.
|
||||
|
||||
## Critique
|
||||
|
||||
Follow `critique_framework.md` exactly:
|
||||
|
||||
1. Hard-gate audit.
|
||||
2. Evidence Fit score, independent of writing.
|
||||
3. Document Quality score, independent of role fit.
|
||||
4. Channel Strength rating.
|
||||
5. Competitive and reader-sequence assessment.
|
||||
6. Canonical claim audit.
|
||||
7. Tiered fixes.
|
||||
8. Cover-letter decision/critique.
|
||||
9. Mechanical verification.
|
||||
|
||||
Do not issue a single overall score. Do not label a high-quality document submit-ready when Evidence Fit fails a hard gate.
|
||||
|
||||
## Cover-Letter Handling
|
||||
|
||||
If the session records `Cover Letter Decision: NO`, do not penalize absence. If a letter exists, first assess whether it should exist under `cl_reference.md`.
|
||||
|
||||
## Save and Report
|
||||
|
||||
Save `output/<Folder>/critique_<name>.md`. Update the session with:
|
||||
|
||||
- Evidence Fit score and Core/Adjacent/Stretch classification.
|
||||
- Hard-gate result.
|
||||
- Document Quality score.
|
||||
- Channel Strength.
|
||||
- Tier 1 fixes.
|
||||
- Critique status and date.
|
||||
|
||||
Present the fit verdict first, then the two scores and actionable fixes. Wait for explicit user approval before finalization or editing.
|
||||
|
||||
When approved, run the finalization checks from `shared_ops.md`. A package with a hard-gate NO-GO may be archived, but must not be described as recommended for submission.
|
||||
@@ -0,0 +1,48 @@
|
||||
<!-- Shared workflow version: 2026-09-11 -->
|
||||
|
||||
# Edit a Resume or Cover Letter
|
||||
|
||||
## Startup
|
||||
|
||||
1. Read `shared_ops.md` and `resume_builder/canonical/claims.json` completely.
|
||||
2. Run the system validator.
|
||||
3. Read the session, real JD, target `.tex`, critique/feedback, selected profile rules and relevant experience files.
|
||||
4. Check `historical_outputs.json`. A package marked unsafe must be regenerated from canonical sources rather than lightly polished.
|
||||
5. If the package was already submitted, preserve it as history. Create a clearly named new version only when the user requests a reusable corrected document.
|
||||
|
||||
## Diagnose
|
||||
|
||||
Classify requested changes as:
|
||||
|
||||
- Truth/provenance correction.
|
||||
- Fit/framing correction.
|
||||
- Information hierarchy or readability.
|
||||
- Content selection.
|
||||
- Mechanical/LaTeX.
|
||||
- Blocked because evidence is absent.
|
||||
|
||||
Do not treat whitespace, equal bullet lengths or an exact character target as defects. Do not add filler. A critique cannot authorize an unsupported claim.
|
||||
|
||||
Present a concise edit plan and wait for explicit confirmation unless the user already approved specific edits.
|
||||
|
||||
## Execute
|
||||
|
||||
- Source every work claim from canonical IDs and experience files.
|
||||
- Keep employer/title/date hierarchy conventional.
|
||||
- Preserve natural bullet lengths and verified ownership scope.
|
||||
- Remove unsupported skills rather than replacing them with synonyms.
|
||||
- Update a cover letter only when its session decision is YES or the user explicitly requests it.
|
||||
- If edits reveal a hard fit gate, update the Evidence Fit classification; writing improvements cannot remove the gate.
|
||||
|
||||
## Verify
|
||||
|
||||
Run the validator on every edited document, compile, inspect the PDF, and verify extracted text order. Compare before/after for:
|
||||
|
||||
- validator failures;
|
||||
- page count;
|
||||
- employer/title/date visibility;
|
||||
- bullet count and relevance;
|
||||
- duplicated sections;
|
||||
- readability at normal size.
|
||||
|
||||
Update the session Edit History, Document Quality, status and next action. Mark the critique stale after material edits. Present results and wait for approval.
|
||||
@@ -0,0 +1,42 @@
|
||||
<!-- Shared workflow version: 2026-09-11 -->
|
||||
|
||||
# Decide and Generate a Cover Letter
|
||||
|
||||
## Startup
|
||||
|
||||
1. Read `shared_ops.md`, `resume_builder/canonical/claims.json`, `cl_reference.md`, the session, real JD and finished resume.
|
||||
2. Run the validator on the resume.
|
||||
3. Confirm that the session has a completed resume and a recorded Cover Letter Decision.
|
||||
|
||||
## Decision Gate
|
||||
|
||||
If the session says NO, explain the recorded reason and stop unless the user explicitly asks to override it. If no decision exists, apply `cl_reference.md` before writing and record YES/NO.
|
||||
|
||||
Do not generate a letter merely because an upload field exists or because older workflow expected a three-page package.
|
||||
|
||||
## Plan
|
||||
|
||||
State in one sentence what the letter adds beyond the resume. Select at most two evidence themes. Verify any named company/product/programme hook from a current primary source and save its URL in the session.
|
||||
|
||||
If the only possible content repeats resume bullets or paraphrases company news, change the decision to NO and explain why.
|
||||
|
||||
## Generate
|
||||
|
||||
- Use the selected audience profile and employer-requested language.
|
||||
- International Tech/industry: normally 200--300 words in two or three paragraphs.
|
||||
- Swiss/DACH: one A4 page, appropriate formality.
|
||||
- Employer-specific: follow the requested format or portal limit.
|
||||
- Lead with candidate-role connection.
|
||||
- Add motivation, decision context or a genuine connection.
|
||||
- Use only claims already present in both the canonical registry and resume.
|
||||
- Never introduce a new tool, metric, customer, ownership claim or result.
|
||||
|
||||
## Verify
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
python resume_builder/helpers/validate_resume_system.py --document <resume.tex> --document <cover_letter.tex>
|
||||
```
|
||||
|
||||
Compile and inspect the letter. Confirm it is readable, within the employer's limit, and materially different from the resume. Update the session and present the letter for approval. Point to `/critique` next.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- Shared workflow version: 2026-09-11 -->
|
||||
|
||||
# Generate a Resume
|
||||
|
||||
## Input
|
||||
|
||||
Accept a JD file, pasted JD, or URL. Obtain the real posting text before analysis. If it cannot be retrieved, ask the user to paste it; never reconstruct it.
|
||||
|
||||
## Phase 0 — Canonical and JD Preflight
|
||||
|
||||
1. Read `resume_builder/reference/shared_ops.md`.
|
||||
2. Read `resume_builder/canonical/claims.json` completely.
|
||||
3. Read `config.md`, `AGENTS.md`, `application_strategy.md`, `resume_reference.md`, and `critique_framework.md`.
|
||||
4. Run `python resume_builder/helpers/validate_resume_system.py`.
|
||||
5. Verify and save the verbatim JD; record source/date/status.
|
||||
6. Create the output folder and session from `session_file_template.md`.
|
||||
|
||||
## Phase 1 — Fit Gate Before Writing
|
||||
|
||||
Build a requirement table with Required/Preferred and Direct/Adjacent/Gap/Constraint classifications. Cite canonical claim IDs for every Direct or Adjacent match.
|
||||
|
||||
Compute Evidence Fit and identify hard gates using `critique_framework.md`. Then assign:
|
||||
|
||||
- Core.
|
||||
- Adjacent.
|
||||
- Stretch.
|
||||
- No-go.
|
||||
|
||||
Record Channel Strength and a warm-channel action for Core roles.
|
||||
|
||||
Stop and present the fit decision before resume planning when:
|
||||
|
||||
- a hard gate fails;
|
||||
- the role is Stretch;
|
||||
- a practical constraint is unresolved.
|
||||
|
||||
Proceed on a no-go only after the user explicitly overrides the stated reason. An override does not change the fit classification.
|
||||
|
||||
## Phase 2 — Audience and Content Plan
|
||||
|
||||
Select International Tech by default. Use Swiss/DACH or Employer-specific format only when the employer/context supports it.
|
||||
|
||||
Read the matching role bundle, `skills_taxonomy.md`, `achievement_reframing_guide.md`, and only the experience files needed for selected claims.
|
||||
|
||||
Plan:
|
||||
|
||||
- Optional 2--3 line summary.
|
||||
- Four to six evidence-backed skills lines.
|
||||
- Swisscom 4--5 bullets.
|
||||
- Bosch 3--4 bullets.
|
||||
- Zero or one bullet for each older role.
|
||||
- Normally 11--14 bullets total, with no page-fill quota.
|
||||
|
||||
For each proposed bullet show canonical ID, evidence type, scope, and why it belongs. Identify any impact metric that still needs user confirmation.
|
||||
|
||||
Apply `cl_reference.md` and propose `Cover Letter Decision: YES/NO` with one reason.
|
||||
|
||||
Present the plan and wait for explicit user confirmation before generation.
|
||||
|
||||
## Phase 3 — Generate
|
||||
|
||||
Copy `resume.cls` and `resume_template.tex` into the output folder. For a Swiss/DACH audience, also read `resume_template_dach.tex` as an audience-policy overlay; it is not a standalone document. Write fresh content from canonical claims and experience records; never copy a historical output.
|
||||
|
||||
Rules:
|
||||
|
||||
- Employer, formal/transparent title, dates and location come first.
|
||||
- Do not use tailored themes as job titles.
|
||||
- Mix natural bullet lengths.
|
||||
- Use only verified metrics and outcomes.
|
||||
- Preserve allowed verbs and ownership scope.
|
||||
- Never add a skill to mirror the JD.
|
||||
- Certifications appear once.
|
||||
|
||||
## Phase 4 — Validate and Inspect
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
python resume_builder/helpers/validate_resume_system.py --document <resume.tex>
|
||||
```
|
||||
|
||||
Fix every failure. Compile the LaTeX, inspect both pages, and run `pdftotext` to verify parsing and information order. Do not add filler to reduce whitespace.
|
||||
|
||||
Update the session with the as-built content, validation results, page count, cover-letter decision, and next action.
|
||||
|
||||
Present the finished resume and the original Evidence Fit classification. Wait for approval. If the cover-letter decision is YES, point to `/make-cl`; otherwise point directly to `/critique`.
|
||||
@@ -0,0 +1,79 @@
|
||||
<!-- Shared workflow version: 2026-09-11 -->
|
||||
|
||||
# Build or Update the Knowledge Base
|
||||
|
||||
## Authority Model
|
||||
|
||||
The pipeline is:
|
||||
|
||||
`source documents -> extractions -> canonical claims -> experience records/bundles -> generated applications`
|
||||
|
||||
Generated applications never flow backward into the KB.
|
||||
|
||||
Read `resume_builder/canonical/claims.json`, `config.md`, and all relevant extractions before changing normalized sources. Preserve raw source documents. A verified correction must update canonical claims first and then every dependent normalized file.
|
||||
|
||||
## Canonical Claim Record
|
||||
|
||||
For each employment item or achievement, record:
|
||||
|
||||
- stable ID;
|
||||
- employer, title, dates and location;
|
||||
- factual scope;
|
||||
- individual vs shared ownership;
|
||||
- allowed verbs;
|
||||
- forbidden overstatements;
|
||||
- metric/result and verification status;
|
||||
- evidence source;
|
||||
- last verified date.
|
||||
|
||||
When ownership is ambiguous, use contributing/shared scope and ask the user. Do not default to full ownership.
|
||||
|
||||
## Experience Files
|
||||
|
||||
Experience files explain facts and context. They may contain example phrasings, but those examples cannot override canonical scope and should not be optimized to fixed line counts.
|
||||
|
||||
Every achievement section must include canonical ID, source, ownership scope, status, verified result/metric state, relevant skills and role relevance.
|
||||
|
||||
## Skills Taxonomy
|
||||
|
||||
Use only these evidence levels:
|
||||
|
||||
- Production — current.
|
||||
- Production — historical.
|
||||
- Hands-on — current.
|
||||
- Project / proof of concept.
|
||||
- Certification / coursework.
|
||||
- Unverified / never used.
|
||||
|
||||
Do not assign Expert/Proficient/Familiar labels. Add a skill to canonical claims before it can appear in a generated document.
|
||||
|
||||
## Role Bundles
|
||||
|
||||
Each bundle must contain:
|
||||
|
||||
1. Defensible positioning.
|
||||
2. Good-fit roles and hard-gate cautions.
|
||||
3. Achievement priorities.
|
||||
4. Safe reframing rules.
|
||||
5. Evidence-backed skills guidance.
|
||||
6. Conditional cover-letter guidance.
|
||||
|
||||
Bundles may rank evidence; they may not broaden it. Title-defining gaps must be stated explicitly.
|
||||
|
||||
## Significance Context
|
||||
|
||||
Keep company/industry context separate from candidate impact. Mark external claims for re-verification at application time. Never attach generic scale, economics, customers or market trends to the candidate.
|
||||
|
||||
## Correction Propagation
|
||||
|
||||
After any correction, search canonical claims, config, extractions, experience files, bundles, support files, templates and active drafts. Historical submitted outputs remain unchanged but update `historical_outputs.json` when they become unsafe for reuse.
|
||||
|
||||
## Validation
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
python resume_builder/helpers/validate_resume_system.py
|
||||
```
|
||||
|
||||
Then review the diff for dates, titles, ownership, metrics and skill evidence. Present the changed claim records and affected downstream files to the user for confirmation.
|
||||
@@ -0,0 +1,98 @@
|
||||
<!-- Shared workflow version: 2026-09-11 -->
|
||||
|
||||
# Source Evidence Extraction
|
||||
|
||||
Use this skill when adding new evidence to `knowledge_base/extractions/`. Extraction preserves what a source supports; it does not tailor claims to a vacancy.
|
||||
|
||||
## Required inputs
|
||||
|
||||
1. Read `config.md` and the anti-fabrication rules in `AGENTS.md`.
|
||||
2. Read the complete source material.
|
||||
3. Check `resume_builder/canonical/claims.json` for existing facts and identifiers.
|
||||
4. Inspect related extraction files so new evidence does not create a duplicate or contradiction.
|
||||
|
||||
Generated resumes, cover letters, critiques, and session files under `output/` are never evidence sources.
|
||||
|
||||
## Extraction procedure
|
||||
|
||||
### 1. Identify the source
|
||||
|
||||
Record:
|
||||
|
||||
- source filename and type;
|
||||
- source date, if known;
|
||||
- whether it is primary evidence, user-authored notes, or a secondary description;
|
||||
- the person, employer, project, or publication it concerns.
|
||||
|
||||
### 2. Separate evidence from inference
|
||||
|
||||
For every potentially reusable fact, record:
|
||||
|
||||
- the source-supported statement;
|
||||
- the exact supporting passage or precise location;
|
||||
- any interpretation separately as `Inference`;
|
||||
- confidence: `verified`, `user-confirmed`, `needs verification`, or `do not use`.
|
||||
|
||||
Never silently convert an inference into a verified claim.
|
||||
|
||||
### 3. Capture career facts precisely
|
||||
|
||||
When applicable, extract:
|
||||
|
||||
- exact employer, formal title, location, and start/end dates;
|
||||
- ownership scope and collaborators;
|
||||
- work performed, method, technology, and domain;
|
||||
- outcome and beneficiary;
|
||||
- metrics with units, baseline, period, and attribution;
|
||||
- publication status, authorship position, and venue;
|
||||
- current versus historical skill use;
|
||||
- certification versus practical experience.
|
||||
|
||||
For large-company work, describe only the user's component, domain, pipeline, service, or contribution. Do not infer ownership of organization-wide platforms or transformations.
|
||||
|
||||
### 4. Record claim controls
|
||||
|
||||
For each major claim, include where useful:
|
||||
|
||||
- allowed verbs;
|
||||
- verbs or framings that would overstate ownership;
|
||||
- safe resume wording;
|
||||
- known contradictions requiring resolution;
|
||||
- whether a metric is verified, qualitative only, or still missing.
|
||||
|
||||
### 5. Write the extraction
|
||||
|
||||
Create or update a focused Markdown file in `knowledge_base/extractions/`. Use a clear structure such as:
|
||||
|
||||
```markdown
|
||||
# Subject
|
||||
|
||||
## Source
|
||||
## Verified facts
|
||||
## Ownership and attribution
|
||||
## Outcomes and metrics
|
||||
## Skills and recency
|
||||
## Safe claim seeds
|
||||
## Inferences / open questions
|
||||
## Conflicts with existing KB
|
||||
```
|
||||
|
||||
Claim seeds are factual building blocks, not polished resume bullets. Do not add unsupported impact language for relevance.
|
||||
|
||||
## Publication rules
|
||||
|
||||
- Never infer `published`, `accepted`, or `under review` from a manuscript file.
|
||||
- Preserve the user's author position.
|
||||
- Institutional funding is not a personal award.
|
||||
- Follow `config.md` provenance flags.
|
||||
|
||||
## Completion
|
||||
|
||||
Report:
|
||||
|
||||
- files created or updated;
|
||||
- newly verified facts;
|
||||
- unresolved contradictions or missing metrics;
|
||||
- suggested next command: use `setup-build-kb` to promote reviewed evidence into the canonical registry, experience files, bundles, and support files.
|
||||
|
||||
Do not update canonical claims from an unreviewed extraction.
|
||||
Reference in new issue
Block a user