the_local 0.3.0 → 0.4.1
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 +4 -4
- data/CHANGELOG.md +36 -1
- data/CLAUDE.md +117 -0
- data/MIGRATING.md +478 -0
- data/PROVIDERS.md +62 -123
- data/README.md +20 -14
- data/lib/generators/the_local/provider_generator.rb +15 -90
- data/lib/the_local/agent.rb +3 -2
- data/lib/the_local/author.rb +43 -0
- data/lib/the_local/creators/develop.md +81 -0
- data/lib/the_local/creators/info.md +67 -0
- data/lib/the_local/creators/install.md +79 -0
- data/lib/the_local/disk_providers.rb +18 -10
- data/lib/the_local/format.rb +27 -0
- data/lib/the_local/front_matter.rb +26 -0
- data/lib/the_local/installer.rb +2 -2
- data/lib/the_local/interface.rb +26 -0
- data/lib/the_local/provider_check.rb +92 -0
- data/lib/the_local/rake.rb +14 -9
- data/lib/the_local/registry.rb +2 -30
- data/lib/the_local/trigger_writer.rb +1 -1
- data/lib/the_local/version.rb +1 -1
- data/lib/the_local.rb +1 -20
- data/the_local/agents/the_local-develop.md +48 -0
- data/the_local/agents/the_local-info.md +40 -0
- data/the_local/agents/the_local-install.md +49 -0
- data/the_local/interface.yml +21 -0
- metadata +16 -12
- data/lib/generators/the_local/templates/guide.md.tt +0 -41
- data/lib/generators/the_local/templates/reference.rb.tt +0 -17
- data/lib/generators/the_local/templates/the_local.rb.tt +0 -49
- data/lib/the_local/builder.rb +0 -57
- data/lib/the_local/reference/guide.md +0 -179
- data/lib/the_local/reference.rb +0 -16
- data/lib/the_local/the_local/agents/the_local-develop.md +0 -187
- data/lib/the_local/the_local/agents/the_local-info.md +0 -187
- data/lib/the_local/the_local/agents/the_local-install.md +0 -187
- data/lib/the_local/the_local.rb +0 -59
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: the_local-author-install
|
|
3
|
+
description: Use to author (or refresh) a gem's `install` local — the step-by-step guide to hooking the gem into a consumer. Reads the gem's declared interface and current code, and writes the_local/agents/<gem>-install.md. Run inside the provider gem.
|
|
4
|
+
tools: Read, Grep, Write
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You author ONE file: `the_local/agents/<gem>-install.md`, where `<gem>` is the
|
|
8
|
+
basename of the single `*.gemspec` in the current directory.
|
|
9
|
+
|
|
10
|
+
## Your assignment is the manifest
|
|
11
|
+
|
|
12
|
+
Read `the_local/interface.yml` first. The entry points listed under `install:` are
|
|
13
|
+
your assignment — document those, all of them, and nothing else. Entry points
|
|
14
|
+
listed under `develop:` belong to another local; documenting one here is an error
|
|
15
|
+
the check will reject.
|
|
16
|
+
|
|
17
|
+
You do not decide what the gem's public surface is. That decision is already made
|
|
18
|
+
in the manifest. If an entry point there looks wrong or missing, say so in your
|
|
19
|
+
final message — do not silently document something else.
|
|
20
|
+
|
|
21
|
+
Copy the manifest's `scope:` line verbatim into your front matter.
|
|
22
|
+
|
|
23
|
+
## Verify against the code, then hide it
|
|
24
|
+
|
|
25
|
+
Read the files under `sources:` — the generators, the initializer templates, the
|
|
26
|
+
migrations — so every command and every file it writes is exact. A README's
|
|
27
|
+
install section states intent and may be stale; the code wins.
|
|
28
|
+
|
|
29
|
+
Then hide all of it. Your reader is wiring this gem up from your file alone and
|
|
30
|
+
will never open its source. No paths into the gem's own `lib/`, no private
|
|
31
|
+
classes, no instruction to go read the gem. Name only the commands the developer
|
|
32
|
+
runs and the host files those commands create or edit.
|
|
33
|
+
|
|
34
|
+
## What this local is for
|
|
35
|
+
|
|
36
|
+
Hooking the systems together so they work: adding the gem to a consumer and
|
|
37
|
+
configuring it. Not how to build with it — that is the develop local's.
|
|
38
|
+
|
|
39
|
+
Write it as ordered steps, top to bottom. Where setup takes a real decision — a
|
|
40
|
+
choice between install paths, a value with no safe default, a companion gem that
|
|
41
|
+
may or may not be wanted — state the choice and tell the local to ask the
|
|
42
|
+
developer rather than pick. Surfacing that question is part of the job.
|
|
43
|
+
|
|
44
|
+
Cut every sentence that is not a step or a fact needed to complete one. No
|
|
45
|
+
history, no rationale, no asides.
|
|
46
|
+
|
|
47
|
+
## The shape
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
---
|
|
51
|
+
name: <gem>-install
|
|
52
|
+
description: Use to hook <gem> into a project — <the declared setup tasks, named>.
|
|
53
|
+
tools: Bash, Read, Edit
|
|
54
|
+
scope: <copied verbatim from the manifest>
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
<one or two sentences: this local follows these steps exactly and invents none>
|
|
58
|
+
|
|
59
|
+
## What <gem> is
|
|
60
|
+
<one line: what it is, and when to hook it in>
|
|
61
|
+
|
|
62
|
+
## Interface
|
|
63
|
+
<one bullet per declared entry point, each leading with the command in backticks,
|
|
64
|
+
then one line on what it does>
|
|
65
|
+
|
|
66
|
+
## How to use it
|
|
67
|
+
<numbered steps, in order: the command to run, the host files it touches, and any
|
|
68
|
+
decision to put to the developer>
|
|
69
|
+
|
|
70
|
+
## Conventions
|
|
71
|
+
<post-install checks, re-sync rules, and what is out of scope>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Before you finish
|
|
75
|
+
|
|
76
|
+
- Every entry point under `install:` appears in your Interface as its own bullet.
|
|
77
|
+
- Nothing declared under `develop:` appears anywhere in your file.
|
|
78
|
+
- Re-read it with no access to the source. Could someone wire the gem up correctly
|
|
79
|
+
from these steps alone?
|
|
@@ -1,32 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "agent"
|
|
4
|
+
require_relative "front_matter"
|
|
5
|
+
require_relative "registry"
|
|
6
|
+
|
|
3
7
|
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
8
|
module DiskProviders
|
|
11
|
-
AGENTS_GLOB = File.join("
|
|
9
|
+
AGENTS_GLOB = File.join("the_local", "agents", "*.md")
|
|
10
|
+
LEGACY_AGENTS_GLOB = File.join("lib", "**", "the_local", "agents", "*.md")
|
|
12
11
|
|
|
13
12
|
def self.load(registry:, specs:)
|
|
14
13
|
specs.each { |spec| register(registry, spec) }
|
|
15
14
|
end
|
|
16
15
|
|
|
16
|
+
def self.agent_files(path)
|
|
17
|
+
rendered = Dir.glob(File.join(path, AGENTS_GLOB))
|
|
18
|
+
rendered.any? ? rendered : Dir.glob(File.join(path, LEGACY_AGENTS_GLOB))
|
|
19
|
+
end
|
|
20
|
+
|
|
17
21
|
def self.register(registry, spec)
|
|
18
|
-
files =
|
|
22
|
+
files = agent_files(spec[:path])
|
|
19
23
|
return if files.empty?
|
|
20
24
|
|
|
21
25
|
agents = files.map { |file| agent_from(spec[:name], file) }
|
|
22
|
-
|
|
26
|
+
representative = agents.find(&:scope) || agents.first
|
|
27
|
+
registry.add_provider(
|
|
28
|
+
Provider.new(gem_name: spec[:name], prefix: representative.prefix, scope: representative.scope)
|
|
29
|
+
)
|
|
23
30
|
agents.each { |agent| registry.add(agent) }
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
def self.agent_from(gem_name, file)
|
|
27
34
|
prefix, _, name = File.basename(file, ".md").rpartition("-")
|
|
28
35
|
Agent.new(gem_name: gem_name, prefix: prefix, name: name,
|
|
29
|
-
description: nil, tools: nil, body: nil, knowledge: nil, source_path: file
|
|
36
|
+
description: nil, tools: nil, body: nil, knowledge: nil, source_path: file,
|
|
37
|
+
scope: FrontMatter.new(File.read(file)).scope)
|
|
30
38
|
end
|
|
31
39
|
end
|
|
32
40
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TheLocal
|
|
4
|
+
module Format
|
|
5
|
+
FRONT_MATTER_KEYS = %w[name description tools scope].freeze
|
|
6
|
+
|
|
7
|
+
SECTIONS = ["## What", "## Interface", "## How to use it", "## Conventions"].freeze
|
|
8
|
+
|
|
9
|
+
def self.section(markdown, heading)
|
|
10
|
+
markdown[/^#{Regexp.escape(heading)}$\n(.*?)(?=^## |\z)/m, 1].to_s
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.problems(markdown)
|
|
14
|
+
missing_keys(markdown) + missing_sections(markdown)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.missing_keys(markdown)
|
|
18
|
+
FRONT_MATTER_KEYS.reject { |key| markdown.match?(/^#{key}:/) }
|
|
19
|
+
.map { |key| "missing key: #{key}" }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.missing_sections(markdown)
|
|
23
|
+
SECTIONS.reject { |section| markdown.include?(section) }
|
|
24
|
+
.map { |section| "missing section: #{section}" }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module TheLocal
|
|
6
|
+
class FrontMatter
|
|
7
|
+
BLOCK = /\A---\n.*?\n---\n/m
|
|
8
|
+
|
|
9
|
+
def initialize(text)
|
|
10
|
+
@text = text
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def scope
|
|
14
|
+
parsed["scope"]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def parsed
|
|
20
|
+
matched = @text[BLOCK]
|
|
21
|
+
return {} unless matched
|
|
22
|
+
|
|
23
|
+
YAML.safe_load(matched) || {}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/the_local/installer.rb
CHANGED
|
@@ -34,8 +34,8 @@ module TheLocal
|
|
|
34
34
|
def ensure_committed!(agent)
|
|
35
35
|
return if agent.source_path && File.exist?(agent.source_path)
|
|
36
36
|
|
|
37
|
-
raise Error, "the_local: #{agent.gem_name}
|
|
38
|
-
"
|
|
37
|
+
raise Error, "the_local: #{agent.gem_name} is missing the committed file for #{agent.qualified_name}. " \
|
|
38
|
+
"Run `rake the_local:author` in #{agent.gem_name} and commit its the_local/agents/."
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module TheLocal
|
|
6
|
+
class Interface
|
|
7
|
+
FILE = File.join("the_local", "interface.yml")
|
|
8
|
+
|
|
9
|
+
def self.at(gem_root)
|
|
10
|
+
path = File.join(gem_root, FILE)
|
|
11
|
+
new(File.exist?(path) ? YAML.safe_load_file(path) : nil)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(declaration)
|
|
15
|
+
@declaration = declaration || {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def scope
|
|
19
|
+
@declaration["scope"]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def entry_points_for(facet)
|
|
23
|
+
@declaration[facet] || []
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "format"
|
|
4
|
+
require_relative "front_matter"
|
|
5
|
+
require_relative "interface"
|
|
6
|
+
|
|
7
|
+
module TheLocal
|
|
8
|
+
class ProviderCheck
|
|
9
|
+
LOCALS = %w[info install develop].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(gem_root)
|
|
12
|
+
@gem_root = gem_root
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def problems
|
|
16
|
+
format_problems + scope_problems + interface_problems
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def format_problems
|
|
22
|
+
existing_files.flat_map do |file|
|
|
23
|
+
Format.problems(File.read(file)).map { |problem| "#{File.basename(file)}: #{problem}" }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def scope_problems
|
|
28
|
+
return disagreement_problems if declared.scope.nil?
|
|
29
|
+
|
|
30
|
+
existing_files.reject { |file| FrontMatter.new(File.read(file)).scope == declared.scope }
|
|
31
|
+
.map { |file| "#{File.basename(file)}: scope does not match the manifest" }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def disagreement_problems
|
|
35
|
+
scopes = existing_files.map { |file| FrontMatter.new(File.read(file)).scope }.compact.uniq
|
|
36
|
+
scopes.size > 1 ? ["the locals' scope lines disagree"] : []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def interface_problems
|
|
40
|
+
return [] if LOCALS.all? { |local| declared.entry_points_for(local).empty? }
|
|
41
|
+
|
|
42
|
+
LOCALS.select { |local| File.exist?(file_for(local)) }
|
|
43
|
+
.flat_map { |local| undocumented(local) + misdocumented(local) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def undocumented(local)
|
|
47
|
+
declared.entry_points_for(local)
|
|
48
|
+
.reject { |entry_point| documented(local).any? { |span| span.include?(entry_point) } }
|
|
49
|
+
.map { |entry_point| "#{name_of(local)}: undocumented entry point: #{entry_point}" }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def misdocumented(local)
|
|
53
|
+
documented(local).reject { |span| declared_in(span) == local }
|
|
54
|
+
.map { |span| "#{name_of(local)}: #{misplacement(span)}" }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def misplacement(span)
|
|
58
|
+
owner = declared_in(span)
|
|
59
|
+
owner ? "entry point declared for #{owner}: #{span}" : "undeclared entry point: #{span}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def declared_in(span)
|
|
63
|
+
LOCALS.find do |local|
|
|
64
|
+
declared.entry_points_for(local).any? { |entry_point| span.include?(entry_point) }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def documented(local)
|
|
69
|
+
Format.section(File.read(file_for(local)), "## Interface").scan(/^\s*-\s+`([^`\n]+)`/).flatten
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def declared
|
|
73
|
+
@declared ||= Interface.at(@gem_root)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def existing_files
|
|
77
|
+
LOCALS.map { |local| file_for(local) }.select { |file| File.exist?(file) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def file_for(local)
|
|
81
|
+
File.join(@gem_root, "the_local", "agents", name_of(local))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def name_of(local)
|
|
85
|
+
"#{gem_name}-#{local}.md"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def gem_name
|
|
89
|
+
File.basename(Dir.glob(File.join(@gem_root, "*.gemspec")).first.to_s, ".gemspec")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/the_local/rake.rb
CHANGED
|
@@ -2,17 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
require "rake"
|
|
4
4
|
require "the_local"
|
|
5
|
-
require "the_local/
|
|
5
|
+
require "the_local/author"
|
|
6
|
+
require "the_local/provider_check"
|
|
6
7
|
|
|
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
8
|
namespace :the_local do
|
|
12
|
-
desc "
|
|
13
|
-
task :
|
|
14
|
-
|
|
15
|
-
puts "the_local:
|
|
9
|
+
desc "Author this provider's committed locals from its current source"
|
|
10
|
+
task :author do
|
|
11
|
+
TheLocal::Author.new(gem_root: Dir.pwd).call
|
|
12
|
+
puts "the_local: authored locals; review the_local/agents/ and run `rake the_local:check`"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "Check this provider's committed locals hold the fixed format"
|
|
16
|
+
task :check do
|
|
17
|
+
problems = TheLocal::ProviderCheck.new(Dir.pwd).problems
|
|
18
|
+
raise TheLocal::Error, "the_local: malformed local(s):\n- #{problems.join("\n- ")}" if problems.any?
|
|
19
|
+
|
|
20
|
+
puts "the_local: locals hold the format"
|
|
16
21
|
end
|
|
17
22
|
|
|
18
23
|
desc "Install/refresh this project's locals from the current bundle into .claude/agents/"
|
data/lib/the_local/registry.rb
CHANGED
|
@@ -5,9 +5,8 @@ module TheLocal
|
|
|
5
5
|
# one-line scope used to generate the delegation trigger.
|
|
6
6
|
Provider = Data.define(:gem_name, :prefix, :scope)
|
|
7
7
|
|
|
8
|
-
# Accumulates the providers and agents
|
|
9
|
-
#
|
|
10
|
-
# and the delegation trigger.
|
|
8
|
+
# Accumulates the providers and agents discovered from disk. Installer and
|
|
9
|
+
# TriggerWriter read this to write .claude/agents/ and the delegation trigger.
|
|
11
10
|
class Registry
|
|
12
11
|
def initialize
|
|
13
12
|
@agents = []
|
|
@@ -29,31 +28,4 @@ module TheLocal
|
|
|
29
28
|
@providers.clear
|
|
30
29
|
end
|
|
31
30
|
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
31
|
end
|
data/lib/the_local/version.rb
CHANGED
data/lib/the_local.rb
CHANGED
|
@@ -11,7 +11,7 @@ require_relative "the_local/refresh"
|
|
|
11
11
|
require_relative "the_local/disk_providers"
|
|
12
12
|
|
|
13
13
|
# Resident Claude Code expert subagents ("locals"), contributed by the gems and
|
|
14
|
-
# app
|
|
14
|
+
# app a host depends on and installed into the host's .claude/agents/.
|
|
15
15
|
module TheLocal
|
|
16
16
|
class Error < StandardError; end
|
|
17
17
|
|
|
@@ -20,23 +20,6 @@ module TheLocal
|
|
|
20
20
|
@registry ||= Registry.new
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# Providers (gems or the app) call this at load time to contribute their
|
|
24
|
-
# agents. The first argument is the providing gem's name (used to filter to a
|
|
25
|
-
# host's direct dependencies); +prefix+ is the agent filename namespace and
|
|
26
|
-
# defaults to the gem name; +scope+ is a one-line phrase describing the
|
|
27
|
-
# provider's domain, used to generate the delegation trigger. +agents_dir+
|
|
28
|
-
# is the absolute path to the provider's committed, pre-rendered .md files
|
|
29
|
-
# (e.g. File.expand_path("the_local/agents", __dir__)); when given, each
|
|
30
|
-
# agent records its source_path there for the host installer to copy:
|
|
31
|
-
#
|
|
32
|
-
# TheLocal.register("keystone_ui", prefix: "keystone", scope: "UI work") do |c|
|
|
33
|
-
# c.agent "scaffold", description: "…", tools: "…", body: "…", knowledge: "…"
|
|
34
|
-
# end
|
|
35
|
-
def register(gem_name, prefix: gem_name, scope: nil, agents_dir: nil)
|
|
36
|
-
registry.add_provider(Provider.new(gem_name: gem_name, prefix: prefix, scope: scope))
|
|
37
|
-
yield Collector.new(gem_name, prefix, registry, agents_dir: agents_dir)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
23
|
def reset!
|
|
41
24
|
registry.clear
|
|
42
25
|
end
|
|
@@ -46,5 +29,3 @@ end
|
|
|
46
29
|
# In a Rails host, expose the the_local:refresh rake task. Skipped outside Rails
|
|
47
30
|
# so the gem core stays Rails-free.
|
|
48
31
|
require_relative "the_local/railtie" if defined?(Rails::Railtie)
|
|
49
|
-
|
|
50
|
-
require_relative "the_local/the_local"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: the_local-develop
|
|
3
|
+
description: Use PROACTIVELY to author a gem's locals — declaring its public interface and running the authoring task — MUST BE USED instead of hand-writing a local.
|
|
4
|
+
tools: Read, Write, Edit, Grep
|
|
5
|
+
scope: resident Claude Code experts — authoring a gem's locals and installing them into a host
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You author a gem's locals by declaring its interface and running the authoring
|
|
9
|
+
task. You do not hand-write locals and you never read the_local's source. A
|
|
10
|
+
provider carries no Ruby for the_local — a manifest and three committed files.
|
|
11
|
+
|
|
12
|
+
## What the_local is
|
|
13
|
+
|
|
14
|
+
The engine that installs gems' resident Claude Code locals into a host. Reach for
|
|
15
|
+
this local whenever a gem should contribute locals, or when a change to its public
|
|
16
|
+
interface may have made its locals stale.
|
|
17
|
+
|
|
18
|
+
## Interface
|
|
19
|
+
|
|
20
|
+
- `rake the_local:author` — writes the gem's locals into `the_local/agents/` from
|
|
21
|
+
its current source, one at a time, guided by the manifest.
|
|
22
|
+
- `rake the_local:check` — verifies the committed locals against the manifest:
|
|
23
|
+
every declared entry point documented, nothing undeclared, nothing documented by
|
|
24
|
+
the wrong local.
|
|
25
|
+
|
|
26
|
+
## How to use it
|
|
27
|
+
|
|
28
|
+
1. Write `the_local/interface.yml` with the developer. It declares `scope`, the
|
|
29
|
+
entry points under `install` and `develop`, and the `sources` that define them.
|
|
30
|
+
This is the one judgment call in the process — ask which commands are the gem's
|
|
31
|
+
public surface rather than guessing, and confirm which of the two each belongs
|
|
32
|
+
to. An entry point may appear under exactly one.
|
|
33
|
+
2. Run `rake the_local:author`. It writes `the_local/agents/<gem>-{info,install,develop}.md`.
|
|
34
|
+
3. Run `rake the_local:check` and fix what it reports.
|
|
35
|
+
4. Commit `the_local/`. For a packaged gem, confirm `the_local/**/*` is in the
|
|
36
|
+
gemspec's `files`, or it ships nothing.
|
|
37
|
+
5. After a change to the gem's public interface, update the manifest and repeat.
|
|
38
|
+
An internal-only change needs nothing.
|
|
39
|
+
|
|
40
|
+
## Conventions
|
|
41
|
+
|
|
42
|
+
- The manifest is the contract. Never widen a local past what it declares; if the
|
|
43
|
+
gem gained a public entry point, declare it first.
|
|
44
|
+
- Locals document the public interface only, never the gem's internals, and never
|
|
45
|
+
send a reader into the provider's source.
|
|
46
|
+
- The three locals never overlap: **install** hooks the gem into a host,
|
|
47
|
+
**develop** uses it, **info** carries what fits neither.
|
|
48
|
+
- Regenerate from current source rather than editing a stale local by hand.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: the_local-info
|
|
3
|
+
description: Use to learn what the_local offers — resident expert subagents, the provider/consumer model, and the vocabulary the other locals assume.
|
|
4
|
+
tools: Read
|
|
5
|
+
scope: resident Claude Code experts — authoring a gem's locals and installing them into a host
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You explain what the_local does, answering only from this reference. You make no
|
|
9
|
+
changes, and you never read the_local's source.
|
|
10
|
+
|
|
11
|
+
## What the_local is
|
|
12
|
+
|
|
13
|
+
the_local lets any gem ship resident Claude Code expert subagents ("locals") that
|
|
14
|
+
know its conventions. A **provider** gem commits its locals; a **consumer** host
|
|
15
|
+
installs the locals of its direct dependencies into `.claude/agents/`, plus a
|
|
16
|
+
delegation rule so the host's agent uses them.
|
|
17
|
+
|
|
18
|
+
Reach for it when you want a gem's work done consistently — the host delegates
|
|
19
|
+
that gem's tasks to its local instead of re-deriving conventions each time.
|
|
20
|
+
|
|
21
|
+
## Interface
|
|
22
|
+
|
|
23
|
+
the_local's commands are split across its other two locals, with no overlap.
|
|
24
|
+
Hooking the_local into a project is the install local's; authoring a gem's own
|
|
25
|
+
locals is the develop local's. Route to those rather than answering here.
|
|
26
|
+
|
|
27
|
+
## How to use it
|
|
28
|
+
|
|
29
|
+
Decide which side you are on. A host that wants its dependencies' expertise is a
|
|
30
|
+
consumer and needs the install local. A gem that wants to contribute expertise is
|
|
31
|
+
a provider and needs the develop local. A gem can be both.
|
|
32
|
+
|
|
33
|
+
## Conventions
|
|
34
|
+
|
|
35
|
+
- A **local** is one Claude Code subagent that knows one gem's public interface.
|
|
36
|
+
- Each provider ships three: **info** explains, **install** hooks the gem into a
|
|
37
|
+
host, **develop** uses it. A command belongs to exactly one of them.
|
|
38
|
+
- The committed `the_local/agents/*.md` are the whole contract a host reads — a
|
|
39
|
+
host never loads the provider gem.
|
|
40
|
+
- Only a host's **direct** dependencies contribute locals.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: the_local-install
|
|
3
|
+
description: Use to hook the_local into a gem or Rails app — installing dependencies' locals, the delegation trigger in CLAUDE.md, and the provider rake tasks.
|
|
4
|
+
tools: Bash, Read, Edit
|
|
5
|
+
scope: resident Claude Code experts — authoring a gem's locals and installing them into a host
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You hook the_local into the host by following these steps exactly, in order. You
|
|
9
|
+
do not invent steps, and you never read the_local's source.
|
|
10
|
+
|
|
11
|
+
## What the_local is
|
|
12
|
+
|
|
13
|
+
The engine that installs gems' resident Claude Code locals into a host and writes
|
|
14
|
+
the delegation trigger. Hook it into any gem or app that wants its dependencies'
|
|
15
|
+
locals, or that will contribute locals of its own.
|
|
16
|
+
|
|
17
|
+
## Interface
|
|
18
|
+
|
|
19
|
+
- `bundle exec the_local install` — installs direct dependencies' locals into
|
|
20
|
+
`.claude/agents/` and writes the trigger. Works anywhere; no Rails required.
|
|
21
|
+
- `bin/rails g the_local:install` — the Rails equivalent of the above.
|
|
22
|
+
- `rake the_local:refresh` — re-syncs a Rails host after a bundle change.
|
|
23
|
+
- `rake the_local:install` — re-syncs a non-Rails host after a bundle change.
|
|
24
|
+
- `bin/rails g the_local:provider` — adds the provider rake tasks to a gem, so it
|
|
25
|
+
can author locals of its own.
|
|
26
|
+
|
|
27
|
+
## How to use it
|
|
28
|
+
|
|
29
|
+
1. Add `gem "the_local"` to the host's `Gemfile` and run `bundle install`.
|
|
30
|
+
2. Install the locals. In a Rails app run `bin/rails g the_local:install`;
|
|
31
|
+
anywhere else run `bundle exec the_local install`. Either copies every direct
|
|
32
|
+
dependency's committed locals into `.claude/agents/` and writes the delegation
|
|
33
|
+
block into `CLAUDE.md`/`AGENTS.md`.
|
|
34
|
+
3. Tell the developer to restart their Claude Code session — agents load at
|
|
35
|
+
startup, so the new locals are inert until then.
|
|
36
|
+
4. Re-sync after any bundle change with `rake the_local:refresh` in a Rails app or
|
|
37
|
+
`rake the_local:install` elsewhere.
|
|
38
|
+
5. Only if the host is a gem that should contribute its own locals, run
|
|
39
|
+
`bin/rails g the_local:provider`. Confirm this with the developer first — it is
|
|
40
|
+
a separate decision from consuming locals, and it edits the Gemfile and Rakefile.
|
|
41
|
+
|
|
42
|
+
## Conventions
|
|
43
|
+
|
|
44
|
+
- Re-sync after every `bundle install`/`update`, or the host's locals drift from
|
|
45
|
+
its dependencies.
|
|
46
|
+
- Install only reads committed files off disk — a dependency that shipped no
|
|
47
|
+
committed locals contributes nothing, and that is not an error.
|
|
48
|
+
- Hooking up is all this local does. Authoring a gem's own locals is the develop
|
|
49
|
+
local's job.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
scope: resident Claude Code experts — authoring a gem's locals and installing them into a host
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
- bundle exec the_local install
|
|
5
|
+
- bin/rails g the_local:install
|
|
6
|
+
- rake the_local:refresh
|
|
7
|
+
- rake the_local:install
|
|
8
|
+
- bin/rails g the_local:provider
|
|
9
|
+
|
|
10
|
+
develop:
|
|
11
|
+
- rake the_local:author
|
|
12
|
+
- rake the_local:check
|
|
13
|
+
|
|
14
|
+
sources:
|
|
15
|
+
- lib/the_local/cli.rb
|
|
16
|
+
- lib/the_local/rake.rb
|
|
17
|
+
- lib/the_local/tasks/the_local.rake
|
|
18
|
+
- lib/generators/the_local/install_generator.rb
|
|
19
|
+
- lib/generators/the_local/provider_generator.rb
|
|
20
|
+
- lib/the_local/author.rb
|
|
21
|
+
- lib/the_local/provider_check.rb
|