Repository navigation
The first release, v0.1.0 - #2
Open
jeffrey-aguilera wants to merge 108 commits into
Open
jeffrey-aguilera wants to merge 108 commits into
jeffrey-aguilera wants to merge 108 commits into
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
jeffrey-aguilera
marked this pull request as ready for review
October 24, 2026 05:02
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The npm backend's bats could not exec on a Linux runner; the aqua backend fetches the release archive and needs no node. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… release on it Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…exported variable Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A target is a harness that insists on files outside .kit/. kit install names them, the manifest records them, and update and check honor the same set. claude is the one target this release knows; kit target add and remove change the set later. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…op as a list Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…equisite gets its link Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved defects can corrupt update metadata, bypass validation, publish staged work unexpectedly, and remove references to unretained commits.
4 open issues
What changed in this PR
This PR delivers kit v0.1.0: reusable agent rules, issue-to-worktree tooling, installation and update commands, and release automation. It also configures this repository to use its own kit.
Changes:
- Adds the delivered rules, Git hooks, worktree helpers, and optional Claude target.
- Implements manifest-based installation, updates, and integrity checks.
- Adds CI, release packaging, pinned tools, and Bats coverage.
| File | Description |
|---|---|
| tests/release.bats | Tests archive layout and fresh installation. |
| tests/kit.bats | Tests installation, targets, updates, and checks. |
| tests/hooks.bats | Tests rebase protection and commit linting. |
| README.md | Documents adoption, setup, and releases. |
| mise.toml | Pins the development tools. |
| mise.lock | Records tool versions and platform downloads. |
| justfile | Adds the gate, refresh recipe, and kit module. |
| bin/kit | Implements installation, updates, targets, and validation. |
| AGENTS.md | Establishes repository-specific kit rules. |
| .kit/version | Sets the initial release version. |
| .kit/settings.json | Defines the delivered deny rules. |
| .kit/rules/yaml.md | Defines workflow conventions. |
| .kit/rules/markdown.md | Defines Markdown conventions. |
| .kit/rules/just.md | Defines justfile conventions. |
| .kit/rules/json.md | Defines JSON and shared-settings conventions. |
| .kit/rules/gitignore.md | Defines ignore-file conventions. |
| .kit/rules/bash.md | Defines Bash conventions. |
| .kit/manifest | Records installed targets and release digests. |
| .kit/kit.just | Exposes issue, tidy, and status recipes. |
| .kit/hooks/pre-rebase | Guards default-branch rebases. |
| .kit/hooks/pre-commit | Lints staged paths with available tools. |
| .kit/ci.yml | Delivers gate, integrity, and issue-link checks. |
| .kit/bash/git.sh | Implements worktree status and cleanup. |
| .kit/bash/gh.sh | Implements the issue-to-worktree flow. |
| .kit/bash/1st.sh | Provides shared helpers and module loading. |
| .kit/1st.md | Defines the agent workflow and rules index. |
| .gitignore | Excludes per-issue worktrees. |
| .github/workflows/release.yml | Validates tags and publishes release archives. |
| .github/workflows/kit-ci.yml | Installs the kit’s CI workflow locally. |
| .claude/settings.json | Installs the Claude deny rules. |
| .claude/rules/kit-yaml.md | Installs path-scoped YAML rules. |
| .claude/rules/kit-markdown.md | Installs path-scoped Markdown rules. |
| .claude/rules/kit-just.md | Installs path-scoped justfile rules. |
| .claude/rules/kit-json.md | Installs path-scoped JSON rules. |
| .claude/rules/kit-gitignore.md | Installs path-scoped ignore-file rules. |
| .claude/rules/kit-bash.md | Installs path-scoped Bash rules. |
Review effort: Balanced
…memory Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
kit install copies .kit/1st.md and .kit/rules/ and writes the manifest; the bash and the hooks stay in the installed release, reached through kit issue, kit tidy, kit status, and core.hooksPath, which kit setup wires per clone. .kit/kit.just is gone; a root justfile adds veneers, as this one does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e line Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s a merge for json The release root holds tree/, mirrored verbatim at a consumer's root; targets/<name>/, mirrored when the target is taken; and bash/, hooks/, and version, the machinery. bin/kit walks those directories and copies what it finds, a symlink as the file it points at, so the Claude mirrors are links into tree/.kit/rules/ and a new rule is one file and one link. A .json destination is merged by key with one jq definition. Notices live in the delivered files themselves. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… status Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…and kit rebase replace the hooks bin/kit resolves the release and execs bin/kit-<verb>.bash. The hooks are gone: kit lint is the body of a pre-commit hook, run by Lefthook through the delivered .kit/lefthook.yml, which a repository takes with extends; kit rebase refuses the default branch and otherwise rebases a worktree's branch onto origin/main and pushes it, and 1st.md names it as the spelling an agent uses, with git rebase added to the deny list. The human's desk is no longer bound by a hook. bash/kit.sh holds the layout and the merge rule the four install verbs share. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… it; one way into a bash module Lefthook moves to mise.desk.toml, read when MISE_ENV=desk, which a desk exports and a runner never sets; the Mac-written lock had no Linux entry for it and the runner has no use for it. lockfile_platforms makes the lock carry the runner's platform from now on. The loader's autoload stubs go: a verb says use <module>, and that is the one mechanism. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…hine's default Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… reads them; kit lint runs that hook The fragment names which linter runs over which paths, a repository's lefthook.yml extends it, and kit lint is lefthook run pre-commit, over the staged paths or with --all-files over every tracked path, which is also the gate's spelling. Lefthook is back in mise.toml, since a runner runs the same linters; nothing is desk-only today, and the README says where a desk-only tool would go. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… git defaults to master Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…es, tools called bare As enchiridion's justfile has it: mise bin-paths goes first on PATH when the file is parsed, so every recipe calls its tools bare and the pinned tool wins on a desk and on the runner alike, and a bash body is a [script] recipe under set script-interpreter := ['bash'] rather than a shebang recipe. The just rule says so. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ed error Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Region placement grows up: a destination that is a symlink keeps its link and the write lands in its target; a directory at a destination is refused by name; a region that opens and never closes is refused rather than treated as absent; two modules delivering a region to the same path are refused at render; markers follow the file's comment syntax by extension, and a kind of file the kit cannot mark is refused. Removing a region leaves no leading blank line. kit lint takes a named path relative to the directory the command was typed in and reads git's lists NUL-separated. kit gate writes the failed gate to the step summary and detects Justfile and .justfile too. The markdown rule's linter reads a delivered .kit/markdownlint.jsonc, so a fresh repository lints its delivered markdown clean, and this repository's own .markdownlint.jsonc goes. The GitHub provider parses the pull request number from the URL gh prints, and the kit-ci template refuses an unreleased manifest with no bin/kit. The YAML reader accepts keys with digits, underscores, and dashes. Tests shim gh and the step summary wherever the real ones could be reached, the vacuous inverted greps become real ones, and new cases cover each refusal above. Headers, docs, figures, the entry file, and the README say regions, settings, and the delivered check the same way the code does, and every prose line fits 120 columns. Closes nothing on its own; part of #5. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
just refresh ran kit update over the working tree and staged each delivered file whole, so a desk's unstaged text outside a region, or outside the kit's entries in a merged JSON file, was staged with the kit's change. It also took the index's release as its own base, so an entry the source dropped survived the merge. Now the sources and the copies are exported from the index, kit update runs over the export with HEAD's release as the base of every merge, the release the copies were last made from, and the export is staged back through a temporary index. The working tree then takes the same step, so a desk's copies agree with the index inside the kit's parts and its own text stays where it was. Proven by hand: an unstaged line after the AGENTS.md region stays unstaged, and a deny entry removed from the claude module's source leaves both the index and the working tree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The header comments on release.yml and retire-pre-releases.yml read as jargon. They now say, in sentences a reader new to the repository can follow, what event runs each workflow, what each step checks or produces, and why a release is never changed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A release tag is release/0.1.0 and a pre-release tag release/0.1.0-rc.1, so the version tags have a namespace of their own, and the glob that starts the release workflow, release/*, says what it is for. The part after release/ is the version: it is stamped into the packed kit, names the tarball kit-<version>.tar.gz, and names the release. The workflow, kit release verify, the fetch URL in release:fetch and in the delivered kit-ci job, the tests, and the README follow. A mise pin adds version_prefix = "release/", which is how mise maps a plain version onto the tag; curl, gh, and Homebrew need nothing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
kit release publish refuses a release tag anywhere but on the runner, naming the push that starts the release workflow, so a desk cannot publish a release by hand. A desk may still publish a pre-release, and kit release pack still makes the tarball anywhere. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The worktree that holds a pull request's branch was a line of its own after the issues it closes. It now ends the pull request's row, after the checks, so the lines under a pull request are the issues it closes and nothing else. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s regions are made that way A comment line that is `:generate:<command>:until:"<pattern>"`, or `:while:"<pattern>"`, and nothing else opens a region: the command runs from the repository root with the running kit's bin/ first on PATH, and its output replaces the lines after the directive up to the first line matching the pattern, which stays, or every line matching it. The command is one command and its arguments, with no shell operator, so what makes a region is always a verb, a recipe, a target, or a script in the tree. A directive inside a fenced code block is not live. kit generate expands the named files once, or every tracked and untracked-but-not-ignored file to a fixed point, at most twenty passes; --check writes nothing and names every stale file. A file is rewritten only on a clean expansion: a failing command, a region that never closes, a command outside its shape, a conflict marker, or an empty result leaves it as it was and fails the run. kit check runs the check over the whole repository, and the lefthook fragment over the staged paths. The kit's regions are made that way. Install writes the directive naming kit region <path>, the content, and the end marker; kit region prints what the active modules deliver at a path; update regenerates a region rather than merging it; a region there with other content is regenerated rather than refused; and remove takes the block out. This repository's own AGENTS.md, .gitignore, and .claude/CLAUDE.md carry the new form, which the refresh wrote. docs/generate.md is the page; the README, the architecture and modules pages, the entry file, the module pages, and the figure say region the same way. Six cases in tests/generate.bats cover the semantics and each refusal, and the region tests in tests/kit.bats follow. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… that nest, and bash command lines A region opens on a line that is kit:generate <name>, optionally with a backticked command, framed by nothing but punctuation, and closes on kit:end <name> framed the same way, so it is read without knowing the host's comment syntax and a line of prose is never one. The command is a bash command line with pipes; with none it is kit region, reading the name. Every unit is balanced on its own, the file and each command's output, and an output's regions expand in turn, depth first. while: is gone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… by kit region reading the name Install writes kit:generate <path> and kit:end <path> in the file's comment syntax, with no command, and kit region with no argument reads the region's name from KIT_GENERATE_REGION. A home region still spells its command, since the bare form cannot say --home. This repository's own three regions are rewritten by hand, since the old markers are no longer read. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ml as context A module's regions/<base>/<path> is rendered as templates/<base>/ is, before delivery, so kit region prints the rendered content: a top-level setting as @key@, a map's entry as @map.name@, and a bare @key@ meaning the entry under answers:, the module's defaults beneath. The rendered directory keeps the modules/<module>/<kind>/<base> shape so a rendered region still reads as one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…gion as a template Every mention of the old form becomes the new one, and no line of prose opens with the marker, since such a line is a directive or an error. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…by its output, and the bash rule's then From review: a markdown output with an odd number of fence lines wrote a file the next pass refused, so a fence open at the end of any unit is now refused naming its line; a nested command's failure named a line of the output as the file's, so it is named by the unit it is in; the duplicate-name refusal names the line of the expanded file; and every fence toggle takes the rule's then on its own line. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… row, and timed tests kit status takes --mine, --all, and --owner=<login>, given once or more; --all is the default. Every choice but --mine shows the author before the state, 👤 and the login, in a column of its own. The pull request document carries the author, which the GitHub provider reads in the one request it makes, and --json carries it too, the pull requests chosen the same way. The worktree on a pull request's row is named by its branch, the name the worktree's own block showed, not by its path relative to the current directory. just ci runs bats with --timing, so every test's time is in the log. #10 holds the numbers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lychee is pinned and runs in just ci over the README, AGENTS.md, the docs, the modules, and the installed copies: offline, so the gate stays deterministic, with fragments, so a section link no heading matches is a red run. A link to a web page is excluded offline, and the module pages point at GitHub on purpose, since a release ships without docs/. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…kit check holds it Where a format composes, the kit delivers a whole file under .kit/ and the repository's file takes it with one include line. A module now says so under expects:, a path, an extended regular expression some line must match, and a remedy. kit install and kit module add name each unmet one as left to do, kit check refuses until it is met, and the kit writes nothing into the file. The lefthook module is the first: it expects lefthook.yml to extend .kit/lefthook.yml, which the README asked for and nothing checked. The YAML subset gains a list of records, and a single-quoted value is taken literally, so a pattern keeps its backslashes. The module schema grows the key. docs/modules.md states the placement rule: an include where the format composes, a region where it has comments, a merge by key where it has neither, a copy only where the file is the kit's own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e every provider runs bin/forge-gitlab.sh answers every call of the seam over glab and GitLab's REST API, folded with jq in the provider so its logic is the kit's and a test can drive it. It is picked by an origin at gitlab.com, or by forge: gitlab for a self-hosted GitLab. A merge request is the change, its iid the number, its head pipeline's jobs the checks, its resolvable discussions the review threads; GitLab has no command that makes a branch for an issue, so link-branch makes <iid>-<slug> from the default branch, the name GitLab's issue page would give it. The seam grows two calls. noun prints the change's sigil and name, "#" and "pull request" or "!" and "merge request", and pr-url its browser URL, so kit status draws #3 or !3 in otherwise identical rows, and kit linked and kit issue name the change in the forge's words. kit linked's remedy is now a Closes #N line in the description, which both forges honor, in place of GitHub's Development panel. tests/forge-conformance.bats is one world seen through each forge's API: a routed fake stands in for gh and glab, applying gh's --jq so the GitHub provider's own filters are what is tested, and every call of both providers is held to the same answer. What only a real GitLab can prove, that its API answers as the fixtures do, is not proven here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… work items, and references that carry their tracker An issue lives in a tracker, and the tracker is not always the forge. bin/tracker.sh is the seam: a reference names its tracker by its prefix, #7 or a bare 7 for the forge's own issues and AB#4521 for an Azure DevOps work item, and .kit/settings.yml declares every tracker but the forge's own as a record, a prefix, a provider, and the provider's fields. Two providers: tracker-forge.sh, which hands each call to the forge seam, and tracker-ado.sh over az boards, closed in Closed, Done, and Removed, the caller being the identity az is signed in as. kit issue takes references. Each issue is assigned as its tracker knows the caller; the branch opens with the reference's key, 7-<slug> or ab4521-<slug>, made from the default branch when the forge cannot link one; the start commit's subject opens with the reference; and a primary the forge cannot link, like every secondary, gets a Closes <reference> line in the change's body. kit linked accepts a body's closing keyword naming an open item of a declared tracker beside the forge's own closing issues, and kit tidy reads a branch's issue back from its key in any tracker. tests/tracker.bats covers parsing, keys, body references, the ADO provider through a fake az, kit linked on a work item, and the settings check. docs/trackers.md is the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ecked by patterns or Open Policy Agent The toolchain seam, bin/toolchain.sh, asks every tool manager the same questions: install, outdated, update, path, and run. mise reads mise.toml and its lock; brew reads a Brewfile and moves only its formulae; docker builds an image from toolchain.Dockerfile, tagged by the file's digest, runs a command in it with the checkout at /work as the caller's user, and pins its FROM bases by digest. A repository declares its toolchains in .kit/settings.yml or the kit detects them from those files. kit toolchain is the verb, kit gate runs the gate through the toolchain, inside the image where docker is declared, and the delivered workflow installs with kit toolchain install and appends kit toolchain path to the runner's PATH, its mise action kept only to put the mise binary there. All three ran against the real tools on a desk. The policy seam, bin/policy.sh, gathers one facts document, the branch, the commits past the fork, the change and its title, and the patterns with the kit's defaults filled in, and an engine names the refusals. patterns, the default, matches in bash; opa evaluates the Rego under .kit/policy, and the policy-opa module delivers the same rules in Rego, so tests/policy.bats holds both engines to identical refusals. kit policy is the verb, and kit ci runs it as its fourth check; the workflow fetches full history for it. The defaults are the loop's names: a branch <key>-<slug>, a commit subject opening with its issue's reference, a one-line title. This repository turns the commit-subject rule off until #1 merges, since its early commits predate the rule. The forge seam grows pr-title. ere:quote in the loader escapes a pattern in bash, in place of three sed escapes BSD sed refused. The YAML reader unquotes single-quoted scalars and map values literally. opa is pinned for the suite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ths, and the forge asked first The first line says what the fetch moved: the commits new on the default branch, the branches updated, new, and pruned, and the tags new. Each checkout lists its uncommitted paths, with git's status, before its commits. The forge's query starts in the background before the fetch, so the checkouts are drawn while it answers, and its warnings are held for the pull requests' place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e documentation in plain English A rule now carries templates, one per variant of the file it governs, under .kit/templates/<rule>/<variant>. kit new file <path> makes a file from the most specific rule's template, the path taken from the repository root, its holes filled from the path and from flags, and refuses a hole left empty or a file already there. kit lint holds every governed file to the shape of one of its rule's templates, naming the line where a file leaves it; a template, a module's region, and a path .gitattributes marks generated are held to none. The check found four modules with shellcheck directives above their banner, a fixture without its closing rule, and two mise lock files, now marked generated. kit update needs the base release only to merge, by key or over an edited file, and finds every such path before writing anything, so an update is whole or not at all. kit generate passes over templates. The documentation, the comments, and the rules are rewritten in plain English; the code is unchanged but for kit's usage text and one step message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ENTS.md Every row of kit status separates its cells with " · ": the checkout's label from its counts, a pull request's title, author, state, checks, comments, and branch. A pull request's state and its age share a cell, and a failed checks cell reads "<failing> failing <passed> / <total> <took>". AGENTS.md opens with its heading, above the kit's region. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.



The first release of the kit, as #1 describes it, with the review's changes, the module architecture #4 describes, and the portable shape #5 describes: the repository carries the text an agent reads, the release carries the machinery as modules, a file's rules are discovered through
kit contextrather than copied into any harness, and this repository uses its own kit.What changed
.kit/1st.md, the entry: the integrity rule, the root checkout as the human's desk, the five-step loop, plans as issue prose, the ten-line worker contract, and the table of rules by file type. 78 lines..kit/rules/{markdown,bash,just,yaml,json,gitignore}.md: one rule file per kind, rules only.kit issue,kit tidy,kit status, andkit rebase, spelled that way and no other: no justfile veneers, here or in a consumer.rebaserefuses the root checkout, the human's desk, whatever branch it has, and the default branch anywhere, and otherwise rebases a worktree's branch ontoorigin/mainand pushes it with--force-with-lease;1st.mdnames these as the spellings an agent uses, andgit rebasejoins the deny list.issuelinks the branch under Development withgh issue develop, makesworktrees/<N>-<slug>, pushes a start commit, opens a draft pull request, assigns the issues, and sweeps uncommitted work out of the root checkout into the worktree, fast-forwarding the root only when it is on the default branch. It never switches the checkout it runs from.No git hooks of the kit's own, and nothing that binds the human at the desk.
.kit/lefthook.ymldeclares which linter runs over which paths before a commit, a repository'slefthook.ymltakes it withextendsand adds its own, andkit lintislefthook run pre-commit, over the staged paths or with--all-filesover every tracked path, which is also the gate's spelling.Modules (Modules: an extensible kit, with context discovery in place of harness mirrors #4). Everything the release delivers is a module under
modules/<name>/:module.yml(description,default,depends,answers),files/<base>/mirrored at the base's root,templates/<base>/rendered from the consumer's.kit/answerswith@key@keys, anddocs/. Two bases:repo, the repository root, recorded in.kit/manifest, andhome, the user's~, recorded in~/.kit/manifest, the desk's own record, whichkit checkreads where it exists and reports absent where it does not. A module naming another base is refused with the bases listed. No module delivers tohomeyet. One small YAML reader,bin/yaml.sh, servesmodule.ymland the rules' frontmatter alike.modules/module.schema.jsonis the schema formodule.yml, validated in the gate withajvand held to bykit install.docs/architecture.mddraws the whole, in three SVG figures.kit install [<module>…]activates the defaults and the named ones with their dependencies;kit module list | add | removechanges the set, andremoverefuses a module another active one depends on. The manifest records the set on amodulesline. This release carrieskit,lefthook,ci(a template, withci_runneransweredubuntu-latestby default), andclaude.Context (Modules: an extensible kit, with context discovery in place of harness mirrors #4). A rule lives once, in
.kit/rules/<kind>.md, and is copied nowhere.kit context <path>…names the rules governing each path, by thepaths:globs and thecontent:first-line patterns in a rule's frontmatter, and appends every ask to.git/kit/context.log. Theclaudemodule places.claude/CLAUDE.mdimportingAGENTS.md, since Claude Code readsAGENTS.mdonly as a fallback when noCLAUDE.mdis present, soAGENTS.mdopens with the prose sentence every agent reads and no@import; and it merges into.claude/settings.jsonthe deny list, the spellings.kit/1st.mdforbids and aPreToolUsehook onWrite,Edit, andMultiEditthat runskit context --claude, which answers in Claude Code's hook protocol with the rules' paths, the same answer the plain call prints, so reading a rule stays an act the agent takes..claude/rules/kit-<kind>.mdis gone. Cursor, Copilot, and Gemini (Targets for Cursor, Copilot, and Gemini #3) become adapters on this mechanism and stay open..github/workflows/kit-ci.yml, thecimodule's template: oneCIjob whose steps are one kit command each,kit gate(the root justfile'scirecipe, refused when there is none, since a gate that cannot run clears nothing),kit check, andkit linked(the linked-open-issue refusal), on the runner.kit/answersnames. The two new verbs are what a desk can run too.bin/kit:install,update,check,module,context,gate,linked,version.updateretires a path the new release no longer delivers, removing it when it is still the previous release's content, taking its entries out of a merged.jsonwhen the base is at hand, and leaving an edited one with a warning. The manifest records the release's digest per path, so an edited file reads as edited at every later update and is three-way merged with the previous release as the base.KIT_BASEnames the base on a machine without mise..github/workflows/release.yml: only arelease/*tag starts a run. A release,release/<major>.<minor>.<patch>, is tagged onmain(any commit on it, not only its head); a pre-release, the same with-<suffix>, is tagged on a commit that is not onmainand is published as a GitHub pre-release. The repository carries no version file: the workflow stamps the part afterrelease/into the packed kit'sversion, and a checkout isunreleased. The run refuses a tag that is notrelease/and a version, a release tag offmain, and a pre-release tag onmain.This repository:
AGENTS.mdopens with "Read .kit/1st.md first", and the installed copies (.kit/,.claude/,.github/workflows/kit-ci.yml) are committed, as in any consumer. A commit that changes a file undertree/ortargets/rewrites them in the same commit: the pre-commit hook runsjust refreshand stages what it rewrote, proven on two real commits. The copies stay committed rather than ignored so a fresh clone has its entry file, its deny rules, and its workflow before anyone runs anything.tests/is a bats suite over install, update, check, both hooks, and the packed tarball.This repository's own upkeep:
.editorconfig, and Dependabot for the SHA-pinned actions. The markdown rule's linter reads a delivered.kit/markdownlint.jsonc, so a fresh repository lints its delivered markdown clean and this repository has no markdownlint config of its own.The three open questions, decided here
updateremoves the previous release's and adds the new one's, and leaves the rest.kit contextis how a harness, and a person, finds the rules for a file, and the Claude Code hook makes the call for the agent.kit issue 42, from the release onPATH, with no justfile veneer anywhere: an alias is a habit that does not travel.Checked
just cipassed on this desk through the mise-pinned toolchain: the linterslefthook.ymldeclares over every tracked path, 63 bats cases over install, module, context, templates, gate, update, check, targets, lint, rebase, tidy, status, and the packed tarball, andbin/kit checkover this repository's installed copy.just kit status,just kit tidy --dry-run, andjust kit issue 1ran from this worktree against this repository.issuetook the reuse path: existing branch, worktree, start commit, and pull request, nothing duplicated.v0.1.0-rc.1,v0.1.0-rc.2, andv0.1.0-rc.3, the last on the final layout: a fresh repository pinned to rc.3 installed exactly the delivered text and the Claude target's copies, passedkit check, and hadkit rebaserefusemain. For rc.2, a fresh repository pinned to it installed exactly.kit/1st.md,.kit/rules/, and.kit/manifest, wiredcore.hooksPathto the release under mise's install directory, and passedkit check. For rc.1: every step passed, the GitHub pre-release carrieskit-0.1.0-rc.1.tar.gzand its checksum, and in a fresh repository"github:empowerite/kit" = "0.1.0-rc.1"inmise.tomlinstalled it with no asset pattern.kit versionprinted0.1.0-rc.1,kit installandkit checkpassed, andmod kit ".kit/kit.just"listed the three recipes. rc.1 and rc.2 are deleted; rc.3 and rc.4 stay until the branch-deletion workflow, once onmain, finds them unreachable.CIcheck on this pull request is the delivered workflow running on itself, and it passed onubuntu-latest:just ci(the same 14 bats cases),bin/kit check, and the linked-open-issue step. Three earlier runs failed on mise's npm backend for bats, which could not exec on Linux; bats now comes from the registry'saqua:backend, which fetches the release archive and needs no node.From review
Copilot's four findings, answered in their threads: three fixed in 76f202c (the start commit no longer sweeps staged work, tidy keeps a detached worktree whose commit
mainlacks, the gate fails on an unreadable justfile rather than skipping), and one moot, since thepre-rebasehook it concerns was replaced bykit rebase.From the thorough review
Six commits, one per group: four defects in
bin/(an update that overwrote a repository's own file at a path new to the release, a merged.jsonupdated without its base, a dangling release link, a verb without--help) plus the retire workflow's thirty-release limit;git.shwithout state passed through dynamic scope; the on-ramp as named steps;just refreshdriven by the manifest and the delivered workflow without a special case for this repository; the bash and just rules brought in line with the code; and a documentation pass over banners, headers, the entry file, and the tests.Install paths, and the picture of the work
brew install empowerite/tap/kitfrom the tap infrastructure#459 provisions, and the release tarball by hand.bin/kitresolves the release through any symlink, as Homebrew links it, andkit updatefetches the previous release for its merge base from the kit's cache, from mise if mise is there, or from the GitHub release itself; proven across rc.3 and rc.4 with mise offPATH.kit statusdraws the work in flight in three groups, a blank line between blocks and no headings: the desk, the root checkout, with its branch, how far it has moved from where that branch leftorigin/main, the lines and the uncommitted paths of its working tree, and the commits it carries past that point; each worktree underworktrees/drawn the same way, by issue number, labeled by its path from the current directory with the branch underlined; and each open pull request with its state (one of eight, each with its glyph, colored by family), its age, its checks (how many passed and how long they took, or which failed), its unresolved review threads when there are any, the branch a worktree here holds, and the issues it closes.--all, the default, shows every open pull request with its author before the state;--mineshows the caller's alone and no author;--owner=<login>, given once or more, the named authors'. Then the branches whose upstream is gone. Everything that is detail is dim.--jsonprints the same facts as one document, the pull requests chosen the same way. One request to the forge, and every other figure a local git read.gh release download.Not checked
kit updateacross two real releases. The pre-releases before v0.1.0 have a layout this release does not read, and no upgrade path from an unreleased version is kept; the update is tested with a synthetic later release andKIT_BASE, and a base fetched from a GitHub release was proven to land in the cache. The pre-release rc.5, tagged from this branch, installs a fresh repository by hand and through a mise pin, and every verb answers from it.just kit issueon a fresh issue: only the reuse path ran here, to avoid filing a test issue.Noted
actionlintis deprecated in favor ofjactionlint. The fork installs a binary namedjactionlint, so it is not a drop-in for the hook and the workflows;actionlintstays pinned, and the switch is a change of its own.Decided in review
{{...}}interpolation in a justfile; the file's path is an exported variable, and the just rule says so.main, a pre-release tag off it; the tag is the only version, stamped into the packed kit..kit/is1st.md,rules/,lefthook.yml, andmanifest, plus the repository's ownanswerswhere it has any. The verbs and the bash modules stay in the installed release; nothing in the repository reaches them except throughkit.mise.tomlpins what both run, actionlint included, since it reads workflow files and needs no runner;mise.desk.toml, read whenMISE_ENV=desk, which a desk exports, pins Lefthook. The deliveredCIjob setsMISE_ENV=ciso a consumer with a runner-only tool pins it inmise.ci.toml; this repository has none.just ciis the gate wherever it runs; the pre-commit hook is the narrower check the fragment declares, shellcheck and actionlint over the staged paths. The lint tests skip where Lefthook does not answer.MISE_PROFILEis an undocumented alias ofMISE_ENV; a[profiles]table is not a mise feature.lockfile_platformsmakes a lock written on a Mac carry the runner's platform.[kit.lint]table inmise.toml: mise warns on an unknown table on every invocation. The linters are declared where the hook manager reads them.bin/andmodules/, withdocs/beside them. A checkout'skit versionisunreleased; a release's is the stamped tag. This repository's.kit/,.claude/settings.json, and.github/workflows/kit-ci.ymlare its installed copies, as in any consumer; README § Maintaining lists them.Portable (#5)
.kit/settings.ymlreplaces.kit/answers:forge,gate,require_signing, andanswers, read by the kit and never written..kit/modules/<name>/, in the release's shape, validated, listed with their origin, and installed, checked, updated, and removed like a delivered one.kitis the one default module;ci-github,lefthook, andclaudeare named.kit ciruns the gate,kit check, andkit linkedanywhere, every one reported. Theci-githubworkflow takes the release the manifest names from its public URL and runskit ci; mise appears only where the repository has amise.toml.kit gateruns thegatesetting, else the detected justfile recipe, Makefile target, orpackage.jsonscript, and refuses when there is none.bin/forge.shnames nineteen calls,bin/forge-github.shanswers them overgh, chosen by theforgesetting or by origin's host. The core verbs never reach it;ghis the GitHub provider's requirement alone, and the base release is fetched withcurl.regions/<base>/is written between marker comments inside the file the repository owns,kit checkholds only what is between them, update merges only that, and removal takes the region out. Thekitmodule places theAGENTS.mdsentence and the/worktrees/ignore entry that way, so a repository's ownAGENTS.mdand.gitignorenever failkit check. Markers follow the file's comment syntax by extension, one file holds one region, two modules delivering a region to the same path are refused at render, a region that opens and never closes is refused by name, and a symlinked destination keeps its link. This repository'sjust refreshiskit updaterun over the index export withHEAD's release as the merge base, staged back through a temporary index, so a desk's unstaged text around a region is never staged with the kit's change and an entry the source dropped is retired.:generate:<command>:until:"<pattern>"directive, enchiridion's, in a file's own comment syntax opens a region its command fills;kit generateexpands every one in the repository to a fixed point,kit generate --checknames a stale one,kit checkruns it over the whole repository, and the lefthook fragment over the staged paths. The command is one command and its arguments, no shell operator. The kit's own regions are made that way: install writes:generate:kit region <path>:until:"kit:end"and the content,kit region <path>prints what the active modules deliver there, update regenerates a region rather than merging it, and a hand edit is a stale region, refused by the gate.kit lintruns it over the paths the rule governs, with no hook manager; thelefthookfragment is one command calling it. The workflow rule is split out of the YAML rule.Host decisions abstracted (#11, #12, #13)
expects:what line it needs in a file the repository owns, such as lefthook'sextends: [.kit/lefthook.yml];kit installnames an unmet one as left to do,kit checkrefuses until it is there, and the kit writes nothing into the file. The placement rule is now: an include where the format composes, a region where it has comments, a merge by key where it has neither, a copy only where the file is the kit's own.bin/forge-gitlab.shanswers every call of the forge seam overglab, picked by an origin at gitlab.com or byforge: gitlabfor a self-hosted one. The seam growsnounandpr-url, sokit statusdraws#3or!3in otherwise identical rows andkit linkedandkit issuesay pull request or merge request.tests/forge-conformance.batsdrives both providers through a routed fake of their tool against one world and holds every answer to one shape.bin/tracker.shseparates the issue from the forge. A reference names its tracker,#7for the forge's own andAB#4521for an Azure DevOps work item declared in.kit/settings.yml.kit issue,kit linked, andkit tidytake references; a non-forge issue gets anab4521-<slug>branch and aCloses AB#4521line in the change's body.docs/trackers.mdis the page.Neither GitLab nor Azure DevOps has been run against a real instance; the fakes prove each provider's folding of its API's answers, not that the API answers as the fixtures do.
Toolchains and policy (#14, #15)
bin/toolchain.shasks mise, Homebrew, and docker the same questions,install,outdated,update,path, andrun. A repository declares its toolchains or the kit detects them frommise.toml,Brewfile, andtoolchain.Dockerfile.kit gateruns the gate through them, inside the image where docker is declared, and the delivered workflow installs withkit toolchain install, its mise action kept only to put themisebinary on the runner. All three providers ran against the real tools on a desk, and this repository's CI ran the new workflow.kit policygathers one facts document, the branch, the commits past the fork, and the change's title, and an engine names the refusals:patternsby default, or Open Policy Agent over the Rego under.kit/policy/. Thepolicy-opamodule delivers the same rules in Rego, andtests/policy.batsholds the two engines to identical refusals.kit ciruns it as its fourth check. This repository turns the commit-subject rule off until this pull request merges, since its early commits predate the rule.Closes #4
Closes #5
Closes #8
Closes #11
Closes #12
Closes #13
Closes #14
Closes #15
Left to a human
Review and merge. Then tag
release/0.1.0on the merge commit onmain: the release workflow publisheskit-0.1.0.tar.gzwith that version stamped in, as the releasekit 0.1.0. A mise pin is{ version = "0.1.0", version_prefix = "release/" }. Nothing in the tree needs to change first.🤖 Generated with Claude Code