Skip to content

reconcile prune: dry-run under-reports — apply also archives unclaimed log sections by age #54

Description

@ProtocolWarden

Summary

cl reconcile prune --apply archives log sections that never appear in the plan the dry-run prints. The extra sections are chosen purely by age, are claimed by no worksheet item, and are cleared by no gate. On a public repo they are moved into the private-manifest archive, so the working record of in-flight work leaves the repo without the operator seeing it in the preview.

Found while reconciling Custodian (ProtocolWarden/Custodian#71), which only avoided it by passing --recent 20 by hand.

The divergence

Planning and applying use different rules.

Planbuild_plan (reconcile/prune.py:114) only ever records claimed sections:

for item in owned_done:                       # is_done, owned by this repo
    for idx, sec in enumerate(sections):
        if sec.heading and _section_matches_item(sec, kws):
            plan.moves.append(PlannedMove("log.md", sec.heading, item.id))

Previewformat_plan (prune.py:372) renders plan.moves and nothing else.

Apply_retain_recent_log (prune.py:179, called at prune.py:250) archives a second, larger set:

if sec.heading in claimed_headings:
    archived.append(sec); continue
unclaimed_seen += 1
if unclaimed_seen <= recent_n:
    kept.append(sec)
else:
    archived.append(sec)        # ← never in plan.moves, never printed

So --dry-run shows N moves and --apply archives N plus every unclaimed section beyond recent_n (DEFAULT_RECENT_N = 10, prune.py:36). Nothing reports the difference, before or after.

Why the DOC GAP gate doesn't cover this

The gate only inspects done items — check.py:103:

if item.is_done and not item.is_cross_repo(ws.repo):
    if not item.doc: result.doc_gaps.append(...)

An item still partial raises no doc gap, so chk.green stays true and the prune proceeds. But owned_done also excludes it, so its log sections are never claimed — which drops them into exactly the age-swept bucket above.

The net effect is that incomplete, undocumented work is the most likely thing to be archived by age, because it is precisely the work no gate has cleared. That inverts the intent: the gate exists to hold unreleased work back.

To be precise about the mechanism — this is not "recency outranks the DOC GAP gate", as Custodian#71's commit message put it. The gate never applies to partial items at all. The problem is that age-based archival operates on sections no gate has ever evaluated.

Impact seen in Custodian

  • Default --recent 10 would have archived the June work the operator had deliberately left partial (pattern-collision masking, orphan-scaffold cleanup, the --only gate). Avoided only by knowing to pass --recent 20.
  • Custodian enforces RC1, a 400-line budget on .console/log.md, so it prunes often. Frequent prunes plus a default of 10 erode the log faster than either mechanism intends.
  • Destination is the private manifest, so content moves from a public repo to a private one. Recoverable, but not where a contributor would look.

Suggested directions

Not prescriptive — the right call depends on what recent_n is meant to be:

  1. Make the plan total. Compute the age-swept set in build_plan, put it in plan.moves (or a sibling field) so format_plan prints it. This alone fixes the surprise; the policy can stay as-is.
  2. Separate the two operations. Reconciliation (archive completed, documented work) and log trimming (drop old entries) have different safety properties. A --trim-unclaimed/--no-trim-unclaimed split would let a repo reconcile without trimming.
  3. Don't sweep unclaimed sections at all, or only with an explicit flag. recent_n then means "keep at least N" rather than "archive past N".
  4. At minimum, log a warning naming each age-swept section, so it appears in CI output.

Not this issue

The absolute-path pointer bug in _pointer_section is already being fixed — there is an in-progress claude/portable-archive-pointer worktree with portable_archive_ref, a private_root parameter, and an _ABSOLUTE_REF_RE that heals pointers written by older runs. Flagging it here only so nobody opens a duplicate.

Context

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions