cc-guides

module
v0.1.54 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 2, 2026 License: MIT

README

cc-guides

Write the prose you own; import the guides you share. cc-guides composes AGENTS.md, CLAUDE.md, and shell scripts from local fragments and shared guides pulled in by reference, and its Guides workflow makes CI the only writer: edit a fragment, push, and the artifacts re-render and commit themselves.

Release CI

Get started

brew install yasyf/tap/cc-guides

A repo describes each generated file with a .claude/fragments/<target>/ directory: a layout.toml that composes local *.fragment.* prose with imports of shared guides, plus the local pieces. Each imported alias names its source in a [sources.<alias>] table. render fetches each import at a pinned commit, writes the target with a version-free GENERATED marker, and records every pin in a lock file:

$ cat .claude/fragments/AGENTS.md/intro.fragment.md
# Acme

House rules for agents.

$ cat .claude/fragments/AGENTS.md/layout.toml
fragments = [
  "intro",
  "cc-skills:ccx",
]

[sources.cc-skills]
source = "github:yasyf/cc-skills@main"

$ cc-guides render
rendered .claude/fragments/AGENTS.md -> AGENTS.md

$ head -6 AGENTS.md
<!-- GENERATED by cc-guides from .claude/fragments/AGENTS.md/ — do not edit; edit the fragments and push — CI renders. -->
# Acme

House rules for agents.

## Compact Context (ccx)

intro is your prose; cc-skills:ccx arrives verbatim from the shared guides in cc-skills, identical across every repo that imports it. Change the shared guide once and every repo re-renders. render also writes .claude/fragments/cc-guides.lock, which pins every source to the exact commit the artifacts were built against. That first render is the last one anyone runs by hand: wire in the Guides workflow below and CI owns the artifacts — edit a fragment, push, and the render commit follows.

Driving with an agent? Paste this:

Install cc-guides (`brew install yasyf/tap/cc-guides`). Give AGENTS.md a
.claude/fragments/AGENTS.md/ layout: a layout.toml composing my local
*.fragment.md prose with the shared guides I import. Run `cc-guides render`
once (it writes AGENTS.md and .claude/fragments/cc-guides.lock), commit, and
add the Guides shim workflow from the cc-guides README. From then on edit only
the fragments and push — CI re-renders and commits the artifacts; never run
render again. Reference: `cc-guides --help`.

Use cases

Let CI own the rendered artifacts

Rendered artifacts drift when humans and agents write them; the fix is to stop writing them. Every consumer carries the same never-changing shim, and the logic lives in this repo's reusable workflow — it evolves without another consumer edit:

name: Guides
# Stub — all logic lives in yasyf/cc-guides. Edit fragments; CI renders.
on:
  push: {branches: [main]}
  pull_request:
  repository_dispatch: {types: [cc-guides-render]}
  schedule: [{cron: "17 9 * * *"}]
  workflow_dispatch:
permissions: {contents: write}
jobs:
  guides:
    uses: yasyf/cc-guides/.github/workflows/guides.yml@main
    secrets: inherit

A push to main — and the daily cron, and either manual dispatch — runs ci-render: re-render everything, skip a lock diff that only moved source pins, otherwise commit chore: re-render guides (…) and push. A pull request writes nothing; pr-check fails it the moment it hand-edits a CI-owned file:

$ cc-guides pr-check main
AGENTS.md is CI-owned (rendered by cc-guides) — edit .claude/fragments/AGENTS.md/ instead
$ echo "exit: $?"
exit: 1

A fragment-only PR passes (cc-guides: no CI-owned artifact was hand-edited); the artifacts it implies land in the render commit right after merge. With a CC_GUIDES_DEPLOY_KEY repo secret (a deploy key with write access), the render commit pushes over SSH and still triggers your other workflows; without one, checkout falls back to GITHUB_TOKEN and the commit triggers nothing downstream. Adopting this outside a single-owner fleet? Pin the uses: ref to a release tag or commit SHA instead of @main, and pass the deploy key explicitly instead of secrets: inherit — the shim above trusts this repo's main branch with every secret the job can see.

check stays the local verifier: it re-composes each target in memory — pinned to the commits the lock records — and byte-compares against disk:

$ cc-guides check
OK	AGENTS.md

$ echo "agent slop" >> AGENTS.md
$ cc-guides check
STALE	AGENTS.md
$ echo "exit: $?"
exit: 1

OK, STALE, and MISSING go to stdout as TSV; exit 1 signals drift, 2 an invalid layout.

Keep one repo's spin on a shared guide

A repo needs its own version of a guide the rest of the fleet shares. Compose a local fragment in the slot where the import would sit — no import, no shadowing:

fragments = [
  "intro",
  "ccx",          # local ccx.fragment.md, not cc-skills:ccx
]

render reads ccx.fragment.md from the artifact dir instead of fetching the shared guide, and a layout that imports nothing locks no sources at all.

Adopt the lock on an older repo

A repo rendered by an older cc-guides (0.1.12 or earlier) carries a per-artifact banner and no .claude/fragments/cc-guides.lock. check and the Guides workflow are lock-driven, so adopt the lock with one render against the current binary — it rewrites each artifact with a version-free marker and writes the lock:

$ cc-guides render
rendered .claude/fragments/AGENTS.md -> AGENTS.md

$ git add .claude/fragments/cc-guides.lock AGENTS.md

Commit the lock alongside the artifacts and add the Guides shim; from then on CI keeps both current.

Composition

An artifact dir is any directory under .claude/fragments/ that holds a layout.toml, and its path below that root is the target it renders. .claude/fragments/AGENTS.md/ renders AGENTS.md; a nested path renders a nested target. An optional target key names the artifact instead, which frees a repo from carrying a directory that shadows a file its tools read. .claude/fragments/gitignore/ with target = ".gitignore" renders the root .gitignore, and the tree holds only the fragment dir:

target = ".gitignore"

fragments = [
  "cc-skills:gitignore-base",
  "gitignore-local",
]

Renaming an existing artifact dir means adding the target key in the same move, and a full render enforces that. Renaming .claude/fragments/.mcp.json/ to .claude/fragments/mcp.json/ without the key would render mcp.json and orphan the real .mcp.json; the render refuses, because a locked artifact whose file is still on disk may not lose its dir. Removing an artifact for good is the same gesture minus the rename — delete its dir and its rendered file together, or keep the file and pass --prune.

The guards hold either way: a target stays inside the repo, carries a supported extension, and never lands back under .claude/fragments/. A target two artifact dirs share, an override colliding with another dir's path included, is refused in the preflight before anything is written. The generated marker and pr-check both name the dir that rendered the file, which under an override differs from the target's own path. The kind — Markdown, shell comment style, JSON, YAML, TOML, or gitignore — comes from the target extension. A JSON target deep-merges its pieces (arrays concatenate, objects merge) and carries no marker; the lock is its only drift record. A YAML target concatenates its pieces like Markdown (never a semantic merge, so load-bearing comments survive), carries a #-comment marker, and re-validates the composed document at render. A TOML target concatenates the same way — fragments are disjoint table sets, so comments and the marker survive — with one authoring rule: every fragment after the first must open with a table header, so a stray root-level key can't silently land inside the previous fragment's last table. The composed output re-validates at render, catching a slip like the same table defined in two fragments before it ever reaches a consumer. TOML lint tolerates {{token}} placeholders by neutralizing each distinct token to a distinct numeric literal; a token inside a typed scalar (a date like 2026-{{month}}-01) still neutralizes to a malformed value and gets flagged — context-free substitution cannot satisfy every typed position. A gitignore target concatenates its pattern fragments the same way and carries the same #-comment marker; it has no syntax to validate, so any pattern — a {{token}}-bearing one included — passes lint.

layout.toml is an ordered, heterogeneous fragments array, optionally preceded by a target override. Both are top-level keys and come before any [sources.*] table: a top-level key written after a table header nests inside that table, and the binary hard-errors on that shape instead of composing empty.

fragments = [
  "agents-md",                       # local: agents-md.fragment.md in this dir
  "cc-skills:ccx",                   # import: the ccx guide from cc-skills
  { use = "cc-skills:install-binary-latest", args = { binary = "slop-cop", plugin = "slop-cop", repo = "yasyf/slop-cop", brew = "yasyf/tap/slop-cop" } },
]

[sources.cc-skills]                  # required: every imported alias declares its source
source = "github:yasyf/cc-skills@main"

A string entry is a local <name>.fragment.<ext> or an alias:name import; an inline table adds args that fill {{token}} placeholders in the imported body. Every imported alias needs its own [sources.<alias>] table — there is no baked-in default. A source spec takes one of two forms: the manifest form github:<owner>/<repo>[@<ref>], which follows the target repo's cc-guides.toml to its guides dir, or the explicit-path form github:<owner>/<repo>//<path>[@<ref>], which points straight at a subtree. Pieces join with one blank line between them, LF only, one trailing newline. Prose is never token-substituted, so ${{ github.sha }} and {{VAR}} survive verbatim.

Commands

One invocation per surface; run cc-guides <command> --help for the full flag list.

Command What it does
render [paths…] Compose each artifact dir to its target and write the lock. No paths: discover every layout under the repo. A full render refuses to drop a locked artifact whose file is still on disk; --prune allows it.
check [paths…] Re-compose in memory, pinned to the lock's commits, and byte-compare. TSV OK/STALE/MISSING, plus ORPHANED for a locked target no dir renders; exit 1 on drift, 2 on invalid input.
ci-render Render everything, skip a lock diff that only moved source pins, otherwise commit chore: re-render guides (…) and push, retrying a rejected push. The render job's engine.
pr-check <base> Exit 1 when <base>...HEAD touches a lock-listed artifact or the lock itself, naming the artifact dir that renders it — the real dir, target override included; a repo with no lock owns nothing and passes.
lint <dir> Check a shared-guides directory for purity: LF, one trailing newline, kind, shell shebang.
list List each artifact dir and the fragments it composes.
cat <ref> Print a fragment body: an alias:name import or a local piece by name.

--source alias=<spec> overrides where an import resolves — a github: spec or a local directory for development. --version prints the cc-guides version, which render stamps into the lock.

How it fits together

cc-guides ships three things: this binary, the reusable Guides workflow every consumer shims onto, and an install action. The content lives in its consumers — cc-skills is the reference home for the shared guides, and each repo carries its own layouts. An import resolves to an immutable commit through git ls-remote, fetches that tree from codeload, and caches it under the user cache dir; every artifact in a run pins the same sha, recorded in .claude/fragments/cc-guides.lock. check reads those pins from the lock, so it reproduces an artifact offline once the cache is warm and never false-fails across binary versions. Build, test, and lint conventions live in AGENTS.md.

task test   # go test -race ./...
task ci     # vet, lint, test, build

Licensed under MIT.

Directories

Path Synopsis
cmd
cc-guides command
Command cc-guides: Canonical agent guides as a shipped Go binary — render AGENTS.md, CLAUDE.md, and shell artifacts from embedded, versioned fragments
Command cc-guides: Canonical agent guides as a shipped Go binary — render AGENTS.md, CLAUDE.md, and shell artifacts from embedded, versioned fragments
Package guide is a generic, kind-aware composer for artifacts built from local prose fragments and imported shared fragments.
Package guide is a generic, kind-aware composer for artifacts built from local prose fragments and imported shared fragments.
internal
cli
Package cli builds the cobra command tree and owns exit-code mapping.
Package cli builds the cobra command tree and owns exit-code mapping.
log
Package log configures the process-wide structured logger.
Package log configures the process-wide structured logger.
tomlstr
Package tomlstr encodes Go strings as TOML basic strings.
Package tomlstr encodes Go strings as TOML basic strings.
version
Package version exposes the build version.
Package version exposes the build version.
Package layout parses and validates a `.claude/fragments/<target>/layout.toml` file — the per-repo config surface that composes an ordered list of local fragments and imports of shared fragments from another repo.
Package layout parses and validates a `.claude/fragments/<target>/layout.toml` file — the per-repo config surface that composes an ordered list of local fragments and imports of shared fragments from another repo.
Package lockfile reads and writes .claude/fragments/cc-guides.lock — the repo-level provenance record: the render version and one commit pin per source alias.
Package lockfile reads and writes .claude/fragments/cc-guides.lock — the repo-level provenance record: the render version and one commit pin per source alias.
Package source resolves shared-fragment imports: it parses a `github:<owner>/<repo>//<path>[@<ref>]` spec, resolves a ref to an immutable commit sha (shelling out to `git ls-remote`), fetches that commit's tree as a codeload tarball, and caches the extracted subpath under the user cache dir.
Package source resolves shared-fragment imports: it parses a `github:<owner>/<repo>//<path>[@<ref>]` spec, resolves a ref to an immutable commit sha (shelling out to `git ls-remote`), fetches that commit's tree as a codeload tarball, and caches the extracted subpath under the user cache dir.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL