Skip to content

fence_twins — AgentTeamsModule

Resolves the unfenced pre-fencing copy ("twin") of a section that a template now fences. A team rendered before the template fenced the section carries it unfenced, and --update --merge adds the fenced block beside it, so the file holds the section twice.

Source: agentteams/fence_twins.py — integrity-pinned, with its corpus agentteams/data/template_lineage.json (generated by scripts/build_template_lineage.py).

fences._merge_fenced_content calls resolve on every merge, not only when a fence is being added. A twin is dropped only on proof: its text, with whitespace collapsed and nothing else normalised, must equal either the fenced copy in the merged file, or a section text that an earlier template emitted unfenced under that heading (from the lineage corpus, resolved with today's placeholder values). The lineage is consulted only when the current template section, resolved the same way, equals the fresh fenced body. If an adapter or enrichment transforms the section, the twin is kept.

Every other twin is kept, with a twin kept (not provably template) notice, and fleet reports it as REVIEW. A twin is also kept, and never dropped, when:

  • its heading leads more than one fence, or appears unfenced more than once;
  • no heading or fence marker follows it;
  • its span holds a managed marker, a user region or a TOML string delimiter;
  • it overlaps another twin;
  • the run has no backup dir.

An operator who keeps a twin on purpose can record it in the team's references/fence-twin-acks.csv (fence_twin_acks). A matching row turns the notice into twin kept (acknowledged), which fleet does not report as REVIEW. The row binds to ack_digest of the file, the heading and the twin's text, so any edit to the twin brings REVIEW back. The row is read only after the drop decision, so it never causes a drop. The kept-twin notice prints the row to record.

A dropped twin's text goes to <backup>/<rel path>.lost.unfenced-<fence id>.md. Goose recipes are resolved inside their dedented instructions: block, then re-indented.

Name Purpose
TwinSource(template, placeholders, rel_path='', acks=()) Template path, placeholder values, output-relative path and acknowledgement rows for one rendered file; built by cli.render_pipeline.twin_sources.
TwinOutcome content, dropped (fence id to text), kept (headings), acknowledged (the kept headings an acknowledgement matched), notices, handled.
KEPT_PREFIX, DROPPED_PREFIX, ACKED_PREFIX Notice prefixes that fleet parses. KEPT_PREFIX is a REVIEW reason, as is a drop that retired a rule; ACKED_PREFIX is not.
CORPUS_PATH, load_corpus(path) The lineage corpus. If it is missing or unreadable, every twin is kept.

resolve(merged, new_regions, source=None, *, allow_drop=False, fresh='', corpus=None, templates_dir=None)

Drops each provable twin and keeps and flags every other one. Returns a TwinOutcome.

ack_digest(rel_path, heading, twin)

Returns the 16-hex digest that an acknowledgement row binds to: the sha256 of the file path, the heading and the twin's whitespace-collapsed text.

unfenced_section(text, heading, *, allow_eof=False)

Returns the (start, end) span of the single unfenced occurrence of heading, up to the next fence marker or heading of the same or higher level. Returns a refusal reason instead when there is no such span. allow_eof is for the corpus builder only.

fence_leading_headings(text)

Maps each heading that leads a fence body to the fence ids it leads.

dedented_view(text)

Returns a Goose recipe's instructions: body at column 0. Any other text is returned unchanged.

persist_dropped(rel_path, dropped, backup_path)

Writes the .lost.unfenced-<id>.md.agentteams-bak sidecars and returns one recovery notice per twin.

norm(text), resolve_placeholders(text, placeholders)

Collapses whitespace, which is the only normalisation applied, and substitutes body placeholders. This module does its own placeholder substitution so that the drop decision depends only on pinned code.