Implementation Plan

Add merge driver for docs/plans/index.html

todo
2026-06-10 agentics fix

Kill the recurring docs/plans/index.html merge conflict for good: add a git merge driver that auto-resolves the generated gallery index by unioning the plan cards from both sides whenever two plan branches collide — so every merge keeps all plans and no contributor ever hand-resolves the index again. Mirror the proven marketplace.json driver wiring already in the repo.

Implement Read and implement all steps in the plan at docs/plans/add-plans-index-merge-driver.html — Auto-resolve plans-index merge conflicts via a git merge driver
Run as workflow — launch parallel subagents
Run a workflow to implement the plan at docs/plans/add-plans-index-merge-driver.html — Auto-resolve plans-index merge conflicts via a git merge driver
File add-plans-index-merge-driver.html
Path docs/plans/add-plans-index-merge-driver.html
Acceptance criteria 0 / 8 done

Context

docs/plans/index.html is a fully generated file. docs/plans/build-index.sh rebuilds it from every non-index plan .html file, and the kit/plugins/plan-agent/hooks/rebuild-plans-index.py PostToolUse hook re-runs that script on every plan write. Because every plan-adding PR regenerates the index, any two concurrent plan PRs produce a textual merge conflict in this one file — PR #308 hit it twice in a single day.

The repo already solves the identical problem for .claude-plugin/marketplace.json with a custom git merge driver: scripts/merge-marketplace.mjs, mapped in .gitattributes via merge=mkt-version, with the per-clone git config merge.<name>.driver registered idempotently by scripts/setup-merge-driver.sh (auto-run from a SessionStart hook in .claude/settings.json). This plan reuses that exact split: a committed .gitattributes mapping plus a per-clone driver registration.

Resolution strategy (per .claude/rules/plan-hygiene.md): the driver unions the gallery cards from both sides rather than regenerating from disk. Sandbox-verified on git 2.50 (the default ort strategy): a merge driver runs during the in-memory merge, before the incoming branch's new plan files are checked out — so regenerating the index from disk inside the driver would silently drop every incoming plan. But both index versions already contain their side's cards as blobs (%A ours, %B theirs, %O base), so unioning those cards yields a complete index with no disk access, and bakes the resolution straight into the merge commit — exactly how scripts/merge-marketplace.mjs unions plugins[]. Ours' cards keep their order, new cards from theirs are appended, and a card deleted on either side relative to the base stays deleted.

Why no post-merge regeneration: the union already produces a correct, complete index at merge time, so no git hook is needed. Any ordering or timestamp drift versus a clean rebuild is purely cosmetic and self-heals on the very next plan write (which re-runs build-index.sh over the full set on disk). Merge drivers live in local .git/config and never travel with the repo, so this pays off in the repo's standard local rebase-on-main-before-push workflow — the driver fires for git merge, each replayed git rebase commit, and git cherry-pick alike, leaving a branch conflict-free before it reaches GitHub.

Files to Modify

agentics/
  • scripts/
    • merge-plans-index.mjs new driver: union gallery cards from both sides
    • setup-merge-driver.sh modified register the plans-index node driver
  • .claude/rules/
    • marketplace.md modified cross-reference the plans-index driver
    • plan-hygiene.md modified canonical union-strategy doc (added this session)
  • tests/
    • merge-plans-index.test.sh new union + two-branch merge smoke test
    • fixtures/merge-plans-index/
      • base.html, ours.html, theirs.html, expected.html new union test fixtures
  • .gitattributes modified map index.html to merge=plans-index
  • CONTRIBUTING.md modified first-time setup note
  • docs/GITHUB_SETUP.md modified add driver to file inventory

Diagram

Conflict resolution flow
Trigger
git merge / rebase
two concurrent plan branches both regenerated docs/plans/index.html
Conflict
docs/plans/index.html
the generated index differs on both sides
Resolve
merge-plans-index.mjs
union cards from %A + %B, respect base deletions, exit 0
Merged
docs/plans/index.html
holds every plan card — baked into the merge commit, no markers
Self-heal
next plan write → build-index.sh
re-sorts / refreshes; ordering & timestamp drift is cosmetic

Steps

1
todo Write the union merge driver scripts/merge-plans-index.mjs
Git invokes it as node merge-plans-index.mjs %O %A %B (base, ours, theirs). Parse all three index.html versions, extract the <a class="gallery-card" href="…"> entries keyed by href (unique per plan), and union them: ours' cards keep their order, theirs' new cards are appended, and a card present in base but dropped on either side stays dropped. Reassemble ours' document with the unioned card list, write it to %A, and exit 0. On any parse failure, exit 1 so git keeps conflict markers for a human. Mirror the structure and robustness of scripts/merge-marketplace.mjs. chmod +x the file.
Verify
Run on three fixtures — base, ours (adds card X), theirs (adds card Y): node scripts/merge-plans-index.mjs base.html ours.html theirs.html; echo $? prints 0, and ours.html now holds both card X and card Y with no <<<<<<< markers. A card in base+theirs but removed from ours stays absent.
2
todo Map the index path in .gitattributes
Append docs/plans/index.html merge=plans-index to .gitattributes (which already maps marketplace.json to mkt-version). This is the only committed half of the wiring — it tells every clone which named driver handles this path; the driver definition is per-clone (Step 3).
Verify
git check-attr merge -- docs/plans/index.html prints docs/plans/index.html: merge: plans-index, and the existing git check-attr merge -- .claude-plugin/marketplace.json still prints mkt-version.
3
todo Register the driver in scripts/setup-merge-driver.sh
Leaving the existing mkt-version block untouched, add git config merge.plans-index.name "plans index gallery-card union" and git config merge.plans-index.driver pointing at node "$TOPLEVEL/scripts/merge-plans-index.mjs" %O %A %B. No git hooks are installed — the union driver resolves fully at merge time, so post-merge regeneration is unnecessary (cosmetic drift self-heals on the next plan write). Reusing this script means the existing SessionStart hook activates the driver per session with no settings.json change; non-Claude contributors run it once.
Verify
Run bash scripts/setup-merge-driver.sh twice — it must not error or duplicate config. Then git config --get merge.plans-index.driver prints the node "…/merge-plans-index.mjs" %O %A %B command, and git config --get merge.mkt-version.driver is still set (untouched).
4
todo Add union fixtures and the smoke test tests/merge-plans-index.test.sh
Add tests/fixtures/merge-plans-index/{base,ours,theirs,expected}.html (minimal index files differing only in their gallery cards) for a deterministic unit check of the union, and write an executable tests/merge-plans-index.test.sh that (a) runs the driver against the fixtures and diffs the result against expected.html, and (b) in a throwaway mktemp repo wires up the driver + .gitattributes, branches a and b that each add a distinct plan and regenerate index.html, merges b into a, and asserts the merge exits 0, leaves no <<<<<<< markers, and the merged index.html holds both plans' cards. Exit non-zero on any failed assertion. The repo's tests/fixtures/merge-marketplace/ establishes exactly this fixture pattern.
Verify
bash tests/merge-plans-index.test.sh prints a success line and exits 0; corrupting expected.html or removing the .gitattributes mapping in-sandbox makes it exit non-zero (proving it truly tests the driver).
5
todo Align the docs with the new driver
Mirror the mkt-version driver's documentation footprint: (1) CONTRIBUTING.md "First-time Setup" — note that setup-merge-driver.sh now also registers the plans-index driver; (2) docs/GITHUB_SETUP.md — add scripts/merge-plans-index.mjs beside scripts/merge-marketplace.mjs in the file inventory; (3) .claude/rules/marketplace.md "Merge driver" section — cross-reference the plans-index driver. .claude/rules/plan-hygiene.md already documents the union strategy in full (added this session) and stays the canonical description.
Verify
grep -rl "plans-index\|merge-plans-index" CONTRIBUTING.md docs/GITHUB_SETUP.md .claude/rules/marketplace.md .claude/rules/plan-hygiene.md lists all four files.

Tests

Tier 1 — Code-touching plan
Objective Concurrent plan branches auto-resolve docs/plans/index.html

File: tests/merge-plans-index.test.sh

Type: smoke test (self-contained mktemp git sandbox)

Asserts: two branches that each add a distinct plan file and both edit the index merge with no manual conflict resolution (exit 0, no conflict markers), and the merged docs/plans/index.html holds both plans' gallery cards — directly proving the plan's objective.

Run: bash tests/merge-plans-index.test.sh

Unit Gallery-card union logic

File: tests/merge-plans-index.test.sh driving tests/fixtures/merge-plans-index/*.html

Targets: scripts/merge-plans-index.mjs

Key cases: ours' card order preserved and theirs' new card appended (output equals expected.html); a card in base+theirs but removed from ours stays absent (no resurrection); malformed input makes the driver exit 1 so git keeps conflict markers.

Integration Full local merge + rebase

File: tests/merge-plans-index.test.sh

Targets: driver + .gitattributes mapping + docs/plans/build-index.sh working together in a sandbox repo.

Key cases: the merge path unions both plans' cards into index.html with no markers; the rebase path replays the union per commit; the existing mkt-version driver remains registered after setup runs (no clobber).

Acceptance Criteria

Verification

Automated: run bash scripts/setup-merge-driver.sh then bash tests/merge-plans-index.test.sh — the smoke test must pass (exit 0).

Manual end-to-end: from a clean clone, create two branches off main that each add a distinct plan file under docs/plans/ and commit (each commit regenerates index.html via the existing PostToolUse hook). Then git checkout branch-a && git merge --no-edit branch-b: the merge must complete with no conflict prompt, and docs/plans/index.html must contain gallery cards for both new plans. Repeat with git rebase in place of merge to confirm the driver also resolves on the replay path.

Non-regression: confirm git config --get merge.mkt-version.driver is still set and git check-attr merge -- .claude-plugin/marketplace.json still reports mkt-version, proving the marketplace driver is untouched.

Completion Checklist

Required

Completion Report

No items to report — all requirements met.

Next Steps

Add a CI guard that the driver wiring stays intact

Paste this prompt into Claude to execute this follow-up:

In the agentics repo, add a lightweight CI check (a new job or a step in an existing workflow) that asserts the plans-index merge-driver wiring is present: (1) .gitattributes maps docs/plans/index.html to merge=plans-index, (2) scripts/merge-plans-index.mjs exists, and (3) scripts/setup-merge-driver.sh registers merge.plans-index. Run tests/merge-plans-index.test.sh in CI and fail the build if it does not pass. Mirror the style of any existing CI guard for the marketplace driver.
Apply the same driver to the social media gallery index

Paste this prompt into Claude to execute this follow-up:

In the agentics repo, docs/media/social/index.html is a generated gallery index with the same concurrent-PR merge-conflict shape as docs/plans/index.html. Extend the plans-index merge-driver pattern to cover it: add a .gitattributes mapping for docs/media/social/index.html, generalize scripts/merge-plans-index.mjs so its gallery-card union works for that gallery's card markup too, and register the mapping in scripts/setup-merge-driver.sh. Add a smoke test mirroring tests/merge-plans-index.test.sh and update the docs.
Wish List
One generic "generated-index" merge driver shared by every gallery Wish List

Speculative / blue-sky idea — not on the critical path. Paste into Claude when ready to explore:

Design a single generic git merge driver for the agentics repo that auto-resolves ANY generated gallery index (docs/plans/index.html, docs/media/social/index.html, and future ones) by unioning their cards. Drive it from a small config map (glob pattern → card selector) so adding a new generated gallery is a one-line registration rather than a new driver. Propose the config format and the scripts/setup-merge-driver.sh changes, and confirm a single .gitattributes mapping per gallery is all that's needed.
Unresolved Questions
  • Do GitHub server-side merges need a CI safety net?
    For the agentics repo plans-index union merge driver: it fires on local git merge, rebase, and cherry-pick, but merge drivers are local-only, so GitHub's server-side "Merge"/"Squash" buttons never run it — a PR merged on github.com without a prior local rebase could still conflict on docs/plans/index.html. Investigate whether the repo's standard rebase-on-main-before-push workflow makes this a non-issue in practice, or whether a CI safety net is warranted — e.g. a job that regenerates docs/plans/index.html on the default branch after merge and commits the refresh, or a required-status check that blocks merging a branch behind main. Recommend one approach with reasoning, avoiding redundant rebuilds and false positives.