clawthor 0.3.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/LICENSE +21 -0
- data/README.md +365 -0
- data/exe/clawthor +8 -0
- data/lib/clawthor/cli.rb +140 -0
- data/lib/clawthor/compiler.rb +1258 -0
- data/lib/clawthor/dsl.rb +95 -0
- data/lib/clawthor/orchestrator.rb +35 -0
- data/lib/clawthor/primitives/agent.rb +77 -0
- data/lib/clawthor/primitives/command.rb +20 -0
- data/lib/clawthor/primitives/hook.rb +50 -0
- data/lib/clawthor/primitives/module.rb +75 -0
- data/lib/clawthor/primitives/service.rb +80 -0
- data/lib/clawthor/primitives/skill.rb +135 -0
- data/lib/clawthor/primitives/task.rb +60 -0
- data/lib/clawthor/primitives/workspace.rb +71 -0
- data/lib/clawthor/registry.rb +38 -0
- data/lib/clawthor/version.rb +5 -0
- data/lib/clawthor.rb +38 -0
- metadata +92 -0
data/lib/clawthor/dsl.rb
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clawthor
|
|
4
|
+
module DSL
|
|
5
|
+
# ─── Top-level DSL methods ─────────────────────────────────
|
|
6
|
+
# These are mixed into the scope where the DSL file is evaluated.
|
|
7
|
+
|
|
8
|
+
module Methods
|
|
9
|
+
def workspace(name, &block)
|
|
10
|
+
ws = Workspace.new(name)
|
|
11
|
+
ws.instance_eval(&block) if block
|
|
12
|
+
Clawthor::DSL::REGISTRY.set_workspace(ws)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def skill(name, &block)
|
|
16
|
+
s = Skill.new(name)
|
|
17
|
+
s.instance_eval(&block) if block
|
|
18
|
+
Clawthor::DSL::REGISTRY.skills[name] = s
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def hook(name, &block)
|
|
22
|
+
h = Hook.new(name)
|
|
23
|
+
h.instance_eval(&block) if block
|
|
24
|
+
Clawthor::DSL::REGISTRY.hooks[name] = h
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def agent(name, &block)
|
|
28
|
+
a = Agent.new(name)
|
|
29
|
+
a.instance_eval(&block) if block
|
|
30
|
+
Clawthor::DSL::REGISTRY.agents[name] = a
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def task(name, &block)
|
|
34
|
+
t = Task.new(name)
|
|
35
|
+
t.instance_eval(&block) if block
|
|
36
|
+
Clawthor::DSL::REGISTRY.tasks[name] = t
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def command(name, &block)
|
|
40
|
+
c = Command.new(name)
|
|
41
|
+
c.instance_eval(&block) if block
|
|
42
|
+
Clawthor::DSL::REGISTRY.commands[name] = c
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def service(name, &block)
|
|
46
|
+
s = Service.new(name)
|
|
47
|
+
s.instance_eval(&block) if block
|
|
48
|
+
Clawthor::DSL::REGISTRY.services[name] = s
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# ─── Module support ──────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
# Define a reusable module. The block receives the module-scoped
|
|
54
|
+
# DSL (skill, hook, agent, command, task, service).
|
|
55
|
+
def define_module(name, &block)
|
|
56
|
+
mod = Clawthor::DSL::Module.new(name)
|
|
57
|
+
mod.instance_eval(&block) if block
|
|
58
|
+
Clawthor::DSL::REGISTRY.modules[name] = mod
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Include a module's primitives into the top-level registry.
|
|
62
|
+
# The module must have been defined (via define_module) or
|
|
63
|
+
# loaded (via require_module) before this point.
|
|
64
|
+
def use(name)
|
|
65
|
+
mod = Clawthor::DSL::REGISTRY.modules[name]
|
|
66
|
+
raise "Module :#{name} not found. Define it with define_module or load it with require_module." unless mod
|
|
67
|
+
Clawthor::DSL::REGISTRY.merge_module!(mod)
|
|
68
|
+
puts " [module] :#{name} included (#{mod_summary(mod)})"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Load a module definition from an external file, then include it.
|
|
72
|
+
# This lets you ship reusable modules as separate .rb files.
|
|
73
|
+
def require_module(path)
|
|
74
|
+
resolved = File.expand_path(path)
|
|
75
|
+
raise "Module file not found: #{resolved}" unless File.exist?(resolved)
|
|
76
|
+
|
|
77
|
+
# Evaluate the file in our DSL context so define_module works
|
|
78
|
+
instance_eval(File.read(resolved), resolved)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def mod_summary(mod)
|
|
84
|
+
parts = []
|
|
85
|
+
parts << "#{mod.skills.length} skills" unless mod.skills.empty?
|
|
86
|
+
parts << "#{mod.hooks.length} hooks" unless mod.hooks.empty?
|
|
87
|
+
parts << "#{mod.agents.length} agents" unless mod.agents.empty?
|
|
88
|
+
parts << "#{mod.commands.length} commands" unless mod.commands.empty?
|
|
89
|
+
parts << "#{mod.tasks.length} tasks" unless mod.tasks.empty?
|
|
90
|
+
parts << "#{mod.services.length} services" unless mod.services.empty?
|
|
91
|
+
parts.empty? ? "empty" : parts.join(", ")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# ═══════════════════════════════════════════════════════════════
|
|
5
|
+
# Claude Code DSL - Orchestration Layer
|
|
6
|
+
#
|
|
7
|
+
# Write declarations → run compiler → get a working plugin.
|
|
8
|
+
#
|
|
9
|
+
# Primitives:
|
|
10
|
+
# skill — knowledge unit with progressive disclosure
|
|
11
|
+
# hook — lifecycle interceptor
|
|
12
|
+
# agent — specialised sub-instance with a contract
|
|
13
|
+
# task — persistent working memory template
|
|
14
|
+
# command — prompt expansion template
|
|
15
|
+
# service — managed background process
|
|
16
|
+
# ═══════════════════════════════════════════════════════════════
|
|
17
|
+
|
|
18
|
+
module Clawthor
|
|
19
|
+
module DSL
|
|
20
|
+
# Require all primitive classes
|
|
21
|
+
require_relative "registry"
|
|
22
|
+
require_relative "primitives/workspace"
|
|
23
|
+
require_relative "primitives/skill"
|
|
24
|
+
require_relative "primitives/hook"
|
|
25
|
+
require_relative "primitives/agent"
|
|
26
|
+
require_relative "primitives/task"
|
|
27
|
+
require_relative "primitives/command"
|
|
28
|
+
require_relative "primitives/service"
|
|
29
|
+
require_relative "primitives/module"
|
|
30
|
+
require_relative "dsl"
|
|
31
|
+
|
|
32
|
+
# Create global registry instance
|
|
33
|
+
REGISTRY = Registry.new
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clawthor; module DSL
|
|
4
|
+
# ─── Agent ─────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
class Agent
|
|
7
|
+
attr_accessor :name, :description, :role, :model, :tools, :skills_list
|
|
8
|
+
attr_reader :receives_fields, :returns_fields, :rules, :prompt_body
|
|
9
|
+
|
|
10
|
+
def initialize(name)
|
|
11
|
+
@name = name
|
|
12
|
+
@description = ""
|
|
13
|
+
@role = ""
|
|
14
|
+
@model = "inherit"
|
|
15
|
+
@tools = nil
|
|
16
|
+
@skills_list = []
|
|
17
|
+
@receives_fields = []
|
|
18
|
+
@returns_fields = []
|
|
19
|
+
@rules = []
|
|
20
|
+
@prompt_body = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def receives(&block)
|
|
24
|
+
b = FieldBuilder.new
|
|
25
|
+
b.instance_eval(&block)
|
|
26
|
+
@receives_fields = b.fields
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def returns(&block)
|
|
30
|
+
b = FieldBuilder.new
|
|
31
|
+
b.instance_eval(&block)
|
|
32
|
+
@returns_fields = b.fields
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def constraints(&block)
|
|
36
|
+
b = ConstraintBuilder.new
|
|
37
|
+
b.instance_eval(&block)
|
|
38
|
+
@rules = b.rules
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def prompt(text)
|
|
42
|
+
@prompt_body = text
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def uses_skills(*names)
|
|
46
|
+
@skills_list = names
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class FieldBuilder
|
|
51
|
+
attr_reader :fields
|
|
52
|
+
|
|
53
|
+
def initialize
|
|
54
|
+
@fields = []
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def required(name, type: :string, desc: "")
|
|
58
|
+
@fields << { name: name, required: true, type: type, desc: desc }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def optional(name, type: :string, desc: "")
|
|
62
|
+
@fields << { name: name, required: false, type: type, desc: desc }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class ConstraintBuilder
|
|
67
|
+
attr_reader :rules
|
|
68
|
+
|
|
69
|
+
def initialize
|
|
70
|
+
@rules = []
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def rule(text)
|
|
74
|
+
@rules << text
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end; end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clawthor; module DSL
|
|
4
|
+
# ─── Command ───────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
class Command
|
|
7
|
+
attr_accessor :name, :description, :hint, :body
|
|
8
|
+
attr_accessor :disable_model_invocation, :context, :agent_name
|
|
9
|
+
|
|
10
|
+
def initialize(name)
|
|
11
|
+
@name = name
|
|
12
|
+
@description = ""
|
|
13
|
+
@hint = nil
|
|
14
|
+
@body = ""
|
|
15
|
+
@disable_model_invocation = true
|
|
16
|
+
@context = nil
|
|
17
|
+
@agent_name = nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end; end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clawthor; module DSL
|
|
4
|
+
# ─── Hook ──────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
class Hook
|
|
7
|
+
EVENTS = %i[
|
|
8
|
+
session_start user_prompt_submit pre_tool_use post_tool_use
|
|
9
|
+
post_tool_use_failure stop notification
|
|
10
|
+
subagent_start subagent_stop pre_compact session_end
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
EVENT_MAP = {
|
|
14
|
+
session_start: "SessionStart",
|
|
15
|
+
user_prompt_submit: "UserPromptSubmit",
|
|
16
|
+
pre_tool_use: "PreToolUse",
|
|
17
|
+
post_tool_use: "PostToolUse",
|
|
18
|
+
post_tool_use_failure: "PostToolUseFailure",
|
|
19
|
+
stop: "Stop",
|
|
20
|
+
notification: "Notification",
|
|
21
|
+
subagent_start: "SubagentStart",
|
|
22
|
+
subagent_stop: "SubagentStop",
|
|
23
|
+
pre_compact: "PreCompact",
|
|
24
|
+
session_end: "SessionEnd"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
attr_accessor :name, :event, :matcher, :type, :command, :prompt, :timeout
|
|
28
|
+
attr_reader :script_content
|
|
29
|
+
|
|
30
|
+
def initialize(name)
|
|
31
|
+
@name = name
|
|
32
|
+
@event = nil
|
|
33
|
+
@matcher = nil
|
|
34
|
+
@type = "command"
|
|
35
|
+
@command = nil
|
|
36
|
+
@prompt = nil
|
|
37
|
+
@timeout = nil
|
|
38
|
+
@script_content = nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def event_key
|
|
42
|
+
EVENT_MAP[@event] || @event.to_s
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def runs(script_content)
|
|
46
|
+
@script_content = script_content
|
|
47
|
+
@command = "bash \"$CLAUDE_PROJECT_DIR/scripts/#{@name}.sh\""
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end; end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clawthor; module DSL
|
|
4
|
+
# ─── Module ─────────────────────────────────────────────────
|
|
5
|
+
# A reusable bundle of primitives. Define once, include anywhere.
|
|
6
|
+
#
|
|
7
|
+
# Modules can contain any combination of skills, hooks, agents,
|
|
8
|
+
# commands, tasks, and services. When you `use :module_name`
|
|
9
|
+
# in a definition, all the module's primitives merge into the
|
|
10
|
+
# top-level registry.
|
|
11
|
+
#
|
|
12
|
+
# Example:
|
|
13
|
+
# define_module :services_setup do
|
|
14
|
+
# skill :services do ... end
|
|
15
|
+
# command :svc_setup do ... end
|
|
16
|
+
# hook :check_services do ... end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# # In your definition:
|
|
20
|
+
# use :services_setup
|
|
21
|
+
|
|
22
|
+
class Module
|
|
23
|
+
attr_reader :name, :description, :skills, :hooks, :agents, :tasks, :commands, :services
|
|
24
|
+
|
|
25
|
+
def initialize(name)
|
|
26
|
+
@name = name
|
|
27
|
+
@description = ""
|
|
28
|
+
@skills = {}
|
|
29
|
+
@hooks = {}
|
|
30
|
+
@agents = {}
|
|
31
|
+
@tasks = {}
|
|
32
|
+
@commands = {}
|
|
33
|
+
@services = {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Module-scoped DSL methods — same API as top-level,
|
|
37
|
+
# but primitives are collected into the module, not the registry.
|
|
38
|
+
|
|
39
|
+
def skill(sname, &block)
|
|
40
|
+
s = Skill.new(sname)
|
|
41
|
+
s.instance_eval(&block) if block
|
|
42
|
+
@skills[sname] = s
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def hook(hname, &block)
|
|
46
|
+
h = Hook.new(hname)
|
|
47
|
+
h.instance_eval(&block) if block
|
|
48
|
+
@hooks[hname] = h
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def agent(aname, &block)
|
|
52
|
+
a = Agent.new(aname)
|
|
53
|
+
a.instance_eval(&block) if block
|
|
54
|
+
@agents[aname] = a
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def task(tname, &block)
|
|
58
|
+
t = Task.new(tname)
|
|
59
|
+
t.instance_eval(&block) if block
|
|
60
|
+
@tasks[tname] = t
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def command(cname, &block)
|
|
64
|
+
c = Command.new(cname)
|
|
65
|
+
c.instance_eval(&block) if block
|
|
66
|
+
@commands[cname] = c
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def service(sname, &block)
|
|
70
|
+
s = Service.new(sname)
|
|
71
|
+
s.instance_eval(&block) if block
|
|
72
|
+
@services[sname] = s
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end; end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clawthor; module DSL
|
|
4
|
+
# ─── Service ───────────────────────────────────────────────
|
|
5
|
+
# Compiles to a supervisord [program:name] block.
|
|
6
|
+
#
|
|
7
|
+
# The observability loop:
|
|
8
|
+
# Claude edits code -> service auto-restarts -> Claude reads logs
|
|
9
|
+
# -> Claude sees the error -> Claude fixes it
|
|
10
|
+
#
|
|
11
|
+
# Without this, Claude writes backend code into a black hole.
|
|
12
|
+
|
|
13
|
+
class Service
|
|
14
|
+
attr_accessor :name, :command, :cwd, :user
|
|
15
|
+
attr_accessor :auto_restart, :start_retries
|
|
16
|
+
attr_accessor :stop_signal, :stop_wait, :kill_as_group
|
|
17
|
+
attr_reader :env_vars, :log_config
|
|
18
|
+
|
|
19
|
+
def initialize(name)
|
|
20
|
+
@name = name
|
|
21
|
+
@command = ""
|
|
22
|
+
@cwd = "."
|
|
23
|
+
@user = nil
|
|
24
|
+
@auto_restart = true
|
|
25
|
+
@start_retries = 3
|
|
26
|
+
@stop_signal = "TERM"
|
|
27
|
+
@stop_wait = 15
|
|
28
|
+
@kill_as_group = true
|
|
29
|
+
@env_vars = {}
|
|
30
|
+
@log_config = LogConfig.new(name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def logs(&block)
|
|
34
|
+
@log_config.instance_eval(&block) if block
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def env(&block)
|
|
38
|
+
b = EnvBuilder.new
|
|
39
|
+
b.instance_eval(&block)
|
|
40
|
+
@env_vars = b.vars
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class LogConfig
|
|
45
|
+
attr_accessor :dir, :max_bytes, :backups
|
|
46
|
+
|
|
47
|
+
def initialize(service_name)
|
|
48
|
+
@dir = "logs"
|
|
49
|
+
@max_bytes = "50MB"
|
|
50
|
+
@backups = 5
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def dir(path = nil)
|
|
54
|
+
return @dir if path.nil?
|
|
55
|
+
@dir = path
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def max_bytes(val = nil)
|
|
59
|
+
return @max_bytes if val.nil?
|
|
60
|
+
@max_bytes = val
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def backups(count = nil)
|
|
64
|
+
return @backups if count.nil?
|
|
65
|
+
@backups = count
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class EnvBuilder
|
|
70
|
+
attr_reader :vars
|
|
71
|
+
|
|
72
|
+
def initialize
|
|
73
|
+
@vars = {}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def set(key, value: nil, from: nil)
|
|
77
|
+
@vars[key] = value || "(from #{from})"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end; end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clawthor; module DSL
|
|
4
|
+
# ─── Skill ─────────────────────────────────────────────────
|
|
5
|
+
# Skills have temporal awareness:
|
|
6
|
+
# - version/since/status: when and what era this skill belongs to
|
|
7
|
+
# - deprecated blocks: patterns Claude should not introduce
|
|
8
|
+
# - forbidden blocks: patterns Claude must never use
|
|
9
|
+
#
|
|
10
|
+
# This prevents "averaging" between old and new patterns.
|
|
11
|
+
# Claude sees: current standard, what's legacy, what's banned.
|
|
12
|
+
|
|
13
|
+
class Skill
|
|
14
|
+
attr_accessor :name, :description, :model_invocable, :user_invocable
|
|
15
|
+
attr_accessor :skill_version, :since, :status, :applies_to
|
|
16
|
+
attr_reader :sections, :resources, :scripts, :deprecated_patterns, :forbidden_patterns
|
|
17
|
+
|
|
18
|
+
def initialize(name)
|
|
19
|
+
@name = name
|
|
20
|
+
@description = ""
|
|
21
|
+
@model_invocable = true
|
|
22
|
+
@user_invocable = true
|
|
23
|
+
@skill_version = nil
|
|
24
|
+
@since = nil
|
|
25
|
+
@status = :active # :active, :deprecated, :experimental
|
|
26
|
+
@applies_to = nil # e.g. "all repos", "backend services"
|
|
27
|
+
@sections = []
|
|
28
|
+
@resources = []
|
|
29
|
+
@scripts = []
|
|
30
|
+
@deprecated_patterns = []
|
|
31
|
+
@forbidden_patterns = []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def section(title, &block)
|
|
35
|
+
s = Section.new(title)
|
|
36
|
+
s.instance_eval(&block) if block
|
|
37
|
+
@sections << s
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def resource(name, path)
|
|
41
|
+
@resources << { name: name, path: path }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def script(name, &block)
|
|
45
|
+
s = ScriptDef.new(name)
|
|
46
|
+
s.instance_eval(&block) if block
|
|
47
|
+
@scripts << s
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Deprecated patterns: Claude should not introduce these in new code.
|
|
51
|
+
# Legacy code using them is acceptable until touched.
|
|
52
|
+
def deprecated(pattern_name, &block)
|
|
53
|
+
d = PatternBlock.new(pattern_name, :deprecated)
|
|
54
|
+
d.instance_eval(&block) if block
|
|
55
|
+
@deprecated_patterns << d
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Forbidden patterns: Claude must never use these, period.
|
|
59
|
+
def forbidden(pattern_name, &block)
|
|
60
|
+
f = PatternBlock.new(pattern_name, :forbidden)
|
|
61
|
+
f.instance_eval(&block) if block
|
|
62
|
+
@forbidden_patterns << f
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Used for both deprecated and forbidden pattern declarations
|
|
67
|
+
class PatternBlock
|
|
68
|
+
attr_reader :name, :severity, :notes, :migration_hint
|
|
69
|
+
|
|
70
|
+
def initialize(name, severity)
|
|
71
|
+
@name = name
|
|
72
|
+
@severity = severity # :deprecated or :forbidden
|
|
73
|
+
@notes = []
|
|
74
|
+
@migration_hint = nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def note(text)
|
|
78
|
+
@notes << text
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def migrate(hint)
|
|
82
|
+
@migration_hint = hint
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class Section
|
|
87
|
+
attr_reader :title, :guidelines, :references
|
|
88
|
+
|
|
89
|
+
def initialize(title)
|
|
90
|
+
@title = title
|
|
91
|
+
@guidelines = []
|
|
92
|
+
@references = []
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def guideline(text)
|
|
96
|
+
@guidelines << text
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def reference(name, text)
|
|
100
|
+
@references << { name: name, text: text }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class ScriptDef
|
|
105
|
+
attr_accessor :name, :file, :usage, :purpose, :content
|
|
106
|
+
|
|
107
|
+
def initialize(name)
|
|
108
|
+
@name = name
|
|
109
|
+
@file = nil
|
|
110
|
+
@usage = ""
|
|
111
|
+
@purpose = ""
|
|
112
|
+
@content = nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def file(path = nil)
|
|
116
|
+
return @file if path.nil?
|
|
117
|
+
@file = path
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def usage(text = nil)
|
|
121
|
+
return @usage if text.nil?
|
|
122
|
+
@usage = text
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def purpose(text = nil)
|
|
126
|
+
return @purpose if text.nil?
|
|
127
|
+
@purpose = text
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def content(text = nil)
|
|
131
|
+
return @content if text.nil?
|
|
132
|
+
@content = text
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end; end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clawthor; module DSL
|
|
4
|
+
# ─── Task (DevDoc template) ────────────────────────────────
|
|
5
|
+
|
|
6
|
+
class Task
|
|
7
|
+
attr_accessor :name, :directory_pattern
|
|
8
|
+
attr_reader :files, :states, :transitions
|
|
9
|
+
|
|
10
|
+
def initialize(name)
|
|
11
|
+
@name = name
|
|
12
|
+
@directory_pattern = "dev/active/{task_name}/"
|
|
13
|
+
@files = []
|
|
14
|
+
@states = []
|
|
15
|
+
@transitions = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def creates(&block)
|
|
19
|
+
b = TaskFileBuilder.new
|
|
20
|
+
b.instance_eval(&block)
|
|
21
|
+
@files = b.files
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def lifecycle(&block)
|
|
25
|
+
b = LifecycleBuilder.new
|
|
26
|
+
b.instance_eval(&block)
|
|
27
|
+
@states = b.states
|
|
28
|
+
@transitions = b.transitions
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class TaskFileBuilder
|
|
33
|
+
attr_reader :files
|
|
34
|
+
|
|
35
|
+
def initialize
|
|
36
|
+
@files = []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def file(name, filename, sections: [], format: nil)
|
|
40
|
+
@files << { name: name, filename: filename, sections: sections, format: format }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class LifecycleBuilder
|
|
45
|
+
attr_reader :states, :transitions
|
|
46
|
+
|
|
47
|
+
def initialize
|
|
48
|
+
@states = []
|
|
49
|
+
@transitions = []
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def states(*names)
|
|
53
|
+
@states = names
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def transition(from, to:, when: nil)
|
|
57
|
+
@transitions << { from: from, to: to, condition: binding.local_variable_get(:when) }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end; end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clawthor; module DSL
|
|
4
|
+
# ─── Workspace ──────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
class Workspace
|
|
7
|
+
attr_accessor :name, :root, :plugin_name, :description, :version, :author
|
|
8
|
+
attr_accessor :marketplace_name, :marketplace_description, :marketplace_owner_url
|
|
9
|
+
attr_reader :defaults, :authority_ladder
|
|
10
|
+
|
|
11
|
+
def initialize(name)
|
|
12
|
+
@name = name
|
|
13
|
+
@plugin_name = name.to_s.tr("_", "-")
|
|
14
|
+
@root = "."
|
|
15
|
+
@description = "Plugin for Claude workflows."
|
|
16
|
+
@version = "0.1.0"
|
|
17
|
+
@author = "James"
|
|
18
|
+
@marketplace_name = nil
|
|
19
|
+
@marketplace_description = "Claude plugins for Pants projects."
|
|
20
|
+
@marketplace_owner_url = nil
|
|
21
|
+
@defaults = { skill_max_lines: 500, task_dir: "dev/active", archive_dir: "dev/completed" }
|
|
22
|
+
@authority_ladder = []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def defaults_block(&block)
|
|
26
|
+
b = DefaultsBuilder.new(@defaults)
|
|
27
|
+
b.instance_eval(&block)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Declare the conflict resolution hierarchy.
|
|
31
|
+
# Higher entries win over lower entries.
|
|
32
|
+
def authority(&block)
|
|
33
|
+
b = AuthorityBuilder.new
|
|
34
|
+
b.instance_eval(&block)
|
|
35
|
+
@authority_ladder = b.levels
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class AuthorityBuilder
|
|
40
|
+
attr_reader :levels
|
|
41
|
+
|
|
42
|
+
def initialize
|
|
43
|
+
@levels = []
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Declare an authority level. Order of declaration = rank (highest first).
|
|
47
|
+
def level(code, name, description: "", examples: [])
|
|
48
|
+
@levels << {
|
|
49
|
+
code: code.to_s,
|
|
50
|
+
name: name,
|
|
51
|
+
description: description,
|
|
52
|
+
examples: examples,
|
|
53
|
+
rank: @levels.length
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class DefaultsBuilder
|
|
59
|
+
def initialize(hash)
|
|
60
|
+
@hash = hash
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def method_missing(name, value = nil, **_kw)
|
|
64
|
+
@hash[name] = value if value
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def respond_to_missing?(*)
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end; end
|