ingest: add --reingest, the delete-then-fresh-import escape hatch - #139
Conversation
--force runs a reconcile, which deliberately keeps a source's existing treatment and only re-verifies its facts - so a source first ingested under an old rulebook or a weak model is never re-thought, and "all ok, nothing new" is the correct reconcile verdict even when a fresh read would organize it completely differently (e.g. into a Registry page). `citadel ingest --reingest <paths>` re-imports the named tracked sources from scratch: a kind="delete" cleanup session strips each source's previous facts (its manifest key is dropped), then the same run - the deletion group always runs first - ingests it as a brand-new source under the current model, rules, and wiki state. A failed cleanup refuses that source's fresh session (nothing is written on top of the old facts), tracked repos take the first-time repo brief over a full digest (safe because the cleanup already removed the pages a first-time brief would duplicate), the transcript/pdftext caches are kept (same bytes, about to be re-read), and the flag requires explicit paths like --force; --force/--retry refuse to combine with it. tasks/delete.md now briefs the still-on-disk cleanup case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPu8qnxePMUS1whYwen11L
There was a problem hiding this comment.
Pull request overview
This PR introduces a new citadel ingest --reingest <paths> escape hatch that re-imports already-tracked sources from scratch by first running a kind="delete" cleanup (to strip prior facts and drop the manifest key) and then running a fresh first-time ingest in the same run.
Changes:
- Add
reingestplumbing to the ingest engine: plan cleanup jobs ahead of pending sessions, enforce “cleanup must succeed before fresh ingest,” and recordreingest_cleanedin the run report. - Expose
--reingestin the CLI with explicit-paths guarding and mutual exclusions with--force/--retry. - Update progress reporting, rules brief (
tasks/delete.md), changelog/docs, and add targeted tests pinning the semantics.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
citadel/ingest.py |
Implements reingest planning (cleanup + fresh import), refusal logic, cache-prune exceptions, and reporting/logging updates. |
citadel/cli.py |
Adds --reingest flag and CLI-level validation/refusal rules; threads the flag into ingest.ingest(...). |
citadel/progress.py |
Extends start/progress totals to account for extra reingest cleanup jobs. |
citadel/rules/tasks/delete.md |
Updates the delete task brief to cover the “still on disk during reingest cleanup” case. |
tests/test_reingest.py |
New tests specifying and validating reingest semantics (order, refusal on cleanup failure, repo handling, cache behavior). |
tests/test_cli.py |
Adds CLI plumbing tests for --reingest, including refusal without paths and mutual exclusions. |
tests/test_ingest_progress.py |
Updates expected progress start payload to include reingest. |
tests/test_ingest_parallel.py |
Updates a local ingest stub signature to accept the new reingest kwarg. |
CLAUDE.md |
Documents --reingest behavior and constraints. |
.github/copilot-instructions.md |
Regenerated documentation reflecting the --reingest addition. |
CHANGELOG.md |
Adds an Unreleased entry documenting --reingest. |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPu8qnxePMUS1whYwen11L
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (3)
citadel/ingest.py:1197
- Same as the file path: when a reingest cleanup fails for a repo, the refusal error can overwrite the failures catalog entry and hide the original cleanup failure detail. Carrying the prior failure detail forward makes the persisted failure record more diagnosable.
if rjob.key in reingest_keys and rjob.key in manifest_dict:
raise RuntimeError(
"the delete cleanup for this reingest failed, so its previous facts are "
"still in the wiki; fix that failure and re-run `citadel ingest --reingest`"
)
citadel/ingest.py:1102
- If a reingest cleanup fails, the subsequent fresh-session refusal raises a new error that overwrites the failures catalog entry, losing the original cleanup failure detail. Including the prior failure detail in the refusal message keeps
citadel statusactionable even after the run output has scrolled away.
This issue also appears on line 1193 of the same file.
if rel_key in reingest_keys and rel_key in manifest_dict:
raise RuntimeError(
"the delete cleanup for this reingest failed, so its previous facts are "
"still in the wiki; fix that failure and re-run `citadel ingest --reingest`"
)
citadel/ingest.py:719
- The public API currently allows
ingest.ingest(paths, force=True, reingest=True)even though the CLI (and the docstring) treat these flags as mutually exclusive intents. This can lead to ambiguous programmatic usage and makes the API contract diverge from the CLI contract.
if reingest and not paths:
# Same guard, sharper teeth: a reingest costs a cleanup session PLUS a full ingest
# session per source, so a whole-corpus reingest by accident is twice as expensive.
raise ValueError(
"--reingest requires explicit paths (each source runs a delete cleanup session plus "
The reconcile brief drives both `ingest --force` and every `citadel refresh` session, and it only asked the agent to verify/update the facts the wiki already carries - so a re-read after a model or rules upgrade correctly answered "all ok, nothing new" even when the first pass had missed facts, or the wiki had since grown pages this source should link to. Now the brief says a re-read is more than verification: mine the unchanged source for facts the first ingest missed, cross-link against today's wiki, and strip claims the source never supported. The existing-genre-treatment rule stays - structural re-thinks remain --reingest's job. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPu8qnxePMUS1whYwen11L
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (1)
citadel/ingest.py:127
- The
reingest_cleanedfield comment says previous facts were stripped “by a cleanup session”, but--reingestalso marks uncited sources as cleaned without running any delete session (the key is just forgotten). This comment is misleading about the behavior and invariants.
# `--reingest` only: tracked sources whose previous facts were STRIPPED by a cleanup session
# (manifest key dropped) ahead of the fresh plain-ingest session the same run then ran for
# them — the deliberate re-think of an already-ingested source. The fresh session's own
# outcome lands in `processed`/`errors` like any other source's.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPu8qnxePMUS1whYwen11L
verify-corpus leuchtfeuer — PASS (full wave protocol + live probes of this PR's features)Mode A, scratch sandbox, Waves — session kinds exactly as scripted,
Hard gates: D1 delete propagation clean, C1/C2 planted counterfactuals cited with honest Probes of this PR's changes, on the live sandbox wiki:
Misses (ledgered, neither caused by this diff): VCB-006 — T4's intra-wave decommission supersession (30 Sep → 31 Jul 2026) recorded on the portal page but the programme page still words the old date as the live plan; a cross-page supersession gap in plain wave-3 ingest sessions, routed to Generated by Claude Code |
What
--forceruns a reconcile, and the reconcile brief deliberately keeps a source's existing treatment (genre, page structure) — it only re-verifies/updates facts. So a source first ingested under an old rulebook or a weak model is never re-thought: "all ok, nothing new" is the correct reconcile verdict even when a fresh read would organize it completely differently (e.g. into aRegistrypage that didn't exist when it was first ingested). The only way to get a true fresh import was the manual move-the-file-out / full ingest / move-it-back / ingest dance.citadel ingest --reingest <paths>does that in one run: for each named tracked source, akind="delete"cleanup session strips its previous facts from the wiki (manifest key dropped), then — deletions always run first — the same run ingests it as a brand-new source under the current model, rules, and wiki state (fresh cross-links included).Semantics, pinned tests-first in
tests/test_reingest.py:kind="ingest"— never a reconcile; the manifest gets a fresh stamprepobrief over a full digest (safe only because the cleanup removed the pages a first-time brief would duplicate)--force(exit 2 without);--force/--retryrefuse to combine with ittasks/delete.mdnow briefs the still-on-disk cleanup case, and the progress reporter counts the extra cleanup jobs in the overall totalTesting
uv run pytest -q— 1313 passed, 1 skippeduv run ruff check ./ruff format --check .— cleanCITADEL_WORKSPACE=corpora/beverages uv run python -m citadel lint— OKingest.py+rules/; results will be posted on this PR (with the backlog-ledger entry committed) when the waves finish🤖 Generated with Claude Code
https://claude.ai/code/session_01SPu8qnxePMUS1whYwen11L
Generated by Claude Code