feat(packs): compound, ground, and enforce domain knowledge via Compound Packs - #1549
feat(packs): compound, ground, and enforce domain knowledge via Compound Packs#1549kieranklaassen wants to merge 26 commits into
Conversation
A repo can track domain knowledge under .compound-engineering/packs/<id>/ as markdown files with applies_when frontmatter. ce-plan discovers the packs at its research dispatch, the learnings-researcher searches them as extra roots (reading every pack file's frontmatter rather than grep-filtering), and pack-shaped constraints land in the plan with a (pack: <id>, <path>) citation. ce-brainstorm's grounding scout quotes matching pack files and the Product Contract cites them the same way. Zero packs leaves behavior unchanged. No protocol, provider skill, or config key — v0 of the CE Packs proposal. Includes the plan artifact, the CE Pack glossary entry, and a greppable contract test pinning the load-bearing tokens.
…e 25-file threshold
Supersedes the convention-folder v0 (PR #1546, closed): packs are now declared in a packs: config list (repo path, ~ path, or git URL pinned to a tag/sha/branch, with path:/tree-URL subfolder support), read from both config layers additively. Plan is implementation-ready; the v0 consumption machinery on this branch is the base.
Turns the packs: config lists (config.yaml + config.local.yaml, concatenated) into resolved pack roots: path and ref-pinned git sources, GitHub tree-URL sugar, pack selection, id overrides, duplicate-id errors, atomic cached clones under the CE scratch root, and non-interactive git so missing credentials warn instead of hanging. Byte-identical copies in ce-plan, ce-brainstorm, and ce-setup; 23 unit tests cover AE1-AE7 plus parser strictness and cache reuse.
…rounding ce-plan's Pack discovery and ce-brainstorm's grounding scout now run packs-resolve.py and consume its roots; the researcher's standalone fallback probes <root>/solutions/ only. Resolver errors and warnings surface to the user once and never enter the plan. Contract test pins the resolver invocation and the removed convention-folder glob.
Config template gains a commented packs: example (synced to the committed example copy); check-health gains a CE Packs section that runs the skill-local resolver, reports each resolved pack, flags config errors as project issues, and best-effort notes a cached branch ref behind upstream (silently skipped offline). Resolver roots now carry url/ref metadata for git sources so the health check never re-parses config. configuration.md documents the declared-sources shape.
Review findings applied: --end-of-options and a leading-dash guard stop config-supplied refs/urls reaching git as options; the cache root and cached checkouts get the peer-job-runner ownership/symlink check; zero-indent packs: lists parse; a single-pack git source is named from its URL tail instead of the sha cache key; git path: is contained to the checkout; the publish race and unexpected failures degrade to valid JSON; empty pack: selections warn; apostrophes no longer absorb comments; check-health probes interpreter execution (python3/python/py) and scopes the drift note to branch refs with a full non-interactive git env. Citations are now (pack: <id>, <path within the pack>) so git-sourced packs cite stably. 12 new regression tests.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cf606c274f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| warnings.append(f"{label}: cached checkout {dest} is a symlink or not owned by this user; refetching") | ||
| shutil.rmtree(dest, ignore_errors=True) |
There was a problem hiding this comment.
Reject cached checkout symlinks before returning
When the keyed cache path is a directory symlink, os.path.isdir(dest) succeeds and _owned_dir(dest) rejects it, but shutil.rmtree(..., ignore_errors=True) leaves the symlink in place; the later os.replace failure is swallowed and dest is returned. I reproduced the resolver emitting pack roots from the symlink target despite warning that it was refetching, so a corrupted or poisoned cache can silently substitute arbitrary pack content. Unlink or reject the entry and return only after re-verifying the published directory; all three resolver copies are affected.
AGENTS.md reference: AGENTS.md:L46-L46
Useful? React with 👍 / 👎.
| try: | ||
| names = sorted(os.listdir(directory)) | ||
| except OSError: | ||
| return False | ||
| return any(n.endswith(".md") and _is_knowledge_file(os.path.join(directory, n)) for n in names) |
There was a problem hiding this comment.
Recognize knowledge files below the pack root
When an immediate pack directory organizes all of its documents below a nested folder, such as packs/rails/rules/routing.md, this direct-child-only check reports that the source publishes no packs. That contradicts the documented contract that deeper nesting is pack content and makes such packs unusable unless they add an unrelated top-level Markdown file. Determine whether an immediate pack directory contains valid knowledge files recursively while continuing to treat only the source's immediate directories as pack IDs.
Useful? React with 👍 / 👎.
| PY="$(for c in python3 python py; do command -v "$c" >/dev/null 2>&1 && "$c" -c '' >/dev/null 2>&1 && { echo "$c"; break; }; done)"; [ -n "$PY" ] || { echo "no working Python 3 interpreter on PATH" >&2; exit 1; }; | ||
| "$PY" "$SKILL_DIR/scripts/packs-resolve.py" |
There was a problem hiding this comment.
Define the resolver-unavailable failure condition
When none of python3, python, or py runs, this required discovery command exits before producing the JSON assumed by the following paragraph, but neither this block nor its duplicate in ce-brainstorm states whether the caller should continue or block. Since packs are optional, different agents can now abort planning, silently omit packs, or improvise resolution. State at the calling layer that resolver unavailability is surfaced once and proceeds with zero pack roots, or explicitly define it as a blocker, and apply the same condition to both consumers.
AGENTS.md reference: AGENTS.md:L132-L136
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Stale comment
Agentic security review of the CE Packs resolver and config-source changes found one medium issue: pack enumeration follows child directory and knowledge-file symlinks without the realpath containment already applied to git
path:and repo-relativesource:. Other modules reported no qualifying API, config-injection, platform-pattern, or privacy findings.Sent by Cursor Security Agent: Security Reviewer
| def enumerate_packs(source_root: str, self_name: str | None = None) -> dict: | ||
| """Map published pack id -> dir. Immediate children only; self = single pack.""" | ||
| if _has_knowledge_files(source_root): | ||
| name = self_name or os.path.basename(os.path.abspath(source_root)) | ||
| return {name: source_root} | ||
| packs = {} | ||
| try: | ||
| children = sorted(os.listdir(source_root)) | ||
| except OSError: | ||
| return packs | ||
| for name in children: | ||
| child = os.path.join(source_root, name) | ||
| if os.path.isdir(child) and not name.startswith(".") and _has_knowledge_files(child): | ||
| packs[name] = child | ||
| return packs |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: MEDIUM
enumerate_packs treats immediate children as pack roots using os.path.isdir / open without rejecting symlink children or requiring realpath to stay under the already-contained source_root. Git path: and repo-relative source: already apply that containment, but published pack dirs are then appended as-is.
A git pack (or a later update of a branch-pinned pack) can ship a child directory or .md symlink whose target is outside the checkout. Downstream ce-plan / ce-brainstorm consumers then read every markdown file in each returned dir, so escaped host files can enter planning context and plan/dossier artifacts. The same resolver is copied to ce-brainstorm and ce-setup.
Impact: Untrusted pack content can steer the agent to read arbitrary files the local user can open, beyond the checkout the resolver claims to contain.
Reviewed by Cursor Security Reviewer for commit cf606c2. Configure here.
ce-code-review resolves the repo's packs before its institutional-
learnings pass (local-tree reviews only) and its researcher searches
pack roots with the same read-all-frontmatter and evidence-not-
instructions rules, so a diff violating a matching rule is flagged
with a (pack: <id>, <path within the pack>) citation. ce-doc-review
fills a {pack_constraints} template slot so document reviewers flag
plan text contradicting a matching rule. Resolver byte-copies extend
to both skills (five-way parity); plan gains U8-U10.
docs/skills/packs.md: create-your-first-pack walkthrough, applies_when writing guidance, the full entry reference, publishing a multi-pack repo, per-stage behavior, and a troubleshooting table. Linked from the README docs table, the docs index, the four consuming skill pages, and configuration.md (which stays the config-key reference).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a175f1887a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| **CE generic conditional local prompt assets** (`agent-native-reviewer`, `learnings-researcher`) are dispatched only when selected by Stage 3, through the same deterministic foreground batch dispatch as the structured personas. Read their prompt files from `references/personas/`, then give them the same review context bundle the personas receive: entry mode, any PR metadata gathered in Stage 1, intent summary, review base branch name when known, `BASE:` marker, file list, diff, and `UNTRACKED:` scope notes. Do not invoke them with a generic "review this" prompt. Their output is unstructured and synthesized separately in Stage 6. | ||
| **CE generic conditional local prompt assets** (`agent-native-reviewer`, `learnings-researcher`) are dispatched only when selected by Stage 3, through the same deterministic foreground batch dispatch as the structured personas. Read their prompt files from `references/personas/`, then give them the same review context bundle the personas receive: entry mode, any PR metadata gathered in Stage 1, intent summary, review base branch name when known, `BASE:` marker, file list, diff, and `UNTRACKED:` scope notes. | ||
|
|
||
| Before composing the `learnings-researcher` dispatch, resolve any CE Packs declared in config by running this skill's resolver as one command: |
There was a problem hiding this comment.
Select pack review before the learnings dispatch gate
In a local review where CE Packs apply but <root>/solutions/ is absent or has no plausible path/title match, Stage 3 never selects learnings-researcher (references/select-and-route.md:18), so this discovery block is never reached and the declared pack constraints are silently ignored. Make applicable resolved packs an independent selection condition at the routing layer rather than resolving them only after the solutions-based gate has passed.
AGENTS.md reference: AGENTS.md:L134-L136
Useful? React with 👍 / 👎.
| if os.path.isdir(child) and not name.startswith(".") and _has_knowledge_files(child): | ||
| packs[name] = child |
There was a problem hiding this comment.
Reject pack-directory symlinks that escape the checkout
When a configured git source publishes an immediate child that is a symlink to an existing external directory containing a valid knowledge file, os.path.isdir and _has_knowledge_files both follow it and the resolver emits that symlink as a pack root. I reproduced a cloned pack returning a root whose real path was outside the cached checkout; downstream agents are then instructed to read its Markdown, allowing a third-party pack to expose guessed local files to the model. Require every published pack directory's resolved path to remain within source_root before returning it.
Useful? React with 👍 / 👎.
| }) | ||
|
|
||
| describe("ce-setup check-health pack drift note", () => { | ||
| test("notes when a cached branch ref is behind upstream", async () => { |
There was a problem hiding this comment.
Give the two-run health test enough time
This test runs the complete subprocess-heavy health check twice but retains Bun's 5-second default timeout. In the targeted suite I ran, it was killed at 5008 ms before the second assertion, so the new test can fail CI independently of product behavior; set an appropriate timeout for this describe/test as required for legitimately multi-second suites.
AGENTS.md reference: AGENTS.md:L185-L185
Useful? React with 👍 / 👎.
| override = entry.get("id") | ||
| if override is not None: | ||
| if len(selected) != 1: | ||
| errors.append(f"{label}: `id:` override requires the entry to install exactly one pack") | ||
| return | ||
| selected = {str(override): next(iter(selected.values()))} |
There was a problem hiding this comment.
Enforce the documented pack ID format
When id: contains spaces, slashes, Unicode, or even an empty value, the resolver converts it to a string and publishes it unchanged despite docs/skills/configuration.md:72 defining pack IDs as kebab-case ASCII. I reproduced id: "bad id/with slash" returning that exact root ID, which produces ambiguous or malformed (pack: <id>, ...) citations instead of the promised loud configuration error. Validate both overrides and directory-derived IDs against the documented format before appending roots.
Useful? React with 👍 / 👎.
…t boundary A pack can carry arbitrary data the load script never discovers: only top-level .md files with title+applies_when are rules; any subdirectory (resources/, data/, docs/) is inert storage reachable solely through a rule that cites it. Examples show the layout, the rule-as-door pattern, and the one repo = pack + plugin shape (skills ride the plugin door; packs: cannot register skills). Plan defers pack-extras absorption. Health-test file gains the prescribed setDefaultTimeout against the documented under-load flake.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 38db680214
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if indent == 0 and not (in_packs and line.lstrip().startswith("-")): | ||
| # A new top-level key ends the packs block; a zero-indent list item | ||
| # (`- source: ...`) is still part of it -- YAML allows both styles. | ||
| in_packs = line.rstrip() in ("packs:", "packs: []") |
There was a problem hiding this comment.
Reject malformed values assigned to
packs
When a config contains packs: nope (or another unsupported inline value), this comparison simply leaves in_packs false, so the resolver returns empty roots with no warnings or errors and /ce-setup reports "No packs configured." This silently disables declared constraints despite the resolver's contract that unsupported packs shapes are loud configuration errors; distinguish an absent key or supported packs: [] from a present malformed value and append an error in all resolver copies.
Useful? React with 👍 / 👎.
|
|
||
| The caller may pass a **search-root list**: `<root>/solutions/` plus zero or more CE Packs, each as an `id` and an absolute directory, and optionally a list of pack files the origin document already cites. With no list, probe `<root>/solutions/` only — packs are declared in CE config and resolved by the caller, not rediscovered here. Every step below that names `<root>/solutions/` applies to each search root, except Step 2's subdirectory probe and Step 3b's critical-patterns read, which stay scoped to `<root>/solutions/`. Pack-specific rules: | ||
|
|
||
| - **A pack is small and prescriptive, so do not grep-filter it.** Skip the Step 3 pre-filter for a pack root: read the frontmatter of every markdown file in the pack (Step 4), then score with Step 5. Apply the Step 3 pre-filter to a pack only when it holds more than 25 files. The grep-first path for `<root>/solutions/` is unchanged. |
There was a problem hiding this comment.
Limit pack discovery to top-level Markdown
When a pack uses the documented resources/notes.md layout, "every markdown file in the pack" directs the researcher to read that nested file and then report it as malformed for lacking applies_when, although docs/skills/packs.md:156-168 promises that every subdirectory is inert, never scanned, and never warned about. Define pack candidates as top-level .md files only in this prompt and the corresponding code-review/brainstorm consumer prompts so the runtime behavior actually preserves the resource boundary.
AGENTS.md reference: AGENTS.md:L108-L110
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: be57400736
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if not IS_WINDOWS and not _owned_dir(path): | ||
| return False | ||
| return os.path.isdir(path) and os.access(path, os.W_OK) |
There was a problem hiding this comment.
Repair cache roots to mode 0700
When /tmp/compound-engineering-<uid> or its ce-packs child already exists as an owned, writable directory with mode 0755 or 0777, this check accepts it without repairing its permissions. Cached private pack checkouts can therefore be exposed to other local users, and a world-writable root also lets them plant entries in the cache namespace; chmod each managed root to 0700 and verify the resulting mode before using it.
AGENTS.md reference: AGENTS.md:L46-L46
Useful? React with 👍 / 👎.
| Before dispatching, resolve any CE Packs declared in config by running this skill's resolver: | ||
|
|
||
| ```bash | ||
| SKILL_DIR="<absolute path of the directory containing the SKILL.md you just read>"; | ||
| PY="$(for c in python3 python py; do command -v "$c" >/dev/null 2>&1 && "$c" -c '' >/dev/null 2>&1 && { echo "$c"; break; }; done)"; [ -n "$PY" ] || { echo "no working Python 3 interpreter on PATH" >&2; exit 1; }; | ||
| "$PY" "$SKILL_DIR/scripts/packs-resolve.py" |
There was a problem hiding this comment.
Move pack discovery ahead of brainstorm bypasses
When a software brainstorm is Lightweight, or its opening already has clear requirements, phase-0.md:47-59 skips Phase 1.1, so this resolver block never runs and declared prescriptive packs cannot shape the synthesis or Product Contract. Make pack resolution and tier-appropriate consumption a condition on every repo-backed software path that reaches brainstorm synthesis, rather than nesting it inside the Standard/Deep topic-scout path.
AGENTS.md reference: AGENTS.md:L134-L136
Useful? React with 👍 / 👎.
| def _main() -> int: | ||
| if shutil.which("git") is None: | ||
| print(json.dumps({"roots": [], "warnings": ["git binary not found; packs unavailable"], "errors": []})) | ||
| return 0 |
There was a problem hiding this comment.
Preserve path packs when Git is unavailable
When git is absent but the config contains only repo-relative, home, or absolute path sources, this early return discards those live sources too and reports all packs unavailable. The design in docs/plans/2026-08-26-001-feat-ce-packs-config-sources-plan.md:164 limits missing-Git degradation to git entries, so resolver startup must still locate the project and process path entries while warning only for git-backed entries.
Useful? React with 👍 / 👎.
Capture now closes the loop: the resolver runs before Phase 1 dispatch and the Related Docs Finder records pack_overlap, so an insight a pack rule already prescribes is recognized (interactive: refine the rule, capture the repo-specific nuance with a citation, or skip; non-interactive: Documentation skipped with the citation) instead of re-captured. Interactive Full mode can route a prescriptive, cross-repo capture into a writable declared pack — or scaffold one plus its config entry — with the learning-to-rule rewrite; git-sourced packs render as upstream-manual and are never written. Write boundary names the two consented writes. Sixth resolver copy, parity-gated. Also renames the feature to Compound Packs (docs, prose, glossary, health-check header; config key, citation marker, and script name unchanged) and recommends compound-packs/ as the in-repo folder.
Dogfooding in a clean repo showed the R8 skip-report only existed in researcher prose — a pack author validating with check-health never saw that a file was inert. The resolver now warns per skipped top-level .md in each installed pack, so planning, review, and the health check all surface it. Regression test added; six copies synced.
…e ambiguities Twelve-scenario matrix executed through the product's real UX (resolver CLI, shipped skill prose via fresh agents, check-health, docs walkthrough) in a clean repo: 11 Pass, 1 Fixed (silent skip, 1a18a67). Prose pinned per findings: pack rule reads are top-level-only with subdirectories/non-md as assets, scout file:line pointers are pack-relative, pack_overlap rule id is the filename stem.
# Conflicts: # README.md # skills/guides/packs.md




Summary
A repo can teach the whole Compound Engineering pipeline its own domain rules by declaring them: a
packs:list in CE config names each knowledge source — a repo-relative folder, a~/absolute path, or a git URL pinned to a tag, sha, or branch (withpath:subfolder scoping; pasted GitHub…/tree/<ref>/<sub>URLs work as-is) — and selects one pack, several, or everything the source publishes. Compound Packs then flow through every stage: planning grounds in matching rules (ce-brainstorm,ce-plan), review enforces them (ce-code-reviewflags violating diffs,ce-doc-reviewflags contradicting plan text), and capture recognizes them —/ce-compounddetects when an insight is already prescribed by a pack rule instead of re-capturing it, and can route a prescriptive, cross-repo capture straight into a writable pack (or scaffold one) with the learning-to-rule rewrite. Every influence is cited:(pack: <id>, <path within the pack>).The loop this closes: solve → capture → promote → declare → ground → enforce → recognize.
config.local.yamladds personal packs on top of the team list without replacing it. With nopacks:key, behavior is byte-identical to today. Packs add zero entries to the skill roster — they are knowledge ingested into existing steps, not new skills. A pack-bearing plugin is just a git URL; a marketplace is just a catalog of URLs. Full guide with authoring examples:docs/skills/packs.md. Plan:docs/plans/2026-08-26-001-feat-ce-packs-config-sources-plan.md. Supersedes the closed convention-folder attempt (#1546).Design decisions
title+ situationalapplies_whenfrontmatter; matching is semantic per consuming stage, so stage-scoping is phrasing, not a schema field. A pack can carry arbitrary in-pack resources (subdirectories are invisible to discovery, reachable only through a rule that cites them) — big data rides along at zero context cost.peer-job-runner.py). Strict YAML-subset parser with loud per-line errors, per-kind ref rules, tree-URL sugar, atomic ownership-checked caching under the CE scratch root, non-interactive git, JSON-always output.{pack_constraints}template slot; compound's Related Docs Finder recordspack_overlapand the assembly step gates the pack-covered and destination-routing decisions (interactive-only writes; non-interactive stays deterministic; git-sourced packs never written)./ce-setup's Compound Packs health section.--end-of-options+ leading-dash guard, cache owner/symlink verification on create and reuse,path:containment, bounded non-interactive git, no submodule recursion, JSON-always failure mode, evidence-not-instructions at every consumer.Validation
bun run test: 3,690 pass, 0 fail — 34 resolver cases (AE1–AE7, parser strictness, cache/ownership/option-injection, sha refs, tilde, CRLF, zero-indent, six-way parity), 3check-healthfixtures, and contract guards for every consuming stage including compound.release:validate+plugin:validategreen.applies_when, emitted the exact citation, and refused an injected instruction embedded in a pack body./api/invoicesfor a page's own data was flagged againstno-parallel-json-api.mdvia bothapplies_whenclauses, with the exact citation.packs:parse bug. Cross-model Codex pass attempted; skipped (peer CLI failed locally, no schema-shaped output).Known residuals
Informational: tree-URL sugar can't disambiguate branch names containing
/(warning points at explicitref:/path:); frontmatter blocks over 4KB disqualify a file silently; the drift note needs reachable remotes; kebab-case ids documented, not enforced. Deferred by design: provider protocols, auto-update, per-pack pinning within a source, conflict detection, the git-pack upstream-commit flow for compound routing.Post-Deploy Monitoring & Validation
No production/runtime infrastructure impact — bundled script, skill prose, tests, docs. Watch issue reports for: packs mentioned with no
packs:key (R11 regression), hangs attributable to git fetches (should be impossible: non-interactive env + timeout), or resolver tracebacks (should be impossible: JSON-always guard).Security Disclosure
Adds a script that fetches remote git repositories named in repo-committed config and parses user-authored YAML, consumed by planning, review, and capture. Mitigations shipped and tested:
--end-of-options+ leading-dash rejection (option injection), owner/symlink verification of the per-uid cache on create and reuse (cache poisoning), realpath containment for repo-relative sources andpath:subfolders (traversal), non-interactive git with bounded timeout (credential-prompt hangs), no submodule recursion, argv-only subprocesses, pack content treated as untrusted evidence by every consumer (injection refusal exercised in the spot-check). Remote-tree reviews skip local pack resolution. Compound's pack writes are interactive-consent-only and never touch git caches. Residual: cloning URLs the repo's tracked config names is by design — a maintainer decision, like declaring a dependency.Agent Disclosure
Claude Code · Fable 5