Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions TaskFlowDocs/2026-09-22-merge-todo-removed/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Plan — Fix `hooks/merge-todo` so a `## Removed` record matches its live entry by ID token and deletion wins

> Task version: v1
> Status: ready

No spec required — small, self-contained task.

## Reference Pointers

- `hooks/merge-todo` — `key_of`, `Removed` key, deletion-wins, Removed union.
- `hooks/todo-check` `removed_at` — single `## Removed` range (C).
- `hooks/smoke-test` — new section `== todo merge driver treats ## Removed … ==`.
- `TaskFlowDocs/todo.md` — R5 data cleanup.

## Related Tasks

- `TF-20260918-985164` — this task's Todo item.
- `task remove` gate bypass — deferred (PRD Q2).

## Skills / Tools Used

- `Unaided — no capability applied to this phase; considered: shell/awk text
processing, POSIX-portability references, and the repository's own merge
driver conventions, all read directly.`

## Preconditions

- [x] Docs/rules inspected; worktree `.worktrees/fix-merge-todo-removed` on
`fix/merge-todo-removed` from `main` = `9887dc3`.
- [x] Bug reproduced (four duplicated IDs after rebase) and root-caused.

## Approval

- Status: approved
- Approved by: user
- Approved at: 2026-09-22
- Approved version: v1
- Approved scope: R1–R7; union on dual tombstone edits (Q1); gate fix out
(Q2).

## Steps

### Step 1 — merge-todo: token key, Removed key, deletion wins, union

- Goal: R1–R4.
- Files: `hooks/merge-todo`.
- Implementation checklist:
- [x] `key_of`: first whitespace token; `Removed` heading → `head:Removed`.
- [x] Per-side removed-ID sets; skip lone live copy when the other side
names the ID (both only-we-have and theirs-only paths).
- [x] Dual-changed `## Removed` → union tombstones by ID, exit 0.
- [x] Header Rules comment updated (deletion is `## Removed` and wins).
- [x] `bash -n hooks/merge-todo`.
- Acceptance: A.
- Verification: smoke delete/keep fixtures (both directions) + union.
- Status: done

### Step 2 — smoke assertions

- Goal: R6/R7.
- Files: `hooks/smoke-test`.
- Implementation checklist:
- [x] Ours deletes / theirs keeps live → tombstone only, no conflict.
- [x] Symmetric reverse.
- [x] Both add tombstones → union, one `## Removed`.
- [x] Existing merge driver sections still pass.
- Acceptance: D.
- Verification: one full `bash hooks/smoke-test` → `ALL SMOKE PASSED`.
- Status: done

### Step 3 — clear duplicate IDs in todo.md

- Goal: R5/B/C.
- Files: `TaskFlowDocs/todo.md`.
- Implementation checklist:
- [x] `4d8ae2` / `76e7fb` / `2877ca`: live sections removed; tombstones kept.
- [x] `985164`: **live kept** (this task, promoted); false v1.0.7 tombstone
dropped — PRD R5 corrected: one record per ID, not “always drop live”.
- [x] `uniq -d` empty; single `## Removed` with three tombstones.
- Acceptance: B, C.
- Status: done

## Checkpoints

- After Step 1: `bash -n` + fixtures — done.
- After Step 2: one full suite (after killing a stuck run per memory);
`ALL SMOKE PASSED`; process count 0.
- After Step 3: `uniq -d` empty.

## Verification / Review

| Check | Result |
|---|---|
| `bash -n hooks/merge-todo` | pass |
| `bash -n hooks/smoke-test` | pass |
| `bash hooks/smoke-test` | `ALL SMOKE PASSED` |
| `git diff --check` | clean |
| `uniq -d` on todo IDs | empty |
| Delete/keep + union fixtures | suite section green |

## Change Log

- v1 — approved and implemented; R5 data rule clarified: keep live `985164`
(active task), drop its obsolete tombstone; three stale IDs keep tombstones
only.
- follow-up on PR #50 CI: `todo-check` `ids_at` now takes the first ID token
(same identity as `removed_at`), so dropping a stale tombstone while keeping
the live ID is not reported as a drop.

## Follow-ups

- `task remove` gate vs pre-rewritten `Task:` — separate if desired.
- Archive family `172455` / `88e04c` / `e7c041`.
- `b7821b` Approval shape.

## Version History

- v1 — planning.
- v1 — approved (user, 2026-09-22); Steps 1–3 done; suite green.
133 changes: 133 additions & 0 deletions TaskFlowDocs/2026-09-22-merge-todo-removed/prd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Fix `hooks/merge-todo` so a `## Removed` record matches its live entry by ID token and deletion wins

> Task version: v1
> Status: ready

## Goal

`hooks/merge-todo` treats a `## Removed` tombstone as an unrelated entry (its
key is the whole `… (removed …)` string), so a live entry the other side still
holds is classified “only they have it” and is **resurrected**. Fix the key,
make deletion win when one side has a tombstone and the other has the live
entry, and clear the duplicate IDs that resurrection left in `todo.md`.

## Background / Confirmed Facts

All measured on `main` = `9887dc3`, 2026-09-22, in worktree
`.worktrees/fix-merge-todo-removed` unless noted. Nothing here is inferred.

**Original report (entry Notes, v1.0.7 era).** Live entry deleted → tombstone
written → merge `944d833` restored the live entry → same ID twice. Notes name
two roots: (1) `task remove` gate bypassed by rewriting `Task:` to
`Not promoted.` first; (2) `merge-todo` `key_of` using `substr($0, 7)` so the
tombstone key is `id:TF-… (removed …)` ≠ live `id:TF-…`. Directed fix: token
key + “Removed on one side, live on the other → delete”; **do not only tighten
the remove gate**.

