Files
ai-job-search/tools/check_upstream_updates.py
T
LeoWinston-9596andClaude Opus 4.8 41ddc0c73c RFC feat(08): application-form fields as a third /apply artifact (#212)
* feat(08): add application-form fields as a third /apply artifact

/apply produces a CV and a cover letter. Many applications need a third
thing: free-text typed into a portal. Graduate programs, large-employer
ATS systems and startup forms ask for self-introductions, structured
project entries, motivation questions and pitches under a hard character
limit - none of which either document covers, and all of which the
interviewer reads alongside the CV.

Governing rule: a form field selects from what is already true and
arranges it for the question asked. It never introduces a new claim.
All accuracy rules from 03 and 05 apply unchanged.

Covers three field types (self-introduction, structured project entries,
hard character limits), the output format (a plain .txt the candidate
pastes from, with counts stated and internal NOTE TO SELF blocks marked
as not-for-pasting), and a verification checklist.

Two places where form fields are stricter than a CV, because both are
easy to get wrong:

- Project entries carry a name and a role, so they read as ownership of
  the whole project in a way a terse CV bullet does not. Contributory
  work has to be scoped inside the description.
- Project dates are the dates of the project, not of the employment.
  Narrowing them is more accurate where the candidate can say when the
  project started - but never invent a boundary to improve the ratio.

Registers the file in SKILL.md (framework_version 1.0.1 -> 1.1.0) and in
the FRAMEWORK_FILES list in tools/check_upstream_updates.py, so it is
covered by the update check like every other framework file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fixup(08,apply): wire as optional /apply offer; align grounding to three-source union

- apply.md Step 6: offer the third artifact after CV/cover letter are
  produced, mirroring the /outcome house pattern for optional capabilities
  (offer, act only on yes, default output unchanged).
- 08-application-forms.md: ground claims against the framework's
  01-candidate-profile.md + master CV + CLAUDE.md union (per #185)
  instead of only 01, in both the governing rule and the checklist.

Per MadsLorentzen review on PR #212. Rebase onto merged #210 (the
tenure-check reference this file cites) still pending — #210 hasn't
landed on upstream/master yet.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 16:05:42 +02:00

151 lines
5.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Check for framework updates in the upstream repository.
Usage: python tools/check_upstream_updates.py [--remote <remote-name>] [--branch <branch-name>]
This script:
1. Identifies the upstream remote (defaults to 'upstream', falls back to 'origin').
2. Fetches the latest commits from the upstream remote.
3. Compares the 'framework_version' in your local files under
.claude/skills/job-application-assistant/ with those in the upstream remote.
4. Alerts you if a file has been updated upstream with a newer version.
"""
from __future__ import annotations
import argparse
import os
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
FRAMEWORK_FILES = [
".claude/skills/job-application-assistant/01-candidate-profile.md",
".claude/skills/job-application-assistant/02-behavioral-profile.md",
".claude/skills/job-application-assistant/03-writing-style.md",
".claude/skills/job-application-assistant/04-job-evaluation.md",
".claude/skills/job-application-assistant/05-cv-templates.md",
".claude/skills/job-application-assistant/06-cover-letter-templates.md",
".claude/skills/job-application-assistant/07-interview-prep.md",
".claude/skills/job-application-assistant/08-application-forms.md",
".claude/skills/job-application-assistant/SKILL.md",
"AGENTS.md",
]
def run_git(args: list[str]) -> tuple[int, str, str]:
res = subprocess.run(["git"] + args, cwd=str(ROOT), capture_output=True, text=True)
return res.returncode, res.stdout, res.stderr
def get_framework_version_from_text(text: str) -> str | None:
if not text.startswith("---\n"):
return None
end = text.find("\n---", 4)
if end == -1:
return None
for line in text[4:end].splitlines():
if ":" in line:
k, v = line.split(":", 1)
if k.strip() == "framework_version":
return v.strip().strip('"').strip("'")
return None
def parse_semver(version_str: str) -> tuple[int, int, int]:
# Clean version string (e.g. remove 'v' prefix)
match = re.match(r"^v?(\d+)\.(\d+)\.(\d+)", version_str)
if not match:
return (0, 0, 0)
return tuple(int(x) for x in match.groups())
def main() -> int:
parser = argparse.ArgumentParser(description="Check for framework updates upstream.")
parser.add_argument("--remote", default="upstream", help="Name of the git remote for upstream (default: upstream)")
parser.add_argument("--branch", default="master", help="Branch name of the upstream repo (default: master)")
parser.add_argument("--no-fetch", action="store_true", help="Skip fetching from remote")
args = parser.parse_args()
# Verify remote exists
rc, stdout, _ = run_git(["remote"])
remotes = stdout.splitlines()
remote = args.remote
if remote not in remotes:
if "origin" in remotes:
print(f"Warning: Remote '{remote}' not found. Falling back to 'origin'.")
remote = "origin"
else:
print("Error: No git remotes found.")
return 1
if not args.no_fetch:
print(f"Fetching latest from remote '{remote}'...")
rc, _, stderr = run_git(["fetch", remote])
if rc != 0:
print(f"Warning: Failed to fetch from remote '{remote}': {stderr.strip()}")
print("Proceeding with cached remote tracking branches.")
ref = f"{remote}/{args.branch}"
# Verify ref exists
rc, _, _ = run_git(["rev-parse", "--verify", ref])
if rc != 0:
print(f"Error: Ref '{ref}' does not exist. Make sure you fetched and specified the correct branch.")
return 1
print(f"Comparing local files against upstream '{ref}'...\n")
updates_available = []
errors = []
for rel_path in FRAMEWORK_FILES:
local_path = ROOT / rel_path
if not local_path.exists():
print(f"Local file missing: {rel_path}")
continue
# Get local version
local_text = local_path.read_text(encoding="utf-8")
local_ver = get_framework_version_from_text(local_text)
# Get upstream version
rc, upstream_text, _ = run_git(["show", f"{ref}:{rel_path}"])
if rc != 0:
# File might not exist upstream yet
continue
upstream_ver = get_framework_version_from_text(upstream_text)
if not local_ver:
errors.append(f"Local file {rel_path} is missing 'framework_version' in frontmatter.")
continue
if not upstream_ver:
continue
if parse_semver(upstream_ver) > parse_semver(local_ver):
updates_available.append({
"filename": Path(rel_path).name,
"local": local_ver,
"upstream": upstream_ver,
"path": rel_path
})
if errors:
print("Configuration errors:")
for err in errors:
print(f" - {err}")
print()
if updates_available:
print("[UPDATE] Upstream updates available for framework methodology files:")
for up in updates_available:
print(f" - {up['filename']}: local {up['local']} < upstream {up['upstream']}")
print(f" Diff command: git diff {ref} -- {up['path']}")
print()
print("Review these changes to see if they fit your personalized fork!")
return 0
else:
print("[OK] All framework files are up to date with upstream!")
return 0
if __name__ == "__main__":
sys.exit(main())