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.
@@ -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