# Personal data (user fills these)
cv.md
article-digest.md
local/
# Data-root marker (DATA_CONTRACT.md "Custom Data Directory"). It holds a path
# on this machine only, so committing it would point every other clone at a
# directory that does not exist there.
/.career-ops-data
# Everything under data/ is generated or personal at runtime — only .gitkeep
# placeholders are tracked (verify with `git ls-files data/`). A blanket rule
# means a new generated file (e.g. a future log/cache addition) is covered
# for every user automatically, with no per-filename .gitignore update needed.
# Match the root entry itself as well as its contents: the user data root may
# be a symlink to storage outside the checkout, which child globs cannot reach.
/data
!/data/
data/*
!data/.gitkeep
!data/offers/
!data/parser-output/
# Received offers/contracts, promise notes, prep reports (PII — never commit)
data/offers/*   
!data/offers/.gitkeep
data/parser-output/**
!data/parser-output/**/
!data/parser-output/.gitkeep
!data/parser-output/**/.gitkeep
reports/*.md
# Single-level glob above misses subdirectories (e.g. reports/superseded/).
# Kept alongside it rather than replacing it, so update-system.mjs does not
# re-append the original pattern as "missing".
reports/**/*.md
!reports/.gitkeep
/output
!/output/
output/*
!output/.gitkeep
# batch/ holds generated worker output. Ignore the directory and allowlist the
# tracked scaffolding, so a filename a future generator picks is covered by
# default. AGENTS.md already states the intent this way: "Batch in batch/
# (gitignored except scripts and prompt)". Enumerating the outputs instead only
# covers the names that existed when the list was written, which left
# batch/cv-*.json and batch/cover-*.json stageable, and those carry the
# candidate's CV content.
batch/*
!batch/README.md
!batch/aggregate-tokens.mjs
!batch/batch-prompt.md
!batch/batch-runner.sh
!batch/company-scope.mjs
!batch/jd-rescue.mjs
# No trailing slash, so git descends into the two keeper directories. A slashed
# pattern matches only a real directory and would miss a symlink.
!batch/logs
!batch/tracker-additions
# tracker-additions never had a blanket child rule, only the **/*.tsv one below,
# so a non-.tsv file dropped straight into it was stageable. The .gitkeep
# negations further down still win over this on an already-updated install, not
# only in a fresh clone: reconcileGitignore() re-emits a negation the install
# already has after the patterns it appends, which is what #4142 added.
batch/tracker-additions/*
# The enumerated rules below are now redundant in a fresh clone, and are kept
# rather than deleted. The reconciler only ever appends, so deleting them here
# would not remove them from an existing install; it would only drop them from
# fresh clones, where batch/logs/* is still what re-ignores that directory's
# contents once !batch/logs has let git descend into it.
batch/logs/*
!batch/logs/.gitkeep
batch/batch-state.tsv
batch/batch-state-recovery.d/
batch/batch-input.tsv
batch/tracker-additions/**/*.tsv
!batch/tracker-additions/.gitkeep
# Worker scratch: prefetched JD text plus whatever compile cache the worker CLI
# drops here. Fetched postings are user data — never stage them.
batch/tmp/
/jds
!/jds/
jds/*
!jds/.gitkeep

# Personal documents — never commit scans or copies of these (PII).
# Catches common identity/credential filenames so they can't be staged by accident.
*passport*
*diploma*
*简历*
*学位*
*成绩单*

# Interview prep (accumulated STAR+R stories — user data)
interview-prep/*
!/interview-prep/
!interview-prep/.gitkeep
# Session transcripts hold real interviewer names/companies — ignore content,
# ship only the system-owned scaffold (README + .gitkeep). #1242
!interview-prep/sessions/
interview-prep/sessions/*
!interview-prep/sessions/.gitkeep
!interview-prep/sessions/README.md

# Writing samples (user's personal files — never committed)
writing-samples/*
!writing-samples/README.md

# User config and customization (never auto-updated)
config/profile.yml
config/cv-facts.json
config/benchmarks.yml
# Fork-local path declarations — this checkout's own files, read by
# update-system.mjs. Gitignored on purpose: it describes THIS clone, not the
# project, and must survive the sync that overwrites update-system.mjs (#2421).
config/local-paths.txt
portals.yml
modes/_profile.md
modes/_custom.md
modes/_brief.md
voice-dna.md
.update-dismissed
.update-lock
.manifesto-noted

# Plugin layer — opt-in toggles + user-installed plugins + integrity lock (never auto-updated)
config/plugins.yml
plugins.local/
plugins.lock

# Secrets (never commit — use .env.example as template)
.env
# Universal safety net — protects ALL checkouts/branches, incl. untracked subprojects
# (e.g. web/ before it graduates to main). Never commit local env files or TS build caches.
**/.env.local
**/.env*.local
**/*.tsbuildinfo

# Generated
.resolved-prompt-*
node_modules
bun.lock
# Working copy the test suite makes under the repo root (test-all.mjs
# `mkdtempSync(join(ROOT, '.tmp-script-test-'))`). A clean run deletes it; a
# run interrupted mid-way leaves ~650MB of duplicated repo behind, which a
# reflexive `git add .` would then stage as ~1000 new files.
.tmp-script-test-*/

# Report-number reservation sentinels (ephemeral, created by reserve-report-num.mjs)
reports/*-RESERVED.md

# Backup files. Two shapes exist: `{file}.bak` (normalize-statuses.mjs,
# dedup-tracker.mjs, tracker.mjs) and `{file}.bak-{ISO timestamp}`
# (web/src/lib/core/safe-write.ts). A plain `*.bak` glob misses the timestamped
# form, which for cv.md or portals.yml leaves a file holding the same PII as the
# original sitting untracked in the repo root. Trailing `*` covers both.
*.bak*

# Atomic-write temp files, the sibling of the backups above and from the same
# two writers: `.{name}.{pid}.{ts}.{uuid}.tmp` (tracker-utils.mjs
# writeFileAtomic, web/src/lib/core/safe-write.ts atomicWrite). They exist only
# between the write and the rename — but a rename that throws (Windows
# destination-handle contention, ENOSPC, a read-only mount) leaves one behind
# holding a byte-for-byte copy of cv.md / profile.yml / portals.yml, under a
# name the `*.bak*` rule does not match. Same PII, same reflexive `git add .`.
*.tmp*

# The tracker and its follow-ups file in the LEGACY ROOT LAYOUT. resolveTrackerPath()
# documents its own fallback chain — CAREER_OPS_TRACKER > <root>/data/applications.md
# > <root>/applications.md — and that third entry is a supported install shape, not a
# mistake. data/ is covered by the blanket rule at the top of this file; the root
# spelling was not covered by anything.
#
# The content is the user's whole job search: company, role, score, status, notes.
# cv.md, portals.yml, article-digest.md and voice-dna.md are all ignored at the root
# already, and #3469 ignores the derived .db index on the argument that it "holds the
# same PII as the tracker" — while the tracker itself sat beside it, unignored.
#
# Unanchored, like the .db rule below and for the same reason: getCareerOpsRoot()
# resolves a relative CAREER_OPS_ROOT (or .career-ops-data marker) against the
# codebase directory, so a data root configured as `career-data` puts these inside
# the checkout, where an anchored rule would not reach them. The upgrade fixtures
# that share these names stay tracked via the `!test-fixtures/**` rule further down,
# which is a later pattern and therefore wins.
applications.md
follow-ups.md

# The tracker's derived SQLite index (tracker.mjs, #918). It sits beside the
# tracker markdown, so on the standard layout it lands in data/ and is already
# covered by the rules above — but resolveTrackerPath() falls back to
# `<root>/applications.md` when data/applications.md is absent, and the index
# follows it there. That is the legacy install layout, and it is also every
# fresh clone: `node test-all.mjs` leaves a 36KB applications.db in the repo
# root, which `git add .` then stages.
#
# Its content is the whole tracker — company, role, score, status, notes — so it
# carries the same PII as the file it indexes, under a name none of the rules
# above match. SQLite's -wal/-shm sidecars travel with it.
#
# Deliberately not root-anchored. The index follows the tracker markdown, and
# getCareerOpsRoot() resolves a relative CAREER_OPS_ROOT (or .career-ops-data
# marker) against the codebase directory — so a data root configured as
# `career-data` puts both inside the checkout, in neither of the two places an
# anchored rule would cover. Nothing tracked in this repo ends in .db, and an
# ignore rule cannot untrack a file that is already tracked, so the cost of the
# wider glob is a `git add -f` on a .db fixture nobody has added yet; the cost
# of the narrower one is this leak, for every non-default data root.
*.db
*.db-wal
*.db-shm

# OpenRouter runner — generated data (personal, never commit)
data/model-blacklist.json

# OS
.DS_Store
*.mov
*.mp4

# IDE-specific local config
.idea/
.vscode/

# CLI-specific local config
.claude/settings.local.json
.claude/memory/
.opencode/settings.local.json
.opencode/memory/
# OpenCode project config (often holds personal MCP/model/per-machine prefs).
opencode.json
# Claude Code project-level MCP server config — commonly ends up holding
# personal server credentials (e.g. a plugin's API token) once a user adds
# an MCP server, same risk class as .env.
.mcp.json
# Hermes Agent personal project rules. docs/HERMES.md offers it as a way to
# supply rules that override AGENTS.md without sharing them with the repository.
.hermes.md
career-dashboard
career-dashboard.exe
package-lock.json
!web/package-lock.json

# Playwright MCP server run artifacts (ephemeral, machine-local)
.playwright-mcp/
test-results/
playwright-report/

# Planning (local scratchpad)
plan/

# Web — runtime cache/history the dashboard writes locally; nothing here is tracked.
.career-ops-web/

# Upgrade-test fixtures are fictional system-layer data, never user data
# (see DATA_CONTRACT.md). Re-include them past the unanchored user-layer
# patterns above (cv.md, portals.yml).
!test-fixtures/**

# Profile intake sources (master CV / LinkedIn export / diplomas / references
# — user PII, never commit). Ship only the system-owned scaffold. #1723
/documents
!/documents/
documents/*
!documents/.gitkeep
!documents/README.md
# Intake fingerprints — the recorded source filenames may themselves carry
# PII (e.g. cv/jane-doe-master-cv.pdf). #1723
data/intake-state.json
data/agent-inbox.md

# Older installs keep unanchored scaffold negations because reconcileGitignore()
# only appends rules. Re-ignore matching user-layer folders under data/ after
# those legacy patterns so personal files stay out of git status and commits.
data/jds/
data/output/
data/documents/
data/interview-prep/

# Repo-local LaTeX engine placed by install-latex-engine.mjs (.part is its staging copy)
/tectonic
/tectonic.exe
/tectonic.part
/tectonic.part.exe

# Web UI local runtime and environment
.career-ops.env
**/.career-ops.env
.next/
web/.next/
web/out/