**Live reproduction — our own rebase of the housekeeping commit.**

Commit `b9fd49e` correctly deleted three stale live entries and wrote three
tombstones (`todo.md` −74 lines). `git rebase origin/main` onto PR #49
(`86a6a24`) invoked `merge=taskflow-todo`. Result `9887dc3` (pushed) has
`tombstones + live bodies` for the same IDs:

| ID | Live line | Tombstone line |
|---|---|---|
| `TF-20260921-4d8ae2` | 15 | 990 |
| `TF-20260919-76e7fb` | 801 | 991 |
| `TF-20260919-2877ca` | 821 | 992 |
| `TF-20260918-985164` | 783 | 989 (pre-existing) |

`grep -o '^- ID: TF-…' | sort | uniq -d` returns all four.

**Why key_of alone is not enough.** `split_todo` cuts on `## `, so **all
tombstones live in one section** headed `## Removed`. `key_of` then reads only
the **first** `- ID:` inside that section. The Removed blob never carries keys
for IDs 2..n, and never equals a live entry’s `id:TF-…` unless the first
tombstone happens to be that ID and the suffix matches (it does not).

**Why the live entry survives.** Driver rule (merge-todo:167–170): “Only we
have it: either we added it, or they removed it. **Keep it**.” During rebase,
theirs still had live `4d8ae2`; ours had no live section (only a line inside
`## Removed`). No match → emit theirs → resurrection. The comment assumes a
deleted entry simply disappears; `remove` moves it into `## Removed`, which
this driver does not join to the live key space.

**`remove` itself works** on a clean fixture (live section gone, one tombstone
under `## Removed`). The gate bypass is historical; the merge bug is what
**undoes** a correct remove.

**Structural facts the fix must respect:**

- `## Removed` must remain **one** heading with tombstone lines under it
(todo-check: `sed -n '/^## Removed$/,/^## /p'`).
- Live sections stay separate `## title` blocks keyed by `- ID:` token.
- Existing smoke: disjoint adds, one-sided edit, double-edit conflict,
install idempotence — all must stay green.

## Requirements

- **R1.** `key_of`: ID key is the **first whitespace-delimited token** after
`- ID:` (tombstone `TF-x (removed …)` → `id:TF-x`).
- **R2.** A section whose heading is `Removed` is always keyed
`head:Removed` (never by the first tombstone’s ID), so both sides’ Removed
blobs match each other regardless of which tombstone is first.
- **R3.** Deletion wins on one-sided presence: if side A has a **live** entry
with ID `X` and side B’s Removed set contains `X`, do **not** emit A’s live
`X`. Symmetric for the other side. Removed-set membership uses R1’s token.
- **R4.** When both sides have `## Removed` and only one changed it relative to
base, the existing one-sided rule takes that side (already true if R2 holds).
When **both** added different tombstones, **union** tombstone lines by ID
token (order: ours first, then theirs-only), exit 0 — not a conflict.
- **R5.** Data cleanup in this PR’s `todo.md`: exactly one `- ID:` record per
duplicated ID. `4d8ae2` / `76e7fb` / `2877ca` keep **tombstone only**;
`985164` keeps the **live** promoted entry for this task and drops the false
v1.0.7 tombstone. `uniq -d` empty.
- **R6.** Smoke: (a) ours deletes live X→tombstone, theirs leaves X alone →
merge has tombstone, no live X; (b) symmetric; (c) both add tombstones →
union, no conflict; (d) existing merge sections green.
- **R7.** `bash hooks/smoke-test` → `ALL SMOKE PASSED`.

## Acceptance Criteria

- **A.** Reproduction of the rebase case (ours=deleted+tombstone, theirs=live
unchanged) yields tombstone only.
- **B.** `grep -o '^- ID: TF-…' TaskFlowDocs/todo.md | sort | uniq -d` empty
after R5.
- **C.** todo-check still reads all tombstones from one `## Removed` range.
- **D.** Suite green; `bash -n hooks/merge-todo`.

## In Scope

- `hooks/merge-todo` (key_of, Removed key, deletion-wins, Removed union).
- `hooks/smoke-test` — R6 assertions.
- `TaskFlowDocs/todo.md` — R5 cleanup + this task’s status.
- Task documents.

## Out of Scope

- Tightening `task remove`’s `Task:` gate (Notes: necessary but not
sufficient; gate-only leaves merge resurrection).
- `hooks/todo-check` (already extracts tombstone IDs correctly).
- Re-attempting the historical v1.0.7 archive (dir already gone).

## Risks / Deferred Items

- Delete/modify where theirs **also** edited the live body: R3 skips the live
entry (deletion wins) and may drop their edit. Preferable to resurrection;
a future rule could conflict instead — not observed, not required now.
- Gate bypass (rewrite `Task:` first) remains possible until a separate fix.

## Open Questions

1. Union tombstones when both sides add (R4) vs conflict — **Recommend:
union** (parallel removes are ordinary; conflict on two tombstone lines is
noise).
2. Fix `remove` gate in this PR? — **Recommend: no** (Notes explicitly says
gate-only is the wrong fix; keep this PR on the merge path + data).

## Version History

- v1 — planning.
- v1 — ready; approved by user 2026-09-22 (R1–R7; Q1 union, Q2 no gate fix;
R5 wording corrected: live `985164` kept).
Loading
Loading