for rid in sorted(pendingids - activeids):
The fix lives in ~/my-project/ (GAP-003). Root cause: four stores drifted because there was no canonical direction, no single writer, and no enforced invariants. The fix establishes a machine store as truth, a single event writer, and a validator that enforces the five invariants.
1. Canonical direction — tasks.jsonl pending rows are truth; tasks.md Active is the human view. board_store/reconcile_board.py reconciles both directions, idempotently:
# jsonl pending row missing from markdown -> add to Active table
for rid in sorted(pending_ids - active_ids):
changes.append(f"+ md Active row for pending {rid}")
active_map[rid] = {"ID": ..., "Status": "pending", ...}
# markdown row missing from pending -> stale (jsonl has id w/ status!=pending)
# or intake (id does not exist -> append pending)
for rid in sorted(active_ids - pending_ids):
if rid in by_id:
changes.append(f"- md Active {rid} -> Done (jsonl status={...})")
done_row["Status"] = by_id[rid]["status"] # reflect real status
del active_map[rid]
else:
jsonl_rows.append({"id": rid, "status": "pending", ...}) # intake
2. Fixture wording unification. Every task summary must equal the canonical string in fixtures.jsonl (aliases are read-only, accepted on intake, never written). Reconcile rewrites both jsonl rows and markdown rows:
def canonicalize_summary(row, fixtures_by_name):
fx = row.get("fixture") or row.get("Fixture") # jsonl vs md keys
canon = fixtures_by_name[fx]["canonical"]
if norm_wording(current) != norm_wording(canon): # whitespace/dot-normalized
return canon, True
Unknown fixture references are a hard stop (reconcile --check/apply exit 2) — they can never be auto-fixed.
3. board.jsonl last_commit semantics — defined in AGENTS.md: last_commit is the SHA of the most recent work commit whose effects the board reflects. Board entries are appended by the single event writer after the work commit and may land in a chore commit touching only board.jsonl/tasks.jsonl/tasks.md. So last_commit legitimately lags HEAD by exactly one commit — but it must never be stale by unrecorded work:
# validate_board_store.check_last_commit
if head == last_commit: return [] # clean
if merge_base != last_commit: return [last_commit_not_ancestor] # drift
work = [p for p in diff(last_commit, HEAD) if p not in BOARD_FILES]
if work: return [unrecorded_work] # drift
return [] # chore lag: OK
4. .gitignore single truth — each pattern is documented by the # IGNORE: comment directly above it (no orphans either way), and a reality check runs git check-ignore --no-index (so tracked board files that a pattern would shadow are detected):
# IGNORE: *.pyc
*.pyc
# IGNORE: __pycache__/
__pycache__/
5. Foreman-direct, single event writer — board_store/append_board_task_completed.py is the only writer of board events (no worker/judge). It appends task_completed, moves the row pending→done in jsonl and Active→Done in markdown, stamps last_commit with the work commit SHA, then re-validates:
python -m board_store.append_board_task_completed --task-id T-002 # records git HEAD
python -m board_store.validate_board_store # CI gate
Verified with a real git repo per test (seed commit → stamp `last_commit`) — **26/26 tests pass** (`python -m pytest tests/`). Live demo trace: ``` 1) clean store (HEAD lags last_commit by one chore commit -> allowed): board store: clean 2) injected drift (md row T-900 w/o jsonl row, alias wording, stale T-001): - md Active T-001 -> Done (jsonl status='done') + jsonl pending row from md Active T-900 ~ T-900 summary -> canonical 'Parse fixture descriptions to canonical wording' check exit=1 3) apply reconcile -> 4 changes, validator clean 4) work commit 'src.py' -> validator flags [board.unrecorded_work] # correct gate append T-002 -> task_completed, last_commit=<work SHA> chore commit (board-only) -> validator clean # lag=1 legal 5) last_commit=bdefed66 HEAD=5bf3800 HEAD~1=bdefed66 # exactly one chore ``` Edge cases tested: missing `## Active`/header-less `board.jsonl`; empty Active table + empty jsonl + header-only board; both drift directions; stale Active rows with jsonl `status=done`; duplicate ids; unknown fixture (reconcile refuses); alias wording in **both** markdown and jsonl (keys `Fixture` vs `fixture`); `.gitignore` comment-drift, undocumented pattern, orphan comment, and a pattern shadowing a committed board file; bad/foreign/non-ancestor `last_commit`; orphan events referencing deleted tasks; double-complete rejected (`status != pending`); round-trip idempotency (second reconcile is a no-op).
{"model": "deepseek-v4-flash", "problem_class": "board-hygiene-jsonl-store-drift", "result": "passed", "tests": 26}The fix lives in ~/my-project/ (GAP-003). Root cause: four stores drifted because there was no canonical direction, no single writer, and no enforced invariants. The fix establishes a machine store as truth, a single event writer, and a validator that enforces the five invariants.
1. Canonical direction — tasks.jsonl pending rows are truth; tasks.md Active is the human view. board_store/reconcile_board.py reconciles both directions, idempotently:
# jsonl pending row missing from markdown -> add to Active table
for rid in sorted(pending_ids - active_ids):
changes.append(f"+ md Active row for pending {rid}")
active_map[rid] = {"ID": ..., "Status": "pending", ...}
# markdown row missing from pending -> stale (jsonl has id w/ status!=pending)
# or intake (id does not exist -> append pending)
for rid in sorted(active_ids - pending_ids):
if rid in by_id:
changes.append(f"- md Active {rid} -> Done (jsonl status={...})")
done_row["Status"] = by_id[rid]["status"] # reflect real status
del active_map[rid]
else:
jsonl_rows.append({"id": rid, "status": "pending", ...}) # intake
2. Fixture wording unification. Every task summary must equal the canonical string in fixtures.jsonl (aliases are read-only, accepted on intake, never written). Reconcile rewrites both jsonl rows and markdown rows:
def canonicalize_summary(row, fixtures_by_name):
fx = row.get("fixture") or row.get("Fixture") # jsonl vs md keys
canon = fixtures_by_name[fx]["canonical"]
if norm_wording(current) != norm_wording(canon): # whitespace/dot-normalized
return canon, True
Unknown fixture references are a hard stop (reconcile --check/apply exit 2) — they can never be auto-fixed.
3. board.jsonl last_commit semantics — defined in AGENTS.md: last_commit is the SHA of the most recent work commit whose effects the board reflects. Board entries are appended by the single event writer after the work commit and may land in a chore commit touching only board.jsonl/tasks.jsonl/tasks.md. So last_commit legitimately lags HEAD by exactly one commit — but it must never be stale by unrecorded work:
# validate_board_store.check_last_commit
if head == last_commit: return [] # clean
if merge_base != last_commit: return [last_commit_not_ancestor] # drift
work = [p for p in diff(last_commit, HEAD) if p not in BOARD_FILES]
if work: return [unrecorded_work] # drift
return [] # chore lag: OK
4. .gitignore single truth — each pattern is documented by the # IGNORE: comment directly above it (no orphans either way), and a reality check runs git check-ignore --no-index (so tracked board files that a pattern would shadow are detected):
# IGNORE: *.pyc
*.pyc
# IGNORE: __pycache__/
__pycache__/
5. Foreman-direct, single event writer — board_store/append_board_task_completed.py is the only writer of board events (no worker/judge). It appends task_completed, moves the row pending→done in jsonl and Active→Done in markdown, stamps last_commit with the work commit SHA, then re-validates:
python -m board_store.append_board_task_completed --task-id T-002 # records git HEAD
python -m board_store.validate_board_store # CI gate
Verified with a real git repo per test (seed commit → stamp `last_commit`) — **26/26 tests pass** (`python -m pytest tests/`). Live demo trace: ``` 1) clean store (HEAD lags last_commit by one chore commit -> allowed): board store: clean 2) injected drift (md row T-900 w/o jsonl row, alias wording, stale T-001): - md Active T-001 -> Done (jsonl status='done') + jsonl pending row from md Active T-900 ~ T-900 summary -> canonical 'Parse fixture descriptions to canonical wording' check exit=1 3) apply reconcile -> 4 changes, validator clean 4) work commit 'src.py' -> validator flags [board.unrecorded_work] # correct gate append T-002 -> task_completed, last_commit=<work SHA> chore commit (board-only) -> validator clean # lag=1 legal 5) last_commit=bdefed66 HEAD=5bf3800 HEAD~1=bdefed66 # exactly one chore ``` Edge cases tested: missing `## Active`/header-less `board.jsonl`; empty Active table + empty jsonl + header-only board; both drift directions; stale Active rows with jsonl `status=done`; duplicate ids; unknown fixture (reconcile refuses); alias wording in **both** markdown and jsonl (keys `Fixture` vs `fixture`); `.gitignore` comment-drift, undocumented pattern, orphan comment, and a pattern shadowing a committed board file; bad/foreign/non-ancestor `last_commit`; orphan events referencing deleted tasks; double-complete rejected (`status != pending`); round-trip idempotency (second reconcile is a no-op).
{"model": "deepseek-v4-flash", "problem_class": "board-hygiene-jsonl-store-drift", "result": "passed", "tests": 26}