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
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TheLocal
|
|
4
|
+
# Discovers providers by reading their committed agent files straight from each
|
|
5
|
+
# bundled gem's path on disk — no gem code is loaded and no register block runs.
|
|
6
|
+
# The committed .md (the build-and-commit artifact) is the declarative contract;
|
|
7
|
+
# a provider contributes simply by shipping those files. Populates the same
|
|
8
|
+
# registry the install pipeline already reads, so Installer/TriggerWriter/Sync
|
|
9
|
+
# are unchanged.
|
|
10
|
+
module DiskProviders
|
|
11
|
+
AGENTS_GLOB = File.join("lib", "**", "the_local", "agents", "*.md")
|
|
12
|
+
|
|
13
|
+
def self.load(registry:, specs:)
|
|
14
|
+
specs.each { |spec| register(registry, spec) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.register(registry, spec)
|
|
18
|
+
files = Dir.glob(File.join(spec[:path], AGENTS_GLOB))
|
|
19
|
+
return if files.empty?
|
|
20
|
+
|
|
21
|
+
agents = files.map { |file| agent_from(spec[:name], file) }
|
|
22
|
+
registry.add_provider(Provider.new(gem_name: spec[:name], prefix: agents.first.prefix, scope: nil))
|
|
23
|
+
agents.each { |agent| registry.add(agent) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.agent_from(gem_name, file)
|
|
27
|
+
prefix, _, name = File.basename(file, ".md").rpartition("-")
|
|
28
|
+
Agent.new(gem_name: gem_name, prefix: prefix, name: name,
|
|
29
|
+
description: nil, tools: nil, body: nil, knowledge: nil, source_path: file)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module TheLocal
|
|
6
|
+
# Copies each allowed provider's committed agent file into a destination's
|
|
7
|
+
# .claude/agents/ directory, verbatim. Plain Ruby so the Rails generator is a
|
|
8
|
+
# thin wrapper over it.
|
|
9
|
+
class Installer
|
|
10
|
+
AGENTS_DIR = ".claude/agents"
|
|
11
|
+
|
|
12
|
+
def initialize(registry:, destination:, allowed_gems:)
|
|
13
|
+
@registry = registry
|
|
14
|
+
@destination = destination
|
|
15
|
+
@allowed_gems = allowed_gems
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
agents_dir = File.join(@destination, AGENTS_DIR)
|
|
20
|
+
FileUtils.mkdir_p(agents_dir)
|
|
21
|
+
|
|
22
|
+
installed_agents.each do |agent|
|
|
23
|
+
ensure_committed!(agent)
|
|
24
|
+
FileUtils.cp(agent.source_path, File.join(agents_dir, agent.filename))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def installed_agents
|
|
31
|
+
@registry.agents.select { |agent| @allowed_gems.include?(agent.gem_name) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def ensure_committed!(agent)
|
|
35
|
+
return if agent.source_path && File.exist?(agent.source_path)
|
|
36
|
+
|
|
37
|
+
raise Error, "the_local: #{agent.gem_name} registered #{agent.qualified_name} without a committed " \
|
|
38
|
+
"agent file. Run `rake the_local:build` in #{agent.gem_name} and commit its the_local/agents/."
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "process_rules"
|
|
4
|
+
|
|
5
|
+
module TheLocal
|
|
6
|
+
# Writes the canonical develop-process rules into a host's CLAUDE.md as a
|
|
7
|
+
# managed block, read at the start of every session so the host agent always
|
|
8
|
+
# follows one source of truth. Re-propagated on every install/refresh. Uses
|
|
9
|
+
# its own markers so it coexists with the delegation trigger in the same file.
|
|
10
|
+
class ProcessDocWriter
|
|
11
|
+
BEGIN_MARKER = "<!-- the_local:process:begin -->"
|
|
12
|
+
END_MARKER = "<!-- the_local:process:end -->"
|
|
13
|
+
RULES_FILENAME = "develop_process_rules.md"
|
|
14
|
+
|
|
15
|
+
def initialize(destination:, filename: "CLAUDE.md")
|
|
16
|
+
@destination = destination
|
|
17
|
+
@filename = filename
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call
|
|
21
|
+
File.write(File.join(@destination, RULES_FILENAME), "#{ProcessRules.content}\n")
|
|
22
|
+
path = File.join(@destination, @filename)
|
|
23
|
+
existing = File.exist?(path) ? File.read(path) : ""
|
|
24
|
+
File.write(path, "#{merge(existing)}\n")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def block
|
|
28
|
+
<<~MARKDOWN.chomp
|
|
29
|
+
#{BEGIN_MARKER}
|
|
30
|
+
Read and follow this develop process for all work in this project. It is
|
|
31
|
+
also written verbatim to `#{RULES_FILENAME}` — reference that file directly.
|
|
32
|
+
|
|
33
|
+
#{ProcessRules.content}
|
|
34
|
+
#{END_MARKER}
|
|
35
|
+
MARKDOWN
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def merge(existing)
|
|
41
|
+
section = /#{Regexp.escape(BEGIN_MARKER)}.*?#{Regexp.escape(END_MARKER)}/m
|
|
42
|
+
return existing.sub(section, block) if existing.match?(section)
|
|
43
|
+
return block if existing.strip.empty?
|
|
44
|
+
|
|
45
|
+
"#{existing.chomp}\n\n#{block}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Develop Process
|
|
2
|
+
|
|
3
|
+
The standard process for writing code across all projects. Default to these rules
|
|
4
|
+
unless a project explicitly overrides them.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Diverging from this process
|
|
9
|
+
|
|
10
|
+
Read this process before starting work and follow it — it is the default for
|
|
11
|
+
every session. If a task genuinely calls for breaking one of these rules, do not
|
|
12
|
+
silently deviate: **PAUSE and ask for a one-time exception**, naming the rule and
|
|
13
|
+
why it should be set aside here. An exception is granted for that instance only —
|
|
14
|
+
it needs no doc or notes update — and then you continue. Do not treat a granted
|
|
15
|
+
exception as a standing change to the process.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Test-Driven Development
|
|
20
|
+
|
|
21
|
+
TDD is the default for everything. Work one tiny cycle at a time:
|
|
22
|
+
|
|
23
|
+
1. **Write one test that asserts one thing.**
|
|
24
|
+
2. **Run it and watch it fail** — for the right reason. A test you never saw fail
|
|
25
|
+
proves nothing.
|
|
26
|
+
3. **Write the minimum code to make it pass.**
|
|
27
|
+
4. **Run the test and watch it pass.**
|
|
28
|
+
5. **Commit.**
|
|
29
|
+
6. Repeat with the next test.
|
|
30
|
+
|
|
31
|
+
One assertion per test. One test per commit cycle. No batching multiple behaviors
|
|
32
|
+
into a single test or a single commit.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Commits
|
|
37
|
+
|
|
38
|
+
- A commit is normally **two files: the test file and the code file.**
|
|
39
|
+
- When implementing or updating an interface (e.g. a new controller endpoint) a
|
|
40
|
+
commit may touch more files (route + controller + view) — that is the minimal
|
|
41
|
+
coherent unit for that interface, and it is allowed.
|
|
42
|
+
- Keep each commit focused on the one behavior the test describes.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## What to Test
|
|
47
|
+
|
|
48
|
+
- **Test our own code only.**
|
|
49
|
+
- **Never test third-party code** — not a gem, not an API, not a framework. The
|
|
50
|
+
only test that may reference a dependency is one that asserts *our system is
|
|
51
|
+
correctly wired to it* (the integration seam), never the dependency's own
|
|
52
|
+
behavior.
|
|
53
|
+
- **Never test another interface inside a unit test.** A test covers one interface.
|
|
54
|
+
The single exception is the smoke integration test described below.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Smoke Integration Test
|
|
59
|
+
|
|
60
|
+
When implementing an interface, write **one smoke integration test** that exercises
|
|
61
|
+
the interface end to end and proves the pieces are connected. This is the one place
|
|
62
|
+
where touching more than the unit under test is expected and correct.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Pull Requests
|
|
67
|
+
|
|
68
|
+
- **Always work on a feature branch and open a PR.** Confirm the target branch
|
|
69
|
+
before any git operation (`git branch --show-current`).
|
|
70
|
+
- **Keep PRs small and manageable** — typically **no more than 8–10 files.**
|
|
71
|
+
- Keep the focus of a PR narrow. One concern per PR.
|
|
72
|
+
- **All tests pass before opening the PR.**
|
|
73
|
+
- **The linter and every other CI check pass before opening the PR.**
|
|
74
|
+
- Never start a new PR until the previous one is merged.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Code Quality
|
|
79
|
+
|
|
80
|
+
- Follow Clean Code principles: small functions, clear names, no surprises.
|
|
81
|
+
- Follow SOLID principles. Readable by a human first.
|
|
82
|
+
- Keep it simple — no abstraction until a real need calls for it.
|
|
83
|
+
- Explicitly require libraries rather than assuming autoload.
|
|
84
|
+
|
|
85
|
+
## Comments
|
|
86
|
+
|
|
87
|
+
- **Write self-documenting code, not comments.** Code should be clean and readable
|
|
88
|
+
on its own. Names — of classes, methods, variables, and partials — carry the intent.
|
|
89
|
+
- **A comment is a smell.** If you feel a comment is needed, the code is either built
|
|
90
|
+
wrong or needs refactoring (a clearer name, a smaller method, an extracted object or
|
|
91
|
+
partial) so the intent is obvious without prose. Follow SOLID and this resolves itself.
|
|
92
|
+
- Do not leave explanatory headers on classes/methods, inline "what this does" notes,
|
|
93
|
+
or section banners. Delete them and let the structure speak.
|
|
94
|
+
- Narrow exceptions, kept rare: a genuinely non-obvious *why* (a workaround for an
|
|
95
|
+
external bug, a legal/security constraint) and machine-readable annotations the
|
|
96
|
+
tooling requires (e.g. `rubocop:disable`). Prefer refactoring over a "why" comment
|
|
97
|
+
whenever you can.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TheLocal
|
|
4
|
+
# Loads the canonical develop-process rules the_local propagates into every
|
|
5
|
+
# host, so a host agent reads and follows one source of truth.
|
|
6
|
+
module ProcessRules
|
|
7
|
+
DIR = File.expand_path("process_rules", __dir__)
|
|
8
|
+
|
|
9
|
+
def self.content
|
|
10
|
+
read("develop_process_rules.md")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.read(name)
|
|
14
|
+
File.read(File.join(DIR, name)).chomp
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
require "the_local"
|
|
5
|
+
|
|
6
|
+
module TheLocal
|
|
7
|
+
# Minimal Railtie whose only job is to expose the `the_local:refresh` rake task
|
|
8
|
+
# to the host app. Registration deliberately does NOT use a Railtie (it happens
|
|
9
|
+
# at gem load — see the design plan); this is purely task exposure.
|
|
10
|
+
class Railtie < Rails::Railtie
|
|
11
|
+
rake_tasks do
|
|
12
|
+
load File.expand_path("tasks/the_local.rake", __dir__)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rake"
|
|
4
|
+
require "the_local"
|
|
5
|
+
require "the_local/builder"
|
|
6
|
+
|
|
7
|
+
# Gem-side build task. A provider adds `require "the_local/rake"` to its Rakefile
|
|
8
|
+
# (after loading the gem, so its locals are registered) and runs
|
|
9
|
+
# `rake the_local:build` to (re)render its committed .claude agent files from the
|
|
10
|
+
# registered definitions. Host apps don't use this — they install/refresh.
|
|
11
|
+
namespace :the_local do
|
|
12
|
+
desc "Render this provider's committed agent files from its registered definitions"
|
|
13
|
+
task :build do
|
|
14
|
+
written = TheLocal::Builder.new(registry: TheLocal.registry).call
|
|
15
|
+
puts "the_local: built #{written.length} agent file(s)"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc "Install/refresh this project's locals from the current bundle into .claude/agents/"
|
|
19
|
+
task :install do
|
|
20
|
+
allowed = TheLocal::Refresh.call(destination: Dir.pwd)
|
|
21
|
+
puts "the_local: installed locals for #{allowed.join(", ")}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
## TheLocal
|
|
2
|
+
|
|
3
|
+
> **DO NOT** explore the the_local gem source code. This reference is the
|
|
4
|
+
> complete user-facing API, embedded verbatim into every the_local local so
|
|
5
|
+
> their guidance never drifts. Keep it the single source of truth.
|
|
6
|
+
|
|
7
|
+
the_local is the engine that lets any gem or app ship resident Claude Code
|
|
8
|
+
expert subagents ("locals") that know its conventions. A provider gem registers
|
|
9
|
+
its locals once; the_local renders them to committed `.md` files and installs
|
|
10
|
+
the aggregated set from every directly-depended provider into a consuming app's
|
|
11
|
+
`.claude/agents/`, plus a delegation rule so the host's agent actually uses them.
|
|
12
|
+
|
|
13
|
+
### The model
|
|
14
|
+
|
|
15
|
+
- **Providers define locals.** A gem (or the app) calls `TheLocal.register` to
|
|
16
|
+
declare its locals; each `c.agent` becomes one local. The register block runs
|
|
17
|
+
only at build time, behind a soft `require "the_local"` guard so the gem still
|
|
18
|
+
works standalone.
|
|
19
|
+
- **`the_local:build` renders committed `.md`.** The provider runs
|
|
20
|
+
`rake the_local:build`; `TheLocal::Builder` writes each agent to its
|
|
21
|
+
`source_path` under `lib/<gem>/the_local/agents/<prefix>-<name>.md`. The
|
|
22
|
+
rendered files are committed to the provider's repo. **These committed files
|
|
23
|
+
are the contract** — they are what a host reads. The register block + `guide.md`
|
|
24
|
+
are the source of truth they're built from.
|
|
25
|
+
- **Install discovers committed `.md` on disk.** In a host, install reads each
|
|
26
|
+
direct dependency's committed `lib/**/the_local/agents/*.md` straight from its
|
|
27
|
+
gem path and copies them into `.claude/agents/` byte-for-byte — no provider
|
|
28
|
+
code is loaded and no register block runs in the host. Output depends only on
|
|
29
|
+
the provider gem version (a true carbon copy across every app), a provider needs
|
|
30
|
+
no install-time wiring to be found, and a fragile gem can't crash the install.
|
|
31
|
+
- **The delegation trigger.** Install also writes a registry-generated block into
|
|
32
|
+
the host's `CLAUDE.md`/`AGENTS.md` telling the host agent to delegate to these
|
|
33
|
+
locals. This is what makes delegation actually happen.
|
|
34
|
+
- **Direct-dependency scope.** Only the host's *direct* dependencies contribute
|
|
35
|
+
locals; transitive provider gems are filtered out, so a host gets exactly the
|
|
36
|
+
experts for the gems it chose.
|
|
37
|
+
|
|
38
|
+
### Install (in any gem or app)
|
|
39
|
+
|
|
40
|
+
1. Add the gem to the host's `Gemfile` (until it is on RubyGems, use a git
|
|
41
|
+
source: `gem "the_local", github: "DYB-Development/the_local"`), then
|
|
42
|
+
`bundle install`.
|
|
43
|
+
2. Run `bundle exec the_local install`. This syncs every direct provider's
|
|
44
|
+
committed locals into `.claude/agents/` and writes the delegation trigger
|
|
45
|
+
into `CLAUDE.md`/`AGENTS.md`. It needs no Rails — a plain gem installs the
|
|
46
|
+
same way an app does.
|
|
47
|
+
3. Re-run `bundle exec the_local install` after any bundle change (a provider
|
|
48
|
+
added, removed, or upgraded) to bring the host's locals back in sync. The
|
|
49
|
+
shell can automate this; the gem only exposes the command.
|
|
50
|
+
|
|
51
|
+
Rails apps can equivalently run `bin/rails g the_local:install` and
|
|
52
|
+
`the_local:refresh`; a gem that already wires `require "the_local/rake"` into
|
|
53
|
+
its Rakefile also gets `rake the_local:install`. All three share one engine.
|
|
54
|
+
|
|
55
|
+
### Author a provider (turn a gem into a provider)
|
|
56
|
+
|
|
57
|
+
1. Run `bin/rails g the_local:provider <gem_name>` (pass `--scope`,
|
|
58
|
+
`--prefix`, `--worker` as needed). It scaffolds `lib/<gem>/reference.rb`, a
|
|
59
|
+
`lib/<gem>/reference/guide.md`, and a `lib/<gem>/the_local.rb` companion that
|
|
60
|
+
registers the standard interface; hooks `the_local:build` into the `Rakefile`;
|
|
61
|
+
requires the companion from the gem entrypoint; and builds the committed
|
|
62
|
+
`.md` for review.
|
|
63
|
+
2. Write `guide.md` in this format — it is the single source of truth and is
|
|
64
|
+
embedded verbatim into every local. Document *your own* gem only: what it
|
|
65
|
+
does, how to install it, the conventions to enforce. Name companion gems but
|
|
66
|
+
do not explain their internals.
|
|
67
|
+
3. Tailor the register block bodies and `scope` to your gem; the standard
|
|
68
|
+
interface is `info` (read-only explainer), `install` (sets the gem up in a
|
|
69
|
+
host), and a domain worker (`develop` for libraries, `operate` for CLIs).
|
|
70
|
+
4. Run `rake the_local:build`, then **commit and ship**
|
|
71
|
+
`lib/<gem>/the_local/agents/*.md` (they must be in the gemspec's `files`).
|
|
72
|
+
This is the whole contract: a host discovers your locals by reading these
|
|
73
|
+
committed files from your gem on disk — it never loads your gem or runs your
|
|
74
|
+
register block — so if they aren't committed and shipped, you contribute
|
|
75
|
+
nothing, and if they are, you contribute everything. A drift test asserting
|
|
76
|
+
each committed file equals its `agent.to_markdown` keeps the artifact honest.
|
|
77
|
+
|
|
78
|
+
### TheLocal.register
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
TheLocal.register("my_gem", prefix: "my_gem", scope: "one-line domain phrase",
|
|
82
|
+
agents_dir: File.expand_path("the_local/agents", __dir__)) do |c|
|
|
83
|
+
c.agent "info",
|
|
84
|
+
description: "Use to learn what my_gem offers.",
|
|
85
|
+
tools: "Read",
|
|
86
|
+
body: "You explain my_gem, answering only from the reference. You make no changes.",
|
|
87
|
+
knowledge: MyGem::Reference.content
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- `gem_name` (first arg) filters to a host's direct dependencies.
|
|
92
|
+
- `prefix` is the agent filename namespace; defaults to the gem name.
|
|
93
|
+
- `scope` is a one-line domain phrase used to generate the delegation trigger.
|
|
94
|
+
- `agents_dir` is the absolute path to the committed `.md` files; each agent
|
|
95
|
+
records its `source_path` there so the installer can copy it verbatim.
|
|
96
|
+
|
|
97
|
+
### Conventions
|
|
98
|
+
|
|
99
|
+
- The register block lives behind `begin require "the_local" … rescue LoadError`
|
|
100
|
+
so the gem still works when the_local is absent.
|
|
101
|
+
- `guide.md` documents the providing gem only and stays the single source of
|
|
102
|
+
truth; never let a rendered `.md` drift from `agent.to_markdown`.
|
|
103
|
+
- Commit the rendered `.md`; never render in the host at install time.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TheLocal
|
|
4
|
+
# Loads the_local's own knowledge guide, embedded verbatim into its locals.
|
|
5
|
+
module Reference
|
|
6
|
+
DIR = File.expand_path("reference", __dir__)
|
|
7
|
+
|
|
8
|
+
def self.content
|
|
9
|
+
read("guide.md")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.read(name)
|
|
13
|
+
File.read(File.join(DIR, name)).chomp
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TheLocal
|
|
4
|
+
# Re-syncs a host's locals from its current bundle. Discovers providers by
|
|
5
|
+
# reading their committed agent files from each bundled gem's path on disk
|
|
6
|
+
# (see DiskProviders) — no gem code is loaded, so a fragile gem can't crash the
|
|
7
|
+
# install and a provider needs no register/require wiring at install time to
|
|
8
|
+
# contribute. Then gathers the direct and bundled gem names from a Bundler
|
|
9
|
+
# definition and runs a Sync. Both the provider discovery and the definition
|
|
10
|
+
# are injectable so the logic stays testable without a real bundle.
|
|
11
|
+
module Refresh
|
|
12
|
+
def self.call(destination:, definition: Bundler.definition,
|
|
13
|
+
load_providers: -> { DiskProviders.load(registry: TheLocal.registry, specs: specs_from(definition)) })
|
|
14
|
+
TheLocal.reset!
|
|
15
|
+
load_providers.call
|
|
16
|
+
Sync.new(
|
|
17
|
+
registry: TheLocal.registry,
|
|
18
|
+
destination: destination,
|
|
19
|
+
direct_dependencies: definition.dependencies.map(&:name),
|
|
20
|
+
bundled_gems: definition.specs.map(&:name)
|
|
21
|
+
).call
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.specs_from(definition)
|
|
25
|
+
definition.specs.map { |spec| { name: spec.name, path: spec.full_gem_path } }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TheLocal
|
|
4
|
+
# A registered provider (gem or app): its gem name, filename prefix, and a
|
|
5
|
+
# one-line scope used to generate the delegation trigger.
|
|
6
|
+
Provider = Data.define(:gem_name, :prefix, :scope)
|
|
7
|
+
|
|
8
|
+
# Accumulates the providers and agents contributed by everything that calls
|
|
9
|
+
# TheLocal.register. The install generator reads this to write .claude/agents/
|
|
10
|
+
# and the delegation trigger.
|
|
11
|
+
class Registry
|
|
12
|
+
def initialize
|
|
13
|
+
@agents = []
|
|
14
|
+
@providers = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :agents, :providers
|
|
18
|
+
|
|
19
|
+
def add(agent)
|
|
20
|
+
@agents << agent
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add_provider(provider)
|
|
24
|
+
@providers << provider
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def clear
|
|
28
|
+
@agents.clear
|
|
29
|
+
@providers.clear
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Yielded to a provider's register block. Turns each `agent` call into an
|
|
34
|
+
# Agent tagged with the providing gem and namespaced under its prefix.
|
|
35
|
+
class Collector
|
|
36
|
+
def initialize(gem_name, prefix, registry, agents_dir: nil)
|
|
37
|
+
@gem_name = gem_name
|
|
38
|
+
@prefix = prefix
|
|
39
|
+
@registry = registry
|
|
40
|
+
@agents_dir = agents_dir
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def agent(name, description:, tools:, body:, knowledge: nil)
|
|
44
|
+
@registry.add(
|
|
45
|
+
Agent.new(gem_name: @gem_name, prefix: @prefix, name: name,
|
|
46
|
+
description: description, tools: tools, body: body, knowledge: knowledge,
|
|
47
|
+
source_path: source_path_for(name))
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def source_path_for(name)
|
|
54
|
+
return nil unless @agents_dir
|
|
55
|
+
|
|
56
|
+
File.join(@agents_dir, "#{@prefix}-#{name}.md")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TheLocal
|
|
4
|
+
# Decides which providers' locals a host installs: its DIRECT dependencies plus
|
|
5
|
+
# the host project itself — never transitive dependencies. A registered
|
|
6
|
+
# provider is included when it is a direct dependency, or when it is not a
|
|
7
|
+
# bundled gem at all (which means it is the app registering its own locals).
|
|
8
|
+
module Scope
|
|
9
|
+
def self.allowed_gems(provider_gem_names:, direct_dependencies:, bundled_gems:)
|
|
10
|
+
provider_gem_names.uniq.select do |name|
|
|
11
|
+
direct_dependencies.include?(name) || !bundled_gems.include?(name)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TheLocal
|
|
4
|
+
# Orchestrates a full sync: resolves which gems are in scope (direct deps + the
|
|
5
|
+
# app), writes their agents, and writes the delegation trigger. Plain Ruby so
|
|
6
|
+
# both the install generator and the refresh rake task share one path. Returns
|
|
7
|
+
# the allowed gem names (for reporting).
|
|
8
|
+
class Sync
|
|
9
|
+
def initialize(registry:, destination:, direct_dependencies:, bundled_gems:)
|
|
10
|
+
@registry = registry
|
|
11
|
+
@destination = destination
|
|
12
|
+
@direct_dependencies = direct_dependencies
|
|
13
|
+
@bundled_gems = bundled_gems
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
allowed = Scope.allowed_gems(
|
|
18
|
+
provider_gem_names: @registry.providers.map(&:gem_name),
|
|
19
|
+
direct_dependencies: @direct_dependencies,
|
|
20
|
+
bundled_gems: @bundled_gems
|
|
21
|
+
)
|
|
22
|
+
Installer.new(registry: @registry, destination: @destination, allowed_gems: allowed).call
|
|
23
|
+
TriggerWriter.new(registry: @registry, destination: @destination, allowed_gems: allowed).call
|
|
24
|
+
ProcessDocWriter.new(destination: @destination).call
|
|
25
|
+
allowed
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :the_local do
|
|
4
|
+
desc "Re-sync installed locals from the current bundle"
|
|
5
|
+
task refresh: :environment do
|
|
6
|
+
allowed = TheLocal::Refresh.call(destination: Rails.root.to_s)
|
|
7
|
+
puts "the_local: refreshed locals for #{allowed.join(", ")}"
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: the_local-develop
|
|
3
|
+
description: Use PROACTIVELY to turn a gem into a the_local provider — scaffolding the companion, authoring the guide, and committing the rendered locals. MUST BE USED instead of wiring a provider by hand.
|
|
4
|
+
tools: Read, Write, Edit, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You turn a gem into a the_local provider following the reference's provider-author workflow: run `the_local:provider`, write guide.md as the single source of truth (your own gem only), tailor the register block, and hook the_local:build into the Rakefile. The deliverable is the committed, shipped lib/<gem>/the_local/agents/*.md — that is the whole contract a host reads from disk; a host never loads the gem, so unless those files are built, committed, and in the gemspec, the gem contributes nothing. You keep them in sync with agent.to_markdown.
|
|
8
|
+
|
|
9
|
+
## TheLocal
|
|
10
|
+
|
|
11
|
+
> **DO NOT** explore the the_local gem source code. This reference is the
|
|
12
|
+
> complete user-facing API, embedded verbatim into every the_local local so
|
|
13
|
+
> their guidance never drifts. Keep it the single source of truth.
|
|
14
|
+
|
|
15
|
+
the_local is the engine that lets any gem or app ship resident Claude Code
|
|
16
|
+
expert subagents ("locals") that know its conventions. A provider gem registers
|
|
17
|
+
its locals once; the_local renders them to committed `.md` files and installs
|
|
18
|
+
the aggregated set from every directly-depended provider into a consuming app's
|
|
19
|
+
`.claude/agents/`, plus a delegation rule so the host's agent actually uses them.
|
|
20
|
+
|
|
21
|
+
### The model
|
|
22
|
+
|
|
23
|
+
- **Providers define locals.** A gem (or the app) calls `TheLocal.register` to
|
|
24
|
+
declare its locals; each `c.agent` becomes one local. The register block runs
|
|
25
|
+
only at build time, behind a soft `require "the_local"` guard so the gem still
|
|
26
|
+
works standalone.
|
|
27
|
+
- **`the_local:build` renders committed `.md`.** The provider runs
|
|
28
|
+
`rake the_local:build`; `TheLocal::Builder` writes each agent to its
|
|
29
|
+
`source_path` under `lib/<gem>/the_local/agents/<prefix>-<name>.md`. The
|
|
30
|
+
rendered files are committed to the provider's repo. **These committed files
|
|
31
|
+
are the contract** — they are what a host reads. The register block + `guide.md`
|
|
32
|
+
are the source of truth they're built from.
|
|
33
|
+
- **Install discovers committed `.md` on disk.** In a host, install reads each
|
|
34
|
+
direct dependency's committed `lib/**/the_local/agents/*.md` straight from its
|
|
35
|
+
gem path and copies them into `.claude/agents/` byte-for-byte — no provider
|
|
36
|
+
code is loaded and no register block runs in the host. Output depends only on
|
|
37
|
+
the provider gem version (a true carbon copy across every app), a provider needs
|
|
38
|
+
no install-time wiring to be found, and a fragile gem can't crash the install.
|
|
39
|
+
- **The delegation trigger.** Install also writes a registry-generated block into
|
|
40
|
+
the host's `CLAUDE.md`/`AGENTS.md` telling the host agent to delegate to these
|
|
41
|
+
locals. This is what makes delegation actually happen.
|
|
42
|
+
- **Direct-dependency scope.** Only the host's *direct* dependencies contribute
|
|
43
|
+
locals; transitive provider gems are filtered out, so a host gets exactly the
|
|
44
|
+
experts for the gems it chose.
|
|
45
|
+
|
|
46
|
+
### Install (in any gem or app)
|
|
47
|
+
|
|
48
|
+
1. Add the gem to the host's `Gemfile` (until it is on RubyGems, use a git
|
|
49
|
+
source: `gem "the_local", github: "DYB-Development/the_local"`), then
|
|
50
|
+
`bundle install`.
|
|
51
|
+
2. Run `bundle exec the_local install`. This syncs every direct provider's
|
|
52
|
+
committed locals into `.claude/agents/` and writes the delegation trigger
|
|
53
|
+
into `CLAUDE.md`/`AGENTS.md`. It needs no Rails — a plain gem installs the
|
|
54
|
+
same way an app does.
|
|
55
|
+
3. Re-run `bundle exec the_local install` after any bundle change (a provider
|
|
56
|
+
added, removed, or upgraded) to bring the host's locals back in sync. The
|
|
57
|
+
shell can automate this; the gem only exposes the command.
|
|
58
|
+
|
|
59
|
+
Rails apps can equivalently run `bin/rails g the_local:install` and
|
|
60
|
+
`the_local:refresh`; a gem that already wires `require "the_local/rake"` into
|
|
61
|
+
its Rakefile also gets `rake the_local:install`. All three share one engine.
|
|
62
|
+
|
|
63
|
+
### Author a provider (turn a gem into a provider)
|
|
64
|
+
|
|
65
|
+
1. Run `bin/rails g the_local:provider <gem_name>` (pass `--scope`,
|
|
66
|
+
`--prefix`, `--worker` as needed). It scaffolds `lib/<gem>/reference.rb`, a
|
|
67
|
+
`lib/<gem>/reference/guide.md`, and a `lib/<gem>/the_local.rb` companion that
|
|
68
|
+
registers the standard interface; hooks `the_local:build` into the `Rakefile`;
|
|
69
|
+
requires the companion from the gem entrypoint; and builds the committed
|
|
70
|
+
`.md` for review.
|
|
71
|
+
2. Write `guide.md` in this format — it is the single source of truth and is
|
|
72
|
+
embedded verbatim into every local. Document *your own* gem only: what it
|
|
73
|
+
does, how to install it, the conventions to enforce. Name companion gems but
|
|
74
|
+
do not explain their internals.
|
|
75
|
+
3. Tailor the register block bodies and `scope` to your gem; the standard
|
|
76
|
+
interface is `info` (read-only explainer), `install` (sets the gem up in a
|
|
77
|
+
host), and a domain worker (`develop` for libraries, `operate` for CLIs).
|
|
78
|
+
4. Run `rake the_local:build`, then **commit and ship**
|
|
79
|
+
`lib/<gem>/the_local/agents/*.md` (they must be in the gemspec's `files`).
|
|
80
|
+
This is the whole contract: a host discovers your locals by reading these
|
|
81
|
+
committed files from your gem on disk — it never loads your gem or runs your
|
|
82
|
+
register block — so if they aren't committed and shipped, you contribute
|
|
83
|
+
nothing, and if they are, you contribute everything. A drift test asserting
|
|
84
|
+
each committed file equals its `agent.to_markdown` keeps the artifact honest.
|
|
85
|
+
|
|
86
|
+
### TheLocal.register
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
TheLocal.register("my_gem", prefix: "my_gem", scope: "one-line domain phrase",
|
|
90
|
+
agents_dir: File.expand_path("the_local/agents", __dir__)) do |c|
|
|
91
|
+
c.agent "info",
|
|
92
|
+
description: "Use to learn what my_gem offers.",
|
|
93
|
+
tools: "Read",
|
|
94
|
+
body: "You explain my_gem, answering only from the reference. You make no changes.",
|
|
95
|
+
knowledge: MyGem::Reference.content
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- `gem_name` (first arg) filters to a host's direct dependencies.
|
|
100
|
+
- `prefix` is the agent filename namespace; defaults to the gem name.
|
|
101
|
+
- `scope` is a one-line domain phrase used to generate the delegation trigger.
|
|
102
|
+
- `agents_dir` is the absolute path to the committed `.md` files; each agent
|
|
103
|
+
records its `source_path` there so the installer can copy it verbatim.
|
|
104
|
+
|
|
105
|
+
### Conventions
|
|
106
|
+
|
|
107
|
+
- The register block lives behind `begin require "the_local" … rescue LoadError`
|
|
108
|
+
so the gem still works when the_local is absent.
|
|
109
|
+
- `guide.md` documents the providing gem only and stays the single source of
|
|
110
|
+
truth; never let a rendered `.md` drift from `agent.to_markdown`.
|
|
111
|
+
- Commit the rendered `.md`; never render in the host at install time.
|