Repository navigation
feat(packs): load shared writing rules through Compound Packs - #3
Open
kieranklaassen wants to merge 26 commits into
Open
kieranklaassen wants to merge 26 commits into
kieranklaassen wants to merge 26 commits into
Conversation
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
marked this pull request as ready for review
September 10, 2026 16:33
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-draftgrounds in the rules that match,cw-dev-edit/cw-line-editrevise against them,cw-voice-check/cw-final-pass/cw-panelflag a draft that contradicts one, andcw-saverecognizes a lesson a pack already prescribes instead of writing a duplicate intoVOICE.mdorSTYLE.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+ situationalapplies_when), the same layout rule (top-level.mdfiles are rules,README.mdis 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 nopacks: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
<home>/.compound-writing/config.yaml+config.local.yaml, anchored to the writing home rather than a git root.compound-engineering/was rejected: engineering rules firing during a line edit are noise, and a repo holding both plugins would have to accept both.cw-packscreatesconfig.yamlfrom its template when it declares or scaffolds a pack;cw-setup-project --with-packscreates it with a new home (opt-in, so the default four-surface scaffold is unchanged);.compound-writing/config.example.yamlat 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..compound-writing/governs; a checkout is only the fallback--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 aGIT_CEILING_DIRECTORIESentry, 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.skills/cw-packs/scripts/packs-resolve.py../../references/,<plugin-root>/skills/cw-setup-project/scripts/...), so CE's byte-identical copies would add a parity rule without a packaging need.VOICE.md/STYLE.mdand aboveexamples/cw-saveis the capture hookcw-packswhen 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-compoundcommand alias;cr:compoundnot createdcr:compoundas a CE-compatible entry point.ARCHITECTURE.mdrequirescw-<name>for every user-invokable skill and command, and a colon is not a valid skill or command name.commands/cw-compound.mdroutes tocw-saveand adds no behavior. It is a Claude Code command: the Codex manifest packagesskills/only, so Codex users invokecw-save; the repo has no Cursor manifest.tests/test_packaging.pyrejects the colon form.release/allowlist.jsonnow names every skill and command allowed in the public package (cw-packsandcw-compoundincluded) andtests/test_packaging.pyholdsskills/andcommands/to it in both directions.create_project.pyanddefaults/project-template/still create exactlyVOICE.md,STYLE.md,examples/,drafts/; the packs config is opt-in.cw-packsis 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:
*.mdblocked the resolver forever; only regular files are opened now.config.yamlaborted the run; it is now that layer's error andconfig.local.yamlstill loads.id:was unvalidated (empty,../../etc,-rfall published); ids must be one plain path segment.cw-packsverifies by the presence of thepack <id>line.cw-save: explicit requests skipped the pack preview; two writable packs had no selection rule; "non-interactive" was undefined.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 indocs/residual-review-findings/cursor-compound-packs-7c77.md.Validation
.github/workflows/ci.yml, new): on pull requests and pushes tomain, runspython3 -m unittest discover -s tests -vandclaude plugin validate --strictagainst 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, 4create_project.py, 12 prose-contract). Resolver tests drive the script through a subprocess against temp homes and local tagged git repos: home discovery (config,.gitdir, worktree.gitfile, nested config, nested checkout, ceiling directory,--homeon 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 forcw-names with onlyname/descriptionfrontmatter, the allowlist, the config example, and the four-surface template. Prose-contract tests pin thecw-save/cw-packs/contract sentences the testers found missing.{"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-optionsplus 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 andpath: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 validatesid:. Pack text is treated as evidence at every consumer;cw-panelhands 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_whennames 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 ruleearn-the-ending.mdmakescw-final-passrefuseReadyon a recap ending and cite the file, and makescw-savedecline to write "never end on a recap" intoSTYLE.mdbecause 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.