the_local 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +15 -0
- data/LICENSE.txt +21 -0
- data/PROVIDERS.md +135 -0
- data/README.md +72 -0
- data/Rakefile +15 -0
- data/exe/the_local +6 -0
- data/lib/generators/the_local/install_generator.rb +21 -0
- data/lib/generators/the_local/provider_generator.rb +144 -0
- data/lib/generators/the_local/templates/guide.md.tt +25 -0
- data/lib/generators/the_local/templates/reference.rb.tt +17 -0
- data/lib/generators/the_local/templates/the_local.rb.tt +46 -0
- data/lib/the_local/agent.rb +41 -0
- data/lib/the_local/builder.rb +30 -0
- data/lib/the_local/cli.rb +34 -0
- data/lib/the_local/disk_providers.rb +32 -0
- data/lib/the_local/installer.rb +41 -0
- data/lib/the_local/process_doc_writer.rb +48 -0
- data/lib/the_local/process_rules/develop_process_rules.md +97 -0
- data/lib/the_local/process_rules.rb +17 -0
- data/lib/the_local/railtie.rb +15 -0
- data/lib/the_local/rake.rb +23 -0
- data/lib/the_local/reference/guide.md +103 -0
- data/lib/the_local/reference.rb +16 -0
- data/lib/the_local/refresh.rb +28 -0
- data/lib/the_local/registry.rb +59 -0
- data/lib/the_local/scope.rb +15 -0
- data/lib/the_local/sync.rb +28 -0
- data/lib/the_local/tasks/the_local.rake +9 -0
- data/lib/the_local/the_local/agents/the_local-develop.md +111 -0
- data/lib/the_local/the_local/agents/the_local-info.md +111 -0
- data/lib/the_local/the_local/agents/the_local-install.md +111 -0
- data/lib/the_local/the_local.rb +59 -0
- data/lib/the_local/trigger_writer.rb +64 -0
- data/lib/the_local/version.rb +5 -0
- data/lib/the_local.rb +51 -0
- data/sig/the_local.rbs +4 -0
- metadata +95 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 16b120e16f6b7ca6130882854eaafb3d7d5c70ae35e2fbab17e094f4cb365056
|
|
4
|
+
data.tar.gz: 32ef6ba59eac385d577fc5d068477ffd465e14130dadf09b47b7b9166bf07b89
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e4fb273d9d3646d17a47f76b6c21048c38d62c2bf40ad491d609b9873e759e9063a47bdfc0f1eb1ccbf7cd33acea2757411b862aa5ec19ad5659d0fbbf3ba69f
|
|
7
|
+
data.tar.gz: 3fa1ff5e3989868bd713e7b21b1b253d866e9bc060b02294b1ea09f37b4056b8c0a2b4df9c009fcfa8198041ab301feab9985a1368daff5c6c561a1c072bf3b5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-06-02
|
|
4
|
+
|
|
5
|
+
- Initial release.
|
|
6
|
+
- `TheLocal.register` API for gems and apps to contribute Claude Code locals,
|
|
7
|
+
behind a soft `require "the_local"` guard so providers work standalone.
|
|
8
|
+
- Provider build model: `TheLocal::Builder` + `rake the_local:build` render each
|
|
9
|
+
agent to a committed `.md`; the installer copies those files verbatim.
|
|
10
|
+
- `the_local:install` and `the_local:provider` Rails generators, plus a
|
|
11
|
+
rake-only `the_local:refresh` to re-sync a host after bundle changes.
|
|
12
|
+
- Direct-dependency install scope and a registry-generated delegation trigger
|
|
13
|
+
written into the host's `CLAUDE.md`/`AGENTS.md`.
|
|
14
|
+
- the_local dogfoods itself as a provider (`the_local-info`/`-install`/`-develop`)
|
|
15
|
+
and propagates a canonical develop-process doc into every host.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tylercschneider
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/PROVIDERS.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Becoming a provider
|
|
2
|
+
|
|
3
|
+
`the_local` has two sides. The **consuming-app** side (`bundle exec the_local
|
|
4
|
+
install`, or `bin/rails g the_local:install` in a Rails app) installs the locals
|
|
5
|
+
of a project's direct dependencies into `.claude/agents/` — by reading each
|
|
6
|
+
dependency's committed `.md` straight from its gem path on disk. This document
|
|
7
|
+
covers the **provider** side: how a gem contributes those locals.
|
|
8
|
+
|
|
9
|
+
A provider registers its agents with `TheLocal.register` behind a soft
|
|
10
|
+
`require "the_local"` guard, so the gem keeps working when `the_local` is absent.
|
|
11
|
+
|
|
12
|
+
## Build at home, copy verbatim
|
|
13
|
+
|
|
14
|
+
The agent *definition* (`the_local.rb` + `guide.md`) is the single source of
|
|
15
|
+
truth. The provider **renders it to committed `.md` files** with a gem-side
|
|
16
|
+
`the_local:build` task and commits those files to its own repo; the host install
|
|
17
|
+
then **reads them straight from your gem's path on disk and copies them verbatim**
|
|
18
|
+
into `.claude/agents/`. No provider code is loaded in the host and no register
|
|
19
|
+
block runs there — the committed, shipped `.md` is the entire contract. If those
|
|
20
|
+
files aren't committed and in the gemspec's `files`, the gem contributes nothing;
|
|
21
|
+
if they are, it contributes everything, with no install-time wiring.
|
|
22
|
+
|
|
23
|
+
So the rendered output depends only on the provider gem version — every app that
|
|
24
|
+
installs the same version gets a byte-identical local, instead of the host
|
|
25
|
+
re-rendering (and possibly drifting) from an in-memory definition. The committed
|
|
26
|
+
`.md` is a reviewable build artifact: it lands in the gem's own PR. Keep it in
|
|
27
|
+
sync with the definition by re-running `the_local:build` and committing the
|
|
28
|
+
result whenever you change the guide or an agent's `body:`/`description:`.
|
|
29
|
+
|
|
30
|
+
## The common command interface
|
|
31
|
+
|
|
32
|
+
`the_local` exists to give every gem the **same command interface to apps**, so
|
|
33
|
+
a host agent always finds the same shape no matter which gem it's delegating to.
|
|
34
|
+
A provider exposes three lifecycle facets:
|
|
35
|
+
|
|
36
|
+
| Facet | Purpose | Typical tools |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| **`info`** | Read-only. Explains what the gem offers — its API and conventions. Makes no changes. | `Read` |
|
|
39
|
+
| **`install`** | Adds the gem to a host and sets it up **correctly** — the exact, gem-specific steps. | `Bash, Read, Edit` |
|
|
40
|
+
| **worker** | The proactive domain worker the host routes real work to. Named `develop` for libraries you build against, `operate` for CLIs you run. | per domain |
|
|
41
|
+
|
|
42
|
+
`info` and `install` are universal; the worker's name varies by the gem's
|
|
43
|
+
nature. Every agent embeds the provider's knowledge (`Reference.content`)
|
|
44
|
+
verbatim — that knowledge is the single source of truth, so the locals never
|
|
45
|
+
drift from the docs.
|
|
46
|
+
|
|
47
|
+
## Adopting it — Rails-engine gems (generator)
|
|
48
|
+
|
|
49
|
+
If the gem has Rails available in development (e.g. a mountable engine), scaffold
|
|
50
|
+
the wiring with the generator:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
bin/rails g the_local:provider <gem_name> \
|
|
54
|
+
--scope "one-line phrase describing the gem's domain" \
|
|
55
|
+
[--prefix <filename-namespace>] \
|
|
56
|
+
[--worker develop|operate]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
It creates, and wires up:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
lib/<gem>/reference.rb # the Reference loader (single source of truth)
|
|
63
|
+
lib/<gem>/reference/guide.md # the knowledge, with TODO markers to fill in
|
|
64
|
+
lib/<gem>/the_local.rb # Companion.register! — info / install / <worker>
|
|
65
|
+
lib/<gem>/the_local/agents/*.md # the rendered locals, built + committed
|
|
66
|
+
Gemfile # + gem "the_local", github: … (soft, dev/test)
|
|
67
|
+
lib/<gem>.rb # + require_relative "<gem>/the_local"
|
|
68
|
+
Rakefile # + require "the_local/rake" (rake the_local:build)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The generator builds the `.md` once on scaffold so they land in the diff for
|
|
72
|
+
review. They are rendered from the TODO placeholder definition, so you rebuild
|
|
73
|
+
them after filling in the real content (below).
|
|
74
|
+
|
|
75
|
+
Then **fill in the scaffold** — this is the real work the generator can't do:
|
|
76
|
+
|
|
77
|
+
1. Write `reference/guide.md` as the complete user-facing API. Its **Install**
|
|
78
|
+
section must be the exact, correct steps for *this* gem (for an engine:
|
|
79
|
+
add the gem → `bundle install` → install + run migrations → wire concerns /
|
|
80
|
+
initializers), not a generic placeholder.
|
|
81
|
+
2. Tailor the three agent `body:` strings in `the_local.rb` to the gem.
|
|
82
|
+
3. **Rebuild and commit the locals.** The scaffold built `.md` from the TODO
|
|
83
|
+
placeholders, so regenerate them from your real definition and commit them:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
rake the_local:build
|
|
87
|
+
git add lib/<gem>/the_local/agents
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Rebuild whenever the guide or an agent's `body:`/`description:` changes — the
|
|
91
|
+
host copies these bytes verbatim, so a stale commit ships stale locals.
|
|
92
|
+
4. Add a `companion_test` asserting the facets register and each embeds
|
|
93
|
+
`Reference.content`, plus a **drift test** asserting each committed file
|
|
94
|
+
equals its `agent.to_markdown` (so a forgotten rebuild fails CI). See
|
|
95
|
+
`test/the_local/companion_test.rb` — the_local is its own provider and uses
|
|
96
|
+
exactly this wiring.
|
|
97
|
+
|
|
98
|
+
## Adopting it — non-Rails gems (manual)
|
|
99
|
+
|
|
100
|
+
A plain gem has no `bin/rails`, so do the same things by hand. `the_local` is
|
|
101
|
+
itself a non-Rails provider built this way — mirror its own wiring
|
|
102
|
+
(`lib/the_local/the_local.rb`, `lib/the_local/reference.rb`, the committed
|
|
103
|
+
`lib/the_local/the_local/agents/`, and the `Rakefile`):
|
|
104
|
+
|
|
105
|
+
1. `lib/<gem>/reference.rb` — a `Reference` module with `DIR`, `content`, and
|
|
106
|
+
`read(name)` reading from `lib/<gem>/reference/`.
|
|
107
|
+
2. `lib/<gem>/reference/guide.md` — the knowledge (single source of truth).
|
|
108
|
+
3. `lib/<gem>/the_local.rb` — a `Companion.register!` that calls `TheLocal.register`
|
|
109
|
+
with an `agents_dir` (where the committed `.md` live) and declares the
|
|
110
|
+
`info` / `install` / worker agents, followed by the guard:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
TheLocal.register("<gem>", scope: "…",
|
|
114
|
+
agents_dir: File.expand_path("the_local/agents", __dir__)) do |c|
|
|
115
|
+
# c.agent "info", …
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
begin
|
|
121
|
+
require "the_local"
|
|
122
|
+
<Gem>::Companion.register!
|
|
123
|
+
rescue LoadError
|
|
124
|
+
# the_local not installed — <gem> works standalone.
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
4. `require_relative "<gem>/the_local"` from the gem's entrypoint (so your own
|
|
128
|
+
`the_local:build` and standalone use load the register block — a host never
|
|
129
|
+
needs it), and add `gem "the_local", github: "DYB-Development/the_local"` to
|
|
130
|
+
the Gemfile (dev/test — an optional companion, not a hard dependency).
|
|
131
|
+
5. Add `require "the_local/rake"` to the `Rakefile`, then build, commit, and
|
|
132
|
+
**ship** the rendered locals — `rake the_local:build && git add
|
|
133
|
+
lib/<gem>/the_local/agents`, and make sure they're in the gemspec's `files`.
|
|
134
|
+
These committed bytes are the whole contract — what the host reads from disk;
|
|
135
|
+
rebuild and recommit on every change.
|
data/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# TheLocal
|
|
2
|
+
|
|
3
|
+
Resident Claude Code expert subagents ("locals"), contributed by the gems an app
|
|
4
|
+
uses. Any gem or app declares the locals that know its conventions; `the_local`
|
|
5
|
+
aggregates the locals of an app's installed providers into its
|
|
6
|
+
`.claude/agents/`, plus a delegation rule so the app's agent actually uses them.
|
|
7
|
+
|
|
8
|
+
A "local" is a Claude Code subagent that knows one gem's conventions cold. The
|
|
9
|
+
host's orchestrating agent delegates that gem's work to it, so usage stays
|
|
10
|
+
consistent instead of drifting.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Add it to your Gemfile:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem "the_local"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
or install it directly with `gem install the_local`. To track an unreleased
|
|
21
|
+
change, point at the repo instead:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem "the_local", github: "DYB-Development/the_local"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The gem's core is Rails-free, but the primary documented workflow uses the
|
|
28
|
+
`bin/rails g the_local:install` / `the_local:provider` generators, so those steps
|
|
29
|
+
assume a Rails host. The `the_local:refresh` rake task and the register API work
|
|
30
|
+
without Rails.
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
There are two sides.
|
|
35
|
+
|
|
36
|
+
**Consuming app** — install the locals of the app's direct dependencies (and the
|
|
37
|
+
app's own) into `.claude/agents/`, and write the delegation trigger:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bin/rails g the_local:install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Install **copies each provider's committed `.md` verbatim** — the rendering
|
|
44
|
+
happens in the provider gem at build time, not in the host — so every app on the
|
|
45
|
+
same gem version gets a byte-identical local. Re-run `the_local:refresh` (rake)
|
|
46
|
+
after a `bundle install`/`update` to re-sync.
|
|
47
|
+
|
|
48
|
+
**Provider gem** — contribute the locals an app installs. A gem registers its
|
|
49
|
+
agents with `TheLocal.register` behind a soft guard, exposing the common command
|
|
50
|
+
interface (`info` / `install` / worker), then renders them to committed `.md`
|
|
51
|
+
with `rake the_local:build`. Scaffold it with:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
bin/rails g the_local:provider <gem_name> --scope "the gem's domain"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
See [PROVIDERS.md](PROVIDERS.md) for the full provider guide, including the
|
|
58
|
+
manual steps for non-Rails gems.
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
63
|
+
|
|
64
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
65
|
+
|
|
66
|
+
## Contributing
|
|
67
|
+
|
|
68
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/DYB-Development/the_local.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "minitest/test_task"
|
|
5
|
+
|
|
6
|
+
Minitest::TestTask.create
|
|
7
|
+
|
|
8
|
+
require "rubocop/rake_task"
|
|
9
|
+
|
|
10
|
+
RuboCop::RakeTask.new
|
|
11
|
+
|
|
12
|
+
task default: %i[test rubocop]
|
|
13
|
+
|
|
14
|
+
require "the_local"
|
|
15
|
+
require "the_local/rake"
|
data/exe/the_local
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "the_local"
|
|
5
|
+
|
|
6
|
+
module TheLocal
|
|
7
|
+
module Generators
|
|
8
|
+
# `bin/rails g the_local:install` — installs the locals contributed by the
|
|
9
|
+
# app's direct dependencies into .claude/agents/, and writes the delegation
|
|
10
|
+
# trigger. A thin wrapper over Refresh, which discovers providers from each
|
|
11
|
+
# bundled gem's committed agents on disk.
|
|
12
|
+
class InstallGenerator < Rails::Generators::Base
|
|
13
|
+
desc "Install the Claude Code locals of this app's direct dependencies"
|
|
14
|
+
|
|
15
|
+
def install_locals
|
|
16
|
+
allowed = Refresh.call(destination: destination_root)
|
|
17
|
+
say "the_local: installed locals for #{allowed.join(", ")}", :green
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "the_local/builder"
|
|
5
|
+
|
|
6
|
+
module TheLocal
|
|
7
|
+
module Generators
|
|
8
|
+
# `bin/rails g the_local:provider <gem_name>` — scaffolds the provider side
|
|
9
|
+
# of the_local into a gem: a Reference loader, a knowledge guide, and a
|
|
10
|
+
# Companion that registers the common command interface (info / install /
|
|
11
|
+
# worker) via TheLocal.register behind a soft `require "the_local"` guard.
|
|
12
|
+
#
|
|
13
|
+
# The companion app side is `the_local:install`; this is its mirror for the
|
|
14
|
+
# gems that *contribute* locals. See PROVIDERS.md.
|
|
15
|
+
class ProviderGenerator < Rails::Generators::Base
|
|
16
|
+
source_root File.expand_path("templates", __dir__)
|
|
17
|
+
|
|
18
|
+
GEMFILE_LINE = %(gem "the_local", github: "DYB-Development/the_local")
|
|
19
|
+
RAKEFILE_REQUIRE = %(require "the_local/rake")
|
|
20
|
+
|
|
21
|
+
desc "Scaffold the_local provider wiring (info/install/worker locals) into this gem"
|
|
22
|
+
|
|
23
|
+
argument :gem_name, type: :string, desc: "The providing gem's name, e.g. citizen"
|
|
24
|
+
class_option :prefix, type: :string,
|
|
25
|
+
desc: "Agent filename namespace (defaults to the gem name)"
|
|
26
|
+
class_option :scope, type: :string, default: "TODO: one-line phrase describing this gem's domain",
|
|
27
|
+
desc: "One-line domain phrase used in the delegation trigger"
|
|
28
|
+
class_option :worker, type: :string, default: "develop",
|
|
29
|
+
desc: "Name of the domain worker facet (develop for libraries, operate for CLIs)"
|
|
30
|
+
|
|
31
|
+
def relocate_to_gem_root
|
|
32
|
+
self.destination_root = gem_root
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create_reference
|
|
36
|
+
template "reference.rb.tt", "lib/#{lib_path}/reference.rb"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create_guide
|
|
40
|
+
template "guide.md.tt", "lib/#{lib_path}/reference/guide.md"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def create_companion
|
|
44
|
+
template "the_local.rb.tt", "lib/#{lib_path}/the_local.rb"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def add_to_gemfile
|
|
48
|
+
return if gem_name == "the_local"
|
|
49
|
+
|
|
50
|
+
gemfile = File.join(destination_root, "Gemfile")
|
|
51
|
+
return unless File.exist?(gemfile)
|
|
52
|
+
return if File.read(gemfile).include?(GEMFILE_LINE)
|
|
53
|
+
|
|
54
|
+
append_to_file "Gemfile",
|
|
55
|
+
"\n# Optional companion: #{gem_name} registers its locals with the_local " \
|
|
56
|
+
"when present.\n# Registration is guarded, so #{gem_name} works standalone.\n#{GEMFILE_LINE}\n"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def require_from_entrypoint
|
|
60
|
+
entrypoint = File.join("lib", "#{lib_path}.rb")
|
|
61
|
+
return unless File.exist?(File.join(destination_root, entrypoint))
|
|
62
|
+
return if File.read(File.join(destination_root, entrypoint)).include?(require_line)
|
|
63
|
+
|
|
64
|
+
append_to_file entrypoint,
|
|
65
|
+
"\n# Register #{gem_name}'s locals when the_local is present (no-op otherwise).\n" \
|
|
66
|
+
"#{require_line}\n"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def hook_build_task_into_rakefile
|
|
70
|
+
return unless File.exist?(File.join(destination_root, "Rakefile"))
|
|
71
|
+
return if File.read(File.join(destination_root, "Rakefile")).include?(RAKEFILE_REQUIRE)
|
|
72
|
+
|
|
73
|
+
append_to_file "Rakefile",
|
|
74
|
+
"\n# Render #{gem_name}'s committed the_local agent files: `rake the_local:build`.\n" \
|
|
75
|
+
"require \"#{lib_path}\"\n#{RAKEFILE_REQUIRE}\n"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Render the committed .md files now, so they land in the diff for review.
|
|
79
|
+
# Loading the companion registers this gem's locals; reset first so only
|
|
80
|
+
# they are built, not anything else the process may have registered.
|
|
81
|
+
def build_agent_files
|
|
82
|
+
companion = File.join(destination_root, "lib", lib_path, "the_local.rb")
|
|
83
|
+
return unless File.exist?(companion)
|
|
84
|
+
|
|
85
|
+
TheLocal.reset!
|
|
86
|
+
load companion
|
|
87
|
+
TheLocal::Builder.new(registry: TheLocal.registry).call
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def require_line
|
|
93
|
+
%(require_relative "#{File.basename(lib_path)}/the_local")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def prefix
|
|
97
|
+
options[:prefix] || gem_name
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Thor renders templates via File.binread, so the ERB buffer is ASCII-8BIT.
|
|
101
|
+
# A UTF-8 scope would flip the buffer mid-render and then clash with the
|
|
102
|
+
# template's own non-ASCII literals; match the buffer's encoding to avoid it.
|
|
103
|
+
def scope
|
|
104
|
+
options[:scope]&.b
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def worker
|
|
108
|
+
options[:worker]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def gem_root
|
|
112
|
+
ascend_to_gemspec(destination_root) || destination_root
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def ascend_to_gemspec(start)
|
|
116
|
+
dir = File.expand_path(start)
|
|
117
|
+
loop do
|
|
118
|
+
return dir if Dir.glob(File.join(dir, "*.gemspec")).any?
|
|
119
|
+
|
|
120
|
+
parent = File.dirname(dir)
|
|
121
|
+
return nil if parent == dir
|
|
122
|
+
|
|
123
|
+
dir = parent
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def lib_path
|
|
128
|
+
gem_name.tr("-", "/")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def module_name
|
|
132
|
+
gem_name.split("-").map { |segment| segment.split("_").map(&:capitalize).join }.join("::")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def open_module
|
|
136
|
+
module_name.split("::").map { |name| "module #{name}" }.join("\n")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def close_module
|
|
140
|
+
module_name.split("::").map { "end" }.join("\n")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## <%= module_name %>
|
|
2
|
+
|
|
3
|
+
> **DO NOT** explore the <%= gem_name %> gem source code. This reference is the
|
|
4
|
+
> complete user-facing API, embedded verbatim into every <%= gem_name %> local so
|
|
5
|
+
> their guidance never drifts. Keep it the single source of truth.
|
|
6
|
+
|
|
7
|
+
TODO: One paragraph — what <%= gem_name %> is and the problem it solves.
|
|
8
|
+
|
|
9
|
+
### What it offers
|
|
10
|
+
|
|
11
|
+
TODO: The public API — the methods/classes/DSL a host actually calls, with tiny
|
|
12
|
+
examples. This is what the `info` and `<%= worker %>` locals answer from.
|
|
13
|
+
|
|
14
|
+
### Install
|
|
15
|
+
|
|
16
|
+
TODO: The exact, correct steps to add <%= gem_name %> to a host and set it up —
|
|
17
|
+
this is what the `install` local follows. Be specific to how <%= gem_name %> is
|
|
18
|
+
actually installed (e.g. for a Rails engine: add the gem, `bundle install`,
|
|
19
|
+
install + run migrations, wire any concerns/initializers). Don't leave it
|
|
20
|
+
generic.
|
|
21
|
+
|
|
22
|
+
### <%= module_name %> conventions
|
|
23
|
+
|
|
24
|
+
TODO: The conventions the `<%= worker %>` local must enforce when doing
|
|
25
|
+
<%= gem_name %> work, so usage stays consistent across the host.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
<%= open_module %>
|
|
4
|
+
# Single source of truth for <%= gem_name %>'s user-facing API, read by the
|
|
5
|
+
# the_local companion subagents so their guidance never drifts from the docs.
|
|
6
|
+
module Reference
|
|
7
|
+
DIR = File.expand_path("reference", __dir__)
|
|
8
|
+
|
|
9
|
+
def self.content
|
|
10
|
+
read("guide.md")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.read(name)
|
|
14
|
+
File.read(File.join(DIR, name)).chomp
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
<%= close_module %>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "reference"
|
|
4
|
+
|
|
5
|
+
<%= open_module %>
|
|
6
|
+
# Registers <%= gem_name %>'s locals (Claude Code subagents) with the_local.
|
|
7
|
+
# These are the common command interface every provider exposes to apps:
|
|
8
|
+
# `info` (read-only, explains the gem), `install` (sets it up in a host), and
|
|
9
|
+
# `<%= worker %>` (the proactive domain worker). Soft dependency: registration
|
|
10
|
+
# is a no-op when the_local is absent.
|
|
11
|
+
module Companion
|
|
12
|
+
def self.register!
|
|
13
|
+
TheLocal.register("<%= gem_name %>"<%= ", prefix: \"#{prefix}\"" unless prefix == gem_name %>, scope: "<%= scope %>",
|
|
14
|
+
agents_dir: File.expand_path("the_local/agents", __dir__)) do |c|
|
|
15
|
+
c.agent "info",
|
|
16
|
+
description: "Use to learn what <%= gem_name %> offers — its API and conventions.",
|
|
17
|
+
tools: "Read",
|
|
18
|
+
body: "You explain what <%= gem_name %> does and how to use it, answering from the " \
|
|
19
|
+
"reference. You make no changes. TODO: tailor this body to <%= gem_name %>.",
|
|
20
|
+
knowledge: <%= module_name %>::Reference.content
|
|
21
|
+
|
|
22
|
+
c.agent "install",
|
|
23
|
+
description: "Use to add <%= gem_name %> to a project and set it up correctly.",
|
|
24
|
+
tools: "Bash, Read, Edit",
|
|
25
|
+
body: "You add <%= gem_name %> to the project and complete its setup, following the " \
|
|
26
|
+
"reference's install section exactly. TODO: tailor this body to <%= gem_name %>.",
|
|
27
|
+
knowledge: <%= module_name %>::Reference.content
|
|
28
|
+
|
|
29
|
+
c.agent "<%= worker %>",
|
|
30
|
+
description: "Use PROACTIVELY for any <%= gem_name %> work. MUST BE USED instead of " \
|
|
31
|
+
"hand-rolling it. TODO: name the concrete tasks this local owns.",
|
|
32
|
+
tools: "Read, Write, Edit, Grep",
|
|
33
|
+
body: "You do <%= gem_name %> work following the reference's conventions. TODO: tailor " \
|
|
34
|
+
"this body to <%= gem_name %>.",
|
|
35
|
+
knowledge: <%= module_name %>::Reference.content
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
<%= close_module %>
|
|
40
|
+
|
|
41
|
+
begin
|
|
42
|
+
require "the_local"
|
|
43
|
+
<%= module_name %>::Companion.register!
|
|
44
|
+
rescue LoadError
|
|
45
|
+
# the_local not installed — <%= gem_name %> works standalone.
|
|
46
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TheLocal
|
|
4
|
+
# An immutable description of one Claude Code subagent contributed by a
|
|
5
|
+
# provider (a gem or the app). Renders to a `.claude/agents/*.md` definition.
|
|
6
|
+
#
|
|
7
|
+
# +gem_name+ is the providing gem (used to filter to a host's direct
|
|
8
|
+
# dependencies). +prefix+ is the filename namespace (often a shorter alias,
|
|
9
|
+
# e.g. gem "keystone_ui" → prefix "keystone"). +knowledge+ is a string or
|
|
10
|
+
# array of strings appended below the role body — the provider's reference(s).
|
|
11
|
+
# +source_path+ is the absolute path to the provider's committed, pre-rendered
|
|
12
|
+
# .md file; the host installer copies it verbatim. It stays nil until a
|
|
13
|
+
# provider supplies an agents_dir, so existing providers keep working.
|
|
14
|
+
Agent = Data.define(:gem_name, :prefix, :name, :description, :tools, :body, :knowledge, :source_path) do
|
|
15
|
+
def initialize(source_path: nil, **args)
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def qualified_name
|
|
20
|
+
"#{prefix}-#{name}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def filename
|
|
24
|
+
"#{qualified_name}.md"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_markdown
|
|
28
|
+
<<~MARKDOWN
|
|
29
|
+
---
|
|
30
|
+
name: #{qualified_name}
|
|
31
|
+
description: #{description}
|
|
32
|
+
tools: #{tools}
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
#{body}
|
|
36
|
+
|
|
37
|
+
#{Array(knowledge).join("\n\n")}
|
|
38
|
+
MARKDOWN
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module TheLocal
|
|
6
|
+
# Renders each registered agent's markdown to its committed source_path, so a
|
|
7
|
+
# provider gem can commit the files and the host installer later copies them
|
|
8
|
+
# verbatim (rather than rendering at install time). Plain Ruby — driven by the
|
|
9
|
+
# the_local:build rake task a provider runs. Agents that declared no agents_dir
|
|
10
|
+
# (and so have no source_path) are skipped: there is nowhere to write them.
|
|
11
|
+
class Builder
|
|
12
|
+
def initialize(registry:)
|
|
13
|
+
@registry = registry
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
buildable_agents.map do |agent|
|
|
18
|
+
FileUtils.mkdir_p(File.dirname(agent.source_path))
|
|
19
|
+
File.write(agent.source_path, agent.to_markdown)
|
|
20
|
+
agent.source_path
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def buildable_agents
|
|
27
|
+
@registry.agents.select(&:source_path)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "the_local"
|
|
4
|
+
|
|
5
|
+
module TheLocal
|
|
6
|
+
# Rails-free entrypoint for the `the_local` executable. `the_local install`
|
|
7
|
+
# syncs the current bundle's locals into .claude/agents/, so a plain gem can
|
|
8
|
+
# install without a Rails generator or a Rakefile.
|
|
9
|
+
class CLI
|
|
10
|
+
def initialize(argv, out: $stdout)
|
|
11
|
+
@argv = argv
|
|
12
|
+
@out = out
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
case @argv.first
|
|
17
|
+
when "install" then install
|
|
18
|
+
else usage
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def install
|
|
25
|
+
allowed = Refresh.call(destination: Dir.pwd)
|
|
26
|
+
@out.puts "the_local: installed locals for #{allowed.join(", ")}"
|
|
27
|
+
@out.puts "Restart your Claude Code session to use them — agents load at startup."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def usage
|
|
31
|
+
@out.puts "Usage: the_local install"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|