Skip to content

feat: memory-lint Phase B.5 trial (provenance + contradiction detection + surfacing rule)#124

Merged
fitz123 merged 21 commits into
mainfrom
feat/memory-lint-trial
May 17, 2026
Merged

feat: memory-lint Phase B.5 trial (provenance + contradiction detection + surfacing rule)#124
fitz123 merged 21 commits into
mainfrom
feat/memory-lint-trial

Conversation

@fitz123
Copy link
Copy Markdown
Owner

@fitz123 fitz123 commented May 17, 2026

Summary

30-day trial of automated cross-file contradiction detection in workspace memory, with proactive in-conversation surfacing.

  • Adds Phase B.5 to memory-consolidation skill: cross-file contradiction detection via cheap candidate generation → LLM judgment on candidates only (per codex-recommended algorithm). Auto-resolve hierarchy: evidence > confidence (the "recency" leg was dropped during review — resolved_at is not a valid freshness proxy for the current claim).
  • Adds frontmatter fields to memory files: confidence (0.0–1.0 float, matches existing Phase B scoring), revisit_if (semantic trigger, ADR-style), plus optional resolved_at / resolution_basis / do_not_reopen (per-pair cooldown list, NOT global).
  • Adds anti-loop protection so the same contradiction doesn't trigger nightly.
  • Adds new "Surfacing pending lint items" section to .claude/rules/platform/memory-protocol.md — agent MUST proactively surface pending items in conversation (topic-relevant preferred, aged escalation >14 days, one-per-session max).
  • Feature-flagged via LINT_PHASE_B5_ENABLED — rollback = flip flag to false, no data migration.

Trial mechanism (30 days)

  • Start: 2026-05-17
  • End / evaluation: 2026-06-17
  • Apple Reminder created in workspace operator's setup for the evaluation date
  • Stats accumulated in memory/lint-stats.jsonl (created on first run)
  • Pending unresolved items surfaced via ## Pending Review (Lint findings) section in workspace MEMORY.md

Rollback criteria

If any of the following during trial: flip LINT_PHASE_B5_ENABLED=false and re-evaluate.

  • ≥3 user-flagged false positives
  • Pending Review section grows >10 unresolved items
  • Surfacing >1 per session in majority of sessions (annoying)
  • Phase B.5 runtime >30s per nightly run (perf regression)

Changes

  • .claude/skills/memory-consolidation/SKILL.md (+128 / -10): feature flag, Phase B.5, expanded Phases C and D, frontmatter format, stats file format, parseable diary prefix
  • .claude/rules/platform/memory-protocol.md (+18): new section "Surfacing pending lint items"
  • docs/plans/completed/2026-05-17-memory-lint-trial.md (+157): archived plan file

3 files, +293/-10. Plan archived to completed/.

Test plan

  • grep -q 'LINT_PHASE_B5_ENABLED' .claude/skills/memory-consolidation/SKILL.md passes
  • grep -q '### Phase B.5' .claude/skills/memory-consolidation/SKILL.md passes
  • grep -q 'evidence > confidence' .claude/skills/memory-consolidation/SKILL.md passes
  • grep -q 'resolved_at' .claude/skills/memory-consolidation/SKILL.md passes
  • grep -q '## Surfacing pending lint items' .claude/rules/platform/memory-protocol.md passes
  • First nightly run after merge: lint phase executes, memory/lint-stats.jsonl created, no errors

References

Notes for reviewer

  • The ralphex run was unusually long (2h36m, 10 codex iterations) due to iterative refinement on edge cases — e.g., do_not_reopen evolved from a scalar to a per-pair {partner, before} list during review to prevent unrelated resolutions from biasing future contradictions.
  • One out-of-scope regression caught locally: ralphex reverted the permissions: pull-requests: read block in .github/workflows/pii-scan.yml (added in PR ci: pii-scan needs explicit pull-requests:read for private consumers #123 this morning). Restored in a separate commit on this branch.

🤖 Generated with Claude Code

fitz123 and others added 20 commits May 17, 2026 18:00
Adds cross-file contradiction detection (Phase B.5) to the nightly
memory-consolidation skill, gated by LINT_PHASE_B5_ENABLED for a 30-day
trial. Phase C now persists confidence and revisit_if in frontmatter;
Phase D writes a Pending Review section to MEMORY.md, appends a
structured JSON line to memory/lint-stats.jsonl, and uses a parseable
diary header prefix for recent-activity grepping.

Auto-resolve uses evidence > confidence > recency hierarchy with
anti-loop frontmatter fields (resolved_at / resolution_basis /
do_not_reopen_before) to prevent re-triggering the same contradiction
nightly. Time-scoped evolution is explicitly excluded from contradiction
treatment. Mutation limit shared with Phase C; never silent-delete —
losing claims get a (superseded: ...) annotation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds platform rule requiring the agent to proactively surface
pending Phase B.5 lint contradictions in conversation: topic-related
inline trigger, 14-day aged escalation, one-per-session cap,
no-interrupt-urgency, and after-resolution cleanup that updates
both the affected memory file and the workspace MEMORY.md
"Pending Review" section. References ADR-069.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Substantive corrections to the memory-consolidation SKILL.md spec and the
companion memory-protocol rule, surfaced by the post-implementation review:

- Mutation budget: Phase C re-zeroed mutations_applied, contradicting Phase
  B.5's "shared budget of 5". Initialize the counter in Phase B.5 and have
  Phase C continue it; explicitly skip Phase C if Phase B.5 already spent it.
- Step 4c referenced evidence_date / updated_at — fields the frontmatter
  schema never persists. Replaced with resolved_at (the field that actually
  exists) and acknowledged the rule mostly defers to flag-for-review.
- Step 4b: handle legacy files lacking the confidence field (default 0.7).
- Per-pair anti-loop: do_not_reopen_before now scopes to a specific partner
  via the new do_not_reopen_partner field, rather than excluding the file
  wholesale from all future scans (which would silently hide unrelated
  third-party contradictions).
- Step 5: edits now MUST use safe-edit.sh, same backup/verify/rollback flow
  as Phase C. Previously unprotected.
- Picked one annotation style (trailing parenthetical) instead of offering
  "strikethrough or parenthetical" as alternatives.
- Phase C frontmatter template no longer inlines commented optional fields
  — those got copied verbatim into new files when the LLM transcribed
  the template literally. Moved to prose with explicit "do not include".
- Dropped the unused claim_phrases accumulator from step 1's representation.
- Pending Review bullets now use a strict, machine-parseable format
  (detected_at=YYYY-MM-DD prefix) so pending_total and avg_age_days math
  has a defined parser. Added deduplication and section-removal scoping.
- Sanitization rules added for free-text fields (resolution_basis,
  revisit_if, reason bullets): single-line, ≤200 chars, no leading #,
  escape ".
- Auto-resolve mutation count: 1 per resolved pair (not 2 per file edit).
- LLM judgment errors now treated as `unrelated` and logged.

Plan validation greps still pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Convert do_not_reopen_partner (scalar) to do_not_reopen_partners (list)
  with APPEND-not-overwrite semantics. The scalar form silently broke the
  anti-loop guarantee when a file participated in more than one resolved
  contradiction over time — the later resolution overwrote the earlier
  partner record, allowing the prior pair to re-trigger.
- Replace literal "N" placeholders in lint-stats.jsonl JSON template with
  explicit <integer> / <number> angle-bracket form; add a jq validation
  step before append so malformed lines never corrupt the trial dataset.
- Define avg_age_days formula explicitly (mean(today − detected_at);
  today is the same YYYY-MM-DD used in the same stats line's "date"
  field; round to 1 decimal).
- Document SUSPICIOUS_SHRINK bypass for the Pending Review section
  removal case — without it, the cleanup edit silently rolls back once
  the section has grown to more than ~20% of MEMORY.md, leaving
  resolved bullets in place forever.
- Update memory-protocol.md after-resolution rule to reference the new
  field name and document the append-not-overwrite requirement so human
  resolutions preserve the anti-loop chain.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- LLM judgment now returns {verdict, claim_a, claim_b} so step 5 has an exact-match anchor for the (superseded:) annotation; downgrade to unrelated if anchor doesn't appear in body
- Sanitization rules extended to do_not_reopen_before when it carries a semantic condition (date form remains unquoted YAML)
- Missing do_not_reopen_before with partner listed is now explicitly "exclusion inactive" rather than undefined
- Mutation budget consistent: Phase B.5 counts each file edit (pair=2), matching Phase C's per-file rule; pre-check verifies remaining budget fits a full pair before starting
- SUSPICIOUS_SHRINK bypass clarifies the section size MUST be measured from the backup before applying the edit, not live during the write
- Phase D preamble pins pending_added to the post-dedup value so diary line and JSONL row agree

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Feature flag default flips to enabled during 2026-05-17 → 2026-06-17
  trial window (unset env var was previously treated as skip, leaving
  the trial dead-on-arrival without runtime env wiring).
- Phase B.5 step 1 now extracts `body_predicates` and `negation_markers`
  from file bodies; step 2 candidate signals reference those extracted
  sets instead of demanding body content the rep didn't carry.
- `do_not_reopen_partners` entries normalized to bare basename at read
  time and deduped, closing an anti-loop bypass when prior cycles wrote
  heterogeneous path formats.
- Step 5 paired edits now use explicit two-phase commit semantics
  (backup BOTH, edit BOTH, verify BOTH, then clean BOTH) so a partial
  failure can't leave file A annotated while file B is untouched.
- Step 5 anchor re-verify added: before applying each pair's edit,
  re-confirm `claim_a`/`claim_b` still appears verbatim in the current
  file body — a prior pair in the same run may have already annotated
  the same line.
- Mutation budget accounting timing pinned: increments fire only after
  both files verify and clean successfully; rolled-back pairs do not
  consume budget.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Six surgical spec fixes to memory-consolidation/SKILL.md and
memory-protocol.md based on the 5th review pass:

- title_tokens: explicit fallback when both heading and description absent
- body_predicates: drop multi-token "do not" (undefined under "whole-word
  match"); already covered by negation_markers
- pending_review canonical entry shape unified across steps 4d, 5, 6 so
  Phase D's parser regex matches every deferral uniformly
- pending_added counter: removed in-phase increment, Phase D step 2 is the
  single source via post-dedup recompute
- do_not_reopen_before: take MAX(existing, new) when persisting; never
  shorten an earlier pair's exclusion window (anti-loop preservation)
- SUSPICIOUS_SHRINK bypass: reframed measurement from pre-edit MEMORY.md
  instead of requiring runner to know .consolidation-backup suffix
- JSONL append: mandate trailing newline (printf '%s\\n') to keep file
  parseable as one JSON object per line

memory-protocol.md updated to lockstep on the do_not_reopen_before MAX rule
for the interactive resolution path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- memory-protocol.md: add "if absent" branch and today+90 default for
  do_not_reopen_before; agent-driven interactive resolution previously had
  no guidance on what value to write when the field was missing
- Phase D step 2: dedup key now (file-A, file-B, reason) so two genuinely
  distinct contradictions between the same pair aren't silently dropped
- Phase D step 2: reason sanitization strips em-dash so it can't collide
  with the ` — ` field separator in the bullet format
- Phase D step 1 / template: gate `### Lint (Phase B.5)` block on
  LINT_PHASE_B5_ENABLED so post-trial diary entries don't emit zeroed stats

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rom ralphex run)

The ralphex run for memory-lint-trial inadvertently reverted the
`permissions: pull-requests: read` block in .github/workflows/pii-scan.yml.
That block was added in commit b909b20 to fix private-repo gitleaks
permissions (PR #123). Restore to origin/main state — out of scope for
this PR.
Copilot AI review requested due to automatic review settings May 17, 2026 17:36
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a 30-day trial specification for memory linting: cross-file contradiction detection in the memory consolidation skill, pending-review surfacing rules, and supporting trial documentation. It also restores explicit GitHub Actions permissions for the local PII scan workflow.

Changes:

  • Adds Phase B.5 linting guidance, frontmatter fields, Pending Review handling, and lint stats output to memory-consolidation.
  • Adds platform rules for proactively surfacing and resolving pending lint findings.
  • Archives the implementation plan and restores pull-requests: read permissions in the PII scan wrapper workflow.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
.claude/skills/memory-consolidation/SKILL.md Defines the Phase B.5 contradiction-detection trial, frontmatter schema, pending-review updates, and stats/diary behavior.
.claude/rules/platform/memory-protocol.md Adds conversation-time surfacing and interactive resolution protocol for pending lint findings.
.github/workflows/pii-scan.yml Adds explicit token permissions for the local PII scan workflow.
docs/plans/completed/2026-05-17-memory-lint-trial.md Archives the trial plan and validation checklist.
Comments suppressed due to low confidence (3)

.claude/skills/memory-consolidation/SKILL.md:240

  • Phase D now assumes the Phase B.5 accumulators exist, but the pipeline can skip directly to Phase D on any fatal failure before Phase B.5 runs. In that path pending_resolved, pending_review, and the lint counters are undefined, so the error-reporting/cleanup phase can fail instead of recording the original failure. Initialize default lint accumulators before any phase that can fail, or have Phase D guard the lint substeps on accumulator availability as well as the feature flag.
Phase D substeps do NOT execute in literal numerical order. The diary (step 1) is written LAST among steps 1–3 so it can summarize the actual outcomes of steps 2 and 3 — including their issues — in a single write (the spec does NOT support amending an already-written diary section). Step 3 is also split: its compute-and-validate work runs early (to surface any issues), and the actual JSONL append runs after the diary. Execution order:

  1. **Step 2** (MEMORY.md "Pending Review" edit) — applies `pending_resolved` removals first, then dedup-appends `pending_review` adds in a single MEMORY.md mutation, finalizes `pending_added`; may surface a `SUSPICIOUS_SHRINK` rollback issue.
  2. **Step 3 compute-and-validate** — compute `pending_total` and `avg_age_days` from the post-step-2 MEMORY.md, compose the JSONL line, validate it via `jq -e`. This sub-step MAY surface issues to the Issues collector: malformed bullets whose `detected_at` fails to parse (counted with age 0; logged), or a candidate JSON line that fails `jq` validation (NOT appended; logged). Do NOT perform the actual `>>` append yet — defer it to after the diary.
  3. **Step 1** (diary) — write diary using the finalized `pending_added` AND the full Issues set collected in (1) and (2).
  4. **Step 3 append** — perform the actual `printf '%s\n' "$LINE" >> memory/lint-stats.jsonl`, only if (2)'s `jq` validation passed.

.claude/skills/memory-consolidation/SKILL.md:124

  • Using any recent ### Memory Changes mention as “direct diary/session evidence” conflates user-grounded updates with automated lint edits from prior runs. Phase B.5 itself updates memory/auto files and the diary records per-file create/update logs, so a file touched only by an auto-resolve can be treated as fresh conversation evidence for a different contradiction within 48 hours. That can make the evidence leg pick the wrong winner; the provenance check should distinguish human-session-derived Phase B updates from Phase B.5 maintenance edits.
   **Provenance extraction for step 4a.** Populate `recent_diary_mention: bool` by scanning diary files in `memory/diary/` whose filename date (`YYYY-MM-DD.md`) is within the last 48 hours of today's date. In each diary file, look only at lines that fall under a `### Memory Changes` subsection (between that heading and the next `### ` or `## ` heading or EOF — Phase D step 1 writes `### Memory Changes` as the canonical heading for the per-file create/update log). Set `recent_diary_mention = true` if any such line contains either (a) the file's bare basename (e.g., `topic-slug.md`) as a substring, OR (b) the path form `memory/auto/<basename>` as a substring. This is the operational definition of "direct diary/session evidence" used by step 4a — a file mentioned in a recent diary's Memory Changes block was grounded by a real conversation event, whereas a file absent from recent diaries is older/inferred. No frontmatter field is required; provenance is derived entirely from the diary log Phase D already writes. If no diary files exist or none fall within 48 hours, `recent_diary_mention = false` for all files (step 4a then yields no winner and the resolver falls through to step 4b).

.claude/skills/memory-consolidation/SKILL.md:270

  • The diary template puts explanatory text on the same line as the required heading. If the agent copies this format literally, the Markdown heading becomes ### Memory Changes # REQUIRED... rather than exactly ### Memory Changes, while Phase B.5's provenance scan depends on that exact subsection name. Move the explanation outside the code block or use a real HTML/Markdown comment on a separate line so the required heading remains literal.
   ### Memory Changes              # REQUIRED heading — do not rename; Phase B.5 step 1 parses this heading text to populate `recent_diary_mention` (renaming silently breaks the evidence leg of the auto-resolve hierarchy).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/pii-scan.yml
Comment thread .claude/skills/memory-consolidation/SKILL.md Outdated
Resolves Copilot review on PR #124: the previous wording was ambiguous —
top section presented LINT_PHASE_B5_ENABLED as if it were an editable
in-file value, but the Phase B.5 paragraph described it as an env var
where only an explicit 'false' disables it. A rollback per the documented
'edit this file' path would have left the env-var check unaffected.

Clarification: SKILL.md is the SOLE source of truth for the flag. Not
an env var, not external config. To roll back: edit the line in the
Feature Flags section to LINT_PHASE_B5_ENABLED=false, commit, next
nightly cron run picks up the new value.
@fitz123
Copy link
Copy Markdown
Owner Author

fitz123 commented May 17, 2026

@copilot please re-review — fixes pushed since the last review

Copy link
Copy Markdown

Copilot AI commented May 17, 2026

@copilot please re-review — fixes pushed since the last review

Re-reviewed the latest updates at 1acfb62 and the flag-source-of-truth wording is now consistent/explicit in SKILL.md. Current PR checks are green, with only the newest dynamic run still in progress.

@fitz123 fitz123 merged commit 8aeb9df into main May 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants