Skip to content

bundles + composites: curated and ad-hoc multi-rule files with upstream tracking: https://github.com/riboseinc/claude-memory-files/issues/17 #17

Description

@opoudjis

Bundles: curated multi-rule files as a new content type

Full plan: https://github.com/riboseinc/claude-memory-files/blob/main/plans/v1-design.md

Filed as a follow-on to issue #1 (foundational v1 design) and issue #2 (update regulation). Surfaces a design question that wasn't visible until a real-world user started consolidating rules in their local ~/.claude/.

The triggering observation

During an audit of one maintainer's local ~/.claude/instructions/ we identified that several related rules — all four github-* PR-mechanics files (github-pr-title-issue-link, github-pr-assignment, github-content-via-file, github-claude-attribution) — fire at the same moment (every gh pr create invocation) and are mentally retrieved as a single concept ("PR discipline"). Twenty separately-named autoloaded rules dilute attention at conversation depth; collapsing co-firing rules into one named hook plausibly improves retention.

The same logic applies to greek-register + greek-terminology (one hook: "Greek narration") and to narrate-plan + paragraph-shape + bilingual-narration + bash-descriptions (one hook: "narration discipline"). At least three natural clusters exist today; more will surface.

The design problem this creates

The v1 schema's atomic cherry-pick unit is one rule per file. Every byte of the contract — name == filename validator rule, /install-memory-file <slug>, the manifest's 1-slug-↔-1-installed-file invariant — assumes that.

Merging upstream rules into one local file breaks the invariant:

  • the manifest can't track per-constituent upstream versions
  • --update can't say "rule X you merged has been amended upstream"
  • uninstalling 1 of 4 constituents means hand-surgery on a merged file
  • ad-hoc merges are private; colleagues can't share them

We could ignore this and tell users "if you merge, you lose upstream tracking on those files". That preserves the schema's simplicity but sacrifices exactly the bidirectional traceability that motivated curating the repo in the first place.

The three options considered

Option 1 — Upstream stays granular; user-side merges are unsupported. Cleanest from upstream's perspective. Users who want clusters do it in their local copy and accept loss of upstream tracking. Downside: sacrifices the trace back to upstream for updates, which is the design's whole premise.

Option 2 — Upstream ships curated bundles as a new content type. Add type: bundle with includes: [slug1, slug2, ...] frontmatter. The bundle file's body is the merged operational text. The installer treats bundle as "atomic install of the merged text, but manifest records the bundle slug AND each constituent slug for upstream tracking". Constituents continue to exist as separate files upstream — installable standalone for users who only want one. --update against a bundle checks each constituent's upstream version and warns when any constituent has diverged from the curated bundle. Adds one new type, one new frontmatter field, validator rules for bundle integrity, installer logic for the bundle install/update path.

Option 3 — Ad-hoc user-side merge with manifest tracking. No new upstream type; the installer grows a --merge-into <local-name> <slug1> <slug2> ... mode that fetches each constituent's body, concatenates them under a user-named local file, and records composite-of: [slug1, slug2, ...] in the manifest. Downside: re-merging blows away local hand-edits; ad-hoc merges are private (no sharing).

Recommendation

Option 2 (curated bundles) for v1.1.

Reasons:

  1. Bundles match the actual use case: a curated cluster with a named hook, not an arbitrary user-side concatenation. The retention benefit comes from the named hook, which only makes sense if the bundle is a stable named artefact — which is what curation gives you.
  2. It preserves cherry-pick atomicity for users who don't want the cluster. Other devs may want only github-pr-title-issue-link standalone; granular files keep that path open.
  3. Implementation cost is contained: one frontmatter field, ~30 lines of installer logic, one validator rule. Cheaper than Option 3's ad-hoc merge engine.
  4. Bidirectional upstream tracking is preserved by construction: the bundle's includes: list IS the trace from merged file to constituents.

Option 3 (ad-hoc user-side merge) planned for v1.2. Real use case, distinct from curated bundles: a user may want to merge rules in combinations the upstream maintainers haven't curated (or shouldn't have to curate), and still keep upstream-tracking discipline. Bundles cover the "named, shared, curated cluster" case; ad-hoc merges cover the "this one user's local convenience" case. Both are legitimate; shipping them in sequence (bundles first because they're easier to reason about, ad-hoc second once bundle semantics have settled) keeps the v1.1 surface contained without forfeiting Option 3.

Proposed schema additions for v1.1

type: bundle                            # new top-level value alongside instruction, memory-*, etc.
includes: [github-pr-title-issue-link,  # required for type: bundle; must be ≥ 2 entries
           github-pr-assignment,         # each must reference a slug that exists in the repo
           github-content-via-file,
           github-claude-attribution]
target: ~/.claude/instructions/<name>.md  # bundles install to the instructions/ target by default
                                           # (since all the seed bundles are clusters of instructions)
autoload: true                             # bundles inherit autoload semantics from instruction type

Validator rules (additive):

  • includes: mandatory for type: bundle, must have ≥ 2 entries.
  • Each entry in includes: must resolve to an existing slug in the repo.
  • A bundle and its constituents can coexist in the repo; a constituent can appear in multiple bundles (no exclusivity).
  • The bundle body should textually contain (or supersede) the operational content of each constituent — validator can't enforce this semantically, but a soft check warns if the bundle body is shorter than the sum of constituent bodies × 0.5 (a heuristic).

Manifest extensions (additive):

{
  "slug": "github-pr-discipline",
  "type": "bundle",
  "includes": ["github-pr-title-issue-link", "github-pr-assignment",
               "github-content-via-file", "github-claude-attribution"],
  "constituents-installed-refs": {       // upstream ref of each constituent at install time
    "github-pr-title-issue-link":  "abc123",
    "github-pr-assignment":         "abc123",
    "github-content-via-file":      "abc123",
    "github-claude-attribution":    "abc123"
  }
}

Installer behaviour:

  • install <bundle-slug>: refuses if any constituent is currently installed standalone (offer --replace-constituents to uninstall them first).
  • --update <bundle-slug>: fetches the bundle's own current ref; also fetches each constituent's current ref; reports "constituent X moved from 1.0.0 → 1.1.0 but the bundle hasn't been re-curated to incorporate the change yet". The user can wait for re-curation, or install the standalone constituent (which then requires uninstalling the bundle, since the standalone and bundle conflict).
  • --remove <bundle-slug>: removes the merged file, manifest entry gone.

Option 3 detail (v1.2)

The ad-hoc user-side merge mechanism is a separate, additive feature. Sketch of the design (full detail to be elaborated when v1.2 work opens):

New installer mode: tools/install.sh --merge-into <local-name> <slug1> <slug2> .... Fetches each constituent's body from upstream, concatenates them under a user-named local file at ~/.claude/instructions/<local-name>.md (or ~/.claude/memory/<local-name>.md for memory constituents), wires the @-include if applicable. The local file's frontmatter is synthesised: type: instruction (or memory-*), name: <local-name>, no scope: (because the constituents may differ in scope), plus the manifest-only composite-of: field.

Manifest extension:

{
  "slug": "<local-name>",
  "composite-of": ["slug1", "slug2", "slug3"],
  "composite-installed-refs": {
    "slug1": "abc123", "slug2": "abc123", "slug3": "abc123"
  },
  "composite-edited-locally": false   // tracks whether the merged file has been hand-edited since merge
}

--update semantics for composites:

  • Fetch each constituent's current upstream ref; compare to recorded composite-installed-refs.
  • If no constituent has changed: no-op.
  • If any constituent has changed AND composite-edited-locally == false: prompt "re-merge from upstream?" — re-fetch all constituents, re-concatenate, overwrite. Updates the recorded refs.
  • If any constituent has changed AND composite-edited-locally == true: prompt "your local merged file has hand-edits since the last merge; re-merge will overwrite them. Options: re-merge / abort / diff first" — default abort.

Bundles vs composites — when to use which:

Curated bundle (v1.1) Ad-hoc composite (v1.2)
Origin Shipped upstream, named, curated User invokes locally with constituent list
Sharing Discoverable by all users via /list-memory-files Private to the one user who created it
Body curation Maintainer arranges constituents for narrative flow Mechanical concatenation, no narrative curation
Versioning Bundle has its own version:, evolves on re-curation No bundle version; derives identity from constituent refs
Use case "Three or more users will want this same merge" "I personally want these two rules merged for my workflow"

A user can promote an ad-hoc composite to a curated bundle by submitting it through /submit-memory-file; the submission process scaffolds the bundle frontmatter from the composite's manifest entry. The reverse — installing a curated bundle as an ad-hoc composite — is also supported (--as-composite flag), for users who want to customise the merged body locally and accept the loss of bundle-level upstream tracking in exchange.

Seed bundle to ship in v1.1 (proposal)

One curated bundle, drawn from the audit:

Bundle slug Constituents Trigger context Scope
github-pr-discipline github-pr-title-issue-link, github-pr-assignment, github-content-via-file, github-claude-attribution Every gh pr create universal

This is the cleanest bundle candidate: all four constituents are scope: universal, all fire at the same moment, and the merge is uncontroversial — any user opening a PR wants all four. Additional bundles can be curated later as patterns emerge in submissions.

Worked example of an ad-hoc composite (v1.2)

The audit surfaced two clusters that are not bundle candidates because they combine scope: universal (or near-universal) rules with scope: personal-share rules:

Composite (private to one user) Constituents Why not a bundle
narration-discipline narrate-plan (universal), paragraph-shape (universal), bilingual-narration (personal-share — Greek-speaking authors only), bash-descriptions (personal-share — bilingual description: field) Mixes public and personal rules; not all users have Greek narration to discipline. Curating as a single upstream bundle would force-bundle a personal preference into a notionally-universal merge.
greek-narration greek-register (personal-share), greek-terminology (personal-share) Both personal-share to one user. A "bundle of personal-share files" is conceptually odd — bundles are for content multiple users will want; personal-share content has one author by definition.

These are the canonical Option-3 use cases. Without composites, a user with Greek-language narration discipline would have to either accept living with the four files separately (the retention problem that motivated this whole ticket) or merge them locally and lose upstream tracking. Composites preserve upstream tracking on each constituent while letting the user collapse the named-hook surface to one entry per cluster.

The narration-discipline example also demonstrates a property bundles can't easily express: mixed-scope composition. The user's local file legitimately mixes scope: universal rules with scope: personal-share rules because the user's lived workflow doesn't respect that boundary. Bundles are constrained to single-scope curations (mixing scopes upstream invites confusion about who the bundle is for); composites are unconstrained because they're private.

Designated v1.2 reference test case. narration-discipline is the test case for the Option 3 implementation: any v1.2 PR that wires the composite mechanism must demonstrate it by composing this specific cluster (two universals + two personal-shares), exercising the mixed-scope path. Until v1.2 lands, the cluster stays uncollapsed in the maintainer's local ~/.claude/ — deliberately, as evidence of the retention cost composites are meant to relieve.

Scope clarification — what bundles are not

  • Bundles are not categories. The repo already has instructions/, memory/, rules/, etc. — those are content-type categories. A bundle is one specific merged file, not a category of files.
  • Bundles are not collections. "All universal-scope rules" or "all metanorma-team rules" is a filter on the catalogue, not a bundle. The picker can already filter on scope: and team:; that's a different mechanism.
  • Bundles are not user-defined. Curated bundles ship upstream; user-defined ad-hoc merges are the Option-3 mechanism, deferred separately.

Relationship to existing tickets

  • Populate repo #1 (foundational design) — bundles are an additive extension to the schema laid out there; v1 ships without them, v1.1 adds them. No v1 design changes required.
  • Update regulation policy: author authority, semver, deprecation, --update flow #2 (update regulation) — bundles inherit the existing semver/owners:/CODEOWNERS rules; the bundle file has its own version, separate from constituent versions. The bundle's version: bumps when the curation changes (text rearranged, constituent added/removed); the constituents' versions evolve independently.
  • Plan fileplans/v1-design.md will be updated to add a "Bundles (v1.1)" subsection pointing at this issue, and to add this issue to the "Issues generated to fulfil this plan" index. That update is a separate plan-only commit.

Open questions for the thread

  • Conflict policy. Should the installer refuse to install a bundle when standalone constituents are present (current proposal), or auto-uninstall the constituents with a single confirmation? Refusing is safer; auto-uninstall is friendlier.
  • Constituent-divergence handling. When a constituent's upstream version is ahead of what's in the bundle, the proposal is to warn but otherwise leave the bundle alone. Alternative: prompt the user to choose between "wait for bundle re-curation" and "install constituent standalone, uninstall bundle". The current proposal is the lower-friction default; the alternative gives the user explicit agency.
  • Bundle vs constituent exclusivity. The proposal allows a constituent to belong to multiple bundles. Is that complexity worth carrying, or should bundles partition the constituent space (each constituent in at most one bundle)? Partitioning is simpler but constrains future curation.
  • Body-format convention. Should the bundle body have a mandatory structure (e.g. one ## heading per constituent, in includes: order)? Or freeform, as long as the operational content is preserved? Mandatory structure aids validator checks; freeform aids retention (the curator can rearrange for narrative flow).

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions