Skip to content

feat(packs): load shared writing rules through Compound Packs - #3

Open
kieranklaassen wants to merge 26 commits into
mainfrom
cursor/compound-packs-7c77
Open

kieranklaassen wants to merge 26 commits into
mainfrom
cursor/compound-packs-7c77

Conversation

@kieranklaassen

@kieranklaassen kieranklaassen commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A writing home can now declare Compound Packs: folders of prescriptive writing rules that a publication, a team, or one writer with several homes shares, and that every step loads at the moment it matters. cw-draft grounds in the rules that match, cw-dev-edit/cw-line-edit revise against them, cw-voice-check/cw-final-pass/cw-panel flag a draft that contradicts one, and cw-save recognizes a lesson a pack already prescribes instead of writing a duplicate into VOICE.md or STYLE.md. Every influence is cited (pack: <id>, <file>). Before this, standards shared beyond one home could only be copied by hand or baked into a skill.

The design mirrors compound-engineering-plugin's packs (#1549, hardened in #1661): the same rule shape (title + situational applies_when), the same layout rule (top-level .md files are rules, README.md is the description, subfolders are storage), the same entry fields, the same guards, and the same evidence-not-instructions posture. One pack repository can therefore serve both plugins; a cross-compatibility run confirmed identical ids, dirs, warnings, and citations from a shared repo under both resolvers. With no packs: key, every skill behaves exactly as before.

Plan: docs/plans/2026-09-10-001-feat-compound-packs-plan.md. Guide: references/packs.md.

Design decisions

Decision Why
Config in <home>/.compound-writing/config.yaml + config.local.yaml, anchored to the writing home rather than a git root A writing home is portable and often not a repository. Sharing .compound-engineering/ was rejected: engineering rules firing during a line edit are noise, and a repo holding both plugins would have to accept both.
Nobody hand-writes the config cw-packs creates config.yaml from its template when it declares or scaffolds a pack; cw-setup-project --with-packs creates it with a new home (opt-in, so the default four-surface scaffold is unchanged); .compound-writing/config.example.yaml at the plugin root is the same template, byte-identical by test, as CE ships .compound-engineering/config.example.yaml. The guide carries a complete copy-pasteable example. CE ships no example packs (its dogfood packs live in a private repo), so none are added here.
The nearest .compound-writing/ governs; a checkout is only the fallback Walk up from --home (a draft or folder) or the working directory to the nearest config; when none exists, the innermost checkout (first .git) is the home, so a plain repo reads its root config. A manuscript repository nested inside a home inherits the home's packs and can override them with its own config. The walk never crosses a filesystem boundary or a GIT_CEILING_DIRECTORIES entry, does not consult git configuration, and skips directories the user does not own. CE reads only a repo root; this is the writing-home analogue.
One resolver copy at skills/cw-packs/scripts/packs-resolve.py This repo's skills already reference siblings (../../references/, <plugin-root>/skills/cw-setup-project/scripts/...), so CE's byte-identical copies would add a parity rule without a packaging need.
Packs enter through the context contract as layer 5, below the home's own VOICE.md/STYLE.md and above examples/ The contract is the seam every voice-sensitive skill already reads; each step adds one paragraph on what it does with a match rather than restating the mechanism. The home's own guides win on a direct conflict and the conflict is named, never merged.
cw-save is the capture hook It resolves packs first, reports a covered lesson with its citation, and offers a writable (path-sourced) pack for a standing rule bigger than one home, scaffolding one through cw-packs when none exists. Every pack write is shown in full (path, frontmatter, body) and waits for approval, even on an explicit request; with two writable packs it asks; a non-interactive run never writes into a pack. Git-sourced packs are read-only caches.
cw-compound command alias; cr:compound not created The request proposed cr:compound as a CE-compatible entry point. ARCHITECTURE.md requires cw-<name> for every user-invokable skill and command, and a colon is not a valid skill or command name. commands/cw-compound.md routes to cw-save and adds no behavior. It is a Claude Code command: the Codex manifest packages skills/ only, so Codex users invoke cw-save; the repo has no Cursor manifest. tests/test_packaging.py rejects the colon form.
The public allowlist is now a file README and ARCHITECTURE have said since 2.2.0 that public releases come from "an explicit generic-skill allowlist", but no such file existed in this repo or the private source snapshot. release/allowlist.json now names every skill and command allowed in the public package (cw-packs and cw-compound included) and tests/test_packaging.py holds skills/ and commands/ to it in both directions.
Writing-home scaffold unchanged create_project.py and defaults/project-template/ still create exactly VOICE.md, STYLE.md, examples/, drafts/; the packs config is opt-in.

cw-packs is the one new skill: declare a source, scaffold <home>/compound-packs/<id>/ from a template after a preview that names the resolved home and config path, and render the resolver's JSON as a health report.

Tester round

Four independent test matrices (resolution, CE cross-compatibility, save/alias, writing flow; 276 cases) found no blockers. Fixed here, each with a regression test:

  • Two-hop symlink escape (security, medium): a directory link into a sibling folder inside the source hid a second link beneath it that left the source. Escape checks now follow in-boundary directory links, each real directory once.
  • A FIFO named *.md blocked the resolver forever; only regular files are opened now.
  • An unreadable config.yaml aborted the run; it is now that layer's error and config.local.yaml still loads.
  • id: was unvalidated (empty, ../../etc, -rf all published); ids must be one plain path segment.
  • A deleted working directory produced a resolver-failure error instead of CE's no-home warning.
  • A README folder whose only rules sat one level down published under the subfolder's name with no signal; it still publishes (CE-identical) and now says so.
  • An entry indented under another top-level key was dropped silently; it is a loud error, and cw-packs verifies by the presence of the pack <id> line.
  • Nested-checkout semantics decided and documented (see the design table).
  • cw-save: explicit requests skipped the pack preview; two writable packs had no selection rule; "non-interactive" was undefined.
  • Cheap nits: packs: [] append kept valid, YAML-unsafe titles quoted, short rule file names, voice-check double error report, handoffs no longer list unmatched rules, the guide's example fires while drafting, full-sha wording, and the runtime claim above.

The ~20 CE resolver cases the suite lacked are ported into tests/test_packs_resolve.py; all pass unchanged. Deferrals with one-line reasons are in docs/residual-review-findings/cursor-compound-packs-7c77.md.

Validation

  • CI (.github/workflows/ci.yml, new): on pull requests and pushes to main, runs python3 -m unittest discover -s tests -v and claude plugin validate --strict against the marketplace catalog and the plugin manifest, validator pinned to 2.1.220 (CE's pin) and cached. First run on this PR: green, 97 tests, both validations passed.
  • python3 -m unittest discover -s tests: 97 pass (70 resolver, 11 packaging incl. allowlist and config-example pins, 4 create_project.py, 12 prose-contract). Resolver tests drive the script through a subprocess against temp homes and local tagged git repos: home discovery (config, .git dir, worktree .git file, nested config, nested checkout, ceiling directory, --home on a draft), path and git sources, every documented error shape, symlink containment including the two-hop case, cache behavior, --declared-only, and the ported CE cases. Packaging tests check every skill and command for cw- names with only name/description frontmatter, the allowlist, the config example, and the four-surface template. Prose-contract tests pin the cw-save/cw-packs/contract sentences the testers found missing.
  • Resolver from this repo root prints {"roots": [], "warnings": [], "errors": [], "entries": 0, "home": "<repo>"}, so a checkout with no live config is silent.

Security disclosure

Adds a script that clones git repositories named in a writing home's config and walks user-authored pack directories, feeding their frontmatter to an agent. Mitigations carried over from CE and pinned by tests: --end-of-options plus a leading-dash guard on git arguments, owner-and-symlink-checked private cache under /tmp/compound-writing-<uid>/cw-packs, realpath containment for home-relative sources and path: subfolders, refusal to publish a pack whose tree links outside its source (now including links reached through an in-boundary directory link), non-interactive bounded git, JSON-always output. New in this port and reviewed: home discovery skips unowned directories, stops at filesystem and ceiling boundaries, and validates id:. Pack text is treated as evidence at every consumer; cw-panel hands rules to reviewers in a quoted block labeled as evidence, not instructions.

New concepts

Compound Pack. A pack is knowledge the agent must honor while working, as opposed to a skill, which is something the agent can do. It is a folder of Markdown rules; each rule's applies_when names the situations in which it fires ("judging whether a piece is ready to publish"), and the consuming step matches those conditions semantically against its own work, reading a rule's body only on a match. Why here rather than a skill: knowledge that must be invoked is knowledge that gets skipped, and skill text is obeyed while pack text is quoted and cited, which is the injection boundary this design depends on. In this PR, a house rule earn-the-ending.md makes cw-final-pass refuse Ready on a recap ending and cite the file, and makes cw-save decline to write "never end on a recap" into STYLE.md because the pack already says it. Do not use a pack for a workflow a writer runs on purpose (a cutting pass, a panel); those stay skills.


Compound Engineering

Open in Web Open in Cursor 

cursoragent and others added 10 commits September 10, 2026 16:03
Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
Port compound-engineering-plugin's packs-resolve.py to Compound Writing:
config lives in <home>/.compound-writing/, the home is found by walking up
from --home or the working directory (git top level as fallback), the cache
sits under /tmp/compound-writing-<uid>/cw-packs, and every output carries
the resolved home. Pack format, entry fields, error vocabulary, and the
security guards are unchanged so one pack repo serves both plugins.

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
…acks

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
…ks guide

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
…, and panel steps

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
…ompound alias

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
…s README, architecture, help, and commands

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
…urface scaffold

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
Home discovery now mirrors git's: stop at the first .compound-writing/ or
.git, never cross a filesystem boundary or a GIT_CEILING_DIRECTORIES entry,
and skip a candidate directory this user does not own, so a config planted
above the working tree cannot steer a run. Consumers stay silent about packs
when none are declared. cw-panel wraps pack rules as evidence for reviewers;
cw-packs previews the resolved home and config path before writing; the guide
limits in-rule references to relative paths inside the pack. Tests cover
path: escapes, id: with multi-pack selection, tree-URL conflicts, empty
selection, zero-indent items, BOM/CRLF config, a symlink planted at the cache
key, and the cw- naming check through check_surface. Includes the simplify
pass (cached git version probe).

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
@kieranklaassen
kieranklaassen marked this pull request as ready for review September 10, 2026 16:33
cursoragent and others added 16 commits September 10, 2026 17:55
- A directory link that stays inside the source is now followed (each real
  directory once) when checking for links that leave it, so a link into a
  sibling folder can no longer hide a second link beneath it. Found by the
  resolution test matrix (E7).
- Only regular files named *.md are opened: a FIFO or a directory named *.md
  no longer blocks or errors (A12b, A11b).
- An unreadable config layer is that layer's error; the other layer still
  loads and the home is still reported (B11).
- A deleted working directory is a no-home warning, not a resolver failure,
  matching CE (ce-compat E2).
- id: overrides are validated: non-empty, one path segment, no leading dash
  or control characters (E1). CE-style ids with spaces still pass.
- A folder holding a README, no top-level rule, and one rule-bearing
  subfolder still publishes the subfolder (CE-identical) but now says so and
  names the fix (X-1).
- A '- source:' line indented under some other top-level key is a loud error
  instead of a silent drop (F-3).

Each fix carries a regression test; test helpers gain git fixtures and an
in-process probe for later ported cases.

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
…t no longer hides the home

Home discovery now prefers the nearest .compound-writing/ at or above the
start path and treats .git only as the fallback: when no directory on the
way up holds a config, the innermost checkout is the home. A manuscript
repository nested inside a writing home therefore inherits the home's packs,
and a .compound-writing/ inside the checkout still overrides it. Ownership,
filesystem-boundary, and GIT_CEILING_DIRECTORIES limits are unchanged; an
unowned checkout is skipped with a warning instead of ending the search.
Resolves the C4 ambiguity from the resolution matrix; CE reads only a repo
root, so this is the writing-home analogue rather than a mirror.

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
Carries the cases from CE's tests/skills/ce-packs-resolver.test.ts that had
no counterpart here: block-style pack: lists, non-string path:, an injected
per-entry exception, BOM and >4 KB frontmatter, README-with-frontmatter,
nested-count depth and hidden dirs, lowercase README, no git binary on PATH,
branch cache reuse, a partial .part- cache dir, sha fetch fallback, a
single-pack git URL's tail as id, ~ against HOME, id: keeping git metadata,
apostrophe plus trailing comment, tree-URL regex groups, child-dir link
skipped with one warning, nested link refusing the pack while siblings
publish, an inside alias published, and _private_root_usable repairing 0700.
All pass unchanged against this resolver.

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
…ne non-interactive

A pack write is now always shown in full and waits for approval, even when
the request was explicit, because the applies_when list decides where the
rule fires in every home that declares the pack (F-1). With more than one
writable pack the skill names each with its rules and asks; it never picks
silently, and a non-interactive run never writes into a pack (F-2). A
non-interactive run is defined as a caller passing mode:non-interactive,
the token ce-compound takes and cw-compound forwards. Rule files take a
short kebab-case name for what they govern, an existing file is revised
rather than duplicated, and YAML-unsafe titles are quoted.

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
…, and states the home rule

The scaffold is done only when the health check prints a pack <id> line;
a missing line or an outside-the-block error means it is not (F-3). A live
'packs: []' key is replaced with 'packs:' before an item is appended so the
file stays valid YAML. The home paragraph matches the resolver: nearest
.compound-writing/, innermost checkout as fallback, nested checkouts
inherit, and a repository-hosted home without config scaffolds at the
repository root unless the writer names the home folder.

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
… matrix findings

Contract: consuming steps need only the Compound Packs section, not the
guide; name only matched rules in handoffs; the onboarding offer is the one
permitted pack mention without config. Voice check reports resolution
problems in the handoff only. Guide: the earn-the-ending example fires
while drafting too; file names are short slugs; YAML-unsafe titles are
quoted; readiness phrasing also reaches dev-edit; the declared-folder rule
explains why rules in a subfolder publish under the subfolder's name;
discovery is the nearest config with the innermost checkout as fallback and
does not consult git configuration; an unselected shared entry installs the
other plugin's packs too; the three anchor-naming messages differ from CE.

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
release/allowlist.json names every skill and command allowed in the public
Compound Writing package, including cw-packs and cw-compound. The README and
ARCHITECTURE.md have referred to an explicit allowlist since 2.2.0 but no
such file exists in this repository or in the private source snapshot; the
list now lives here and tests/test_packaging.py holds skills/ and commands/
to it in both directions, so adding to the public package is a deliberate
edit and a stale entry fails the suite.

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
Mirrors compound-engineering-plugin, which ships .compound-engineering/
config.example.yaml byte-identical to the template its setup skill copies:
.compound-writing/config.example.yaml now sits at the plugin root, the
packaging tests pin it to skills/cw-packs/references/config-template.yaml,
and *.local.yaml under .compound-writing/ is ignored. create_project.py
gains --with-packs, which adds .compound-writing/config.yaml from that
template without touching the default four-surface scaffold and never
overwrites an existing config; cw-setup-project, cw-packs, the guide, and
the README say that the writer never hand-writes the file. The guide gains
a complete copy-pasteable example config. CE ships no example packs (its
dogfood packs live in a private repository), so none are added here.

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
…d main

One job: python3 -m unittest discover -s tests, then claude plugin validate
--strict against the marketplace catalog and the plugin manifest with the
validator pinned to 2.1.220 (compound-engineering-plugin's pin) and cached
on that version.

Co-authored-by: Kieran Klaassen <kieranklaassen@users.noreply.github.com>
… against kate bench

Round 2 of the kate-bench optimize run tested three plugin-side
hypotheses against cw-line-edit unchanged, on the same rows with a paired
per-opportunity ledger: pack authority over the step's checklist (I2),
deterministic checks/ run before judgment (I3), and examples/ retrieved
per paragraph (I6). None passed the selection rule; all raised precision
and left recall of the editor's edits within one of sixty. The one gain
in the round was input, not instruction: the whole draft instead of
three paragraphs.

This branch carries the learnings: a docs/learnings note with the
numbers, two Lessons on cw-line-edit, and a measured paragraph in
references/packs.md. The arms stay, with tests, on
cursor/kate-bench-pack-integration-arms-cf0e.
…n-cf0e

docs: what the pack layer can and cannot do for a line edit, measured against kate bench
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.

2 participants