claude_agent 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/.claude/commands/spec/complete.md +105 -0
- data/.claude/commands/spec/update.md +95 -0
- data/.claude/rules/conventions.md +622 -0
- data/.claude/rules/git.md +86 -0
- data/.claude/rules/pull-requests.md +31 -0
- data/.claude/rules/releases.md +177 -0
- data/.claude/rules/testing.md +267 -0
- data/.claude/settings.json +49 -0
- data/CHANGELOG.md +13 -0
- data/CLAUDE.md +94 -0
- data/LICENSE.txt +21 -0
- data/README.md +679 -0
- data/Rakefile +63 -0
- data/SPEC.md +558 -0
- data/lib/claude_agent/abort_controller.rb +113 -0
- data/lib/claude_agent/client.rb +298 -0
- data/lib/claude_agent/content_blocks.rb +163 -0
- data/lib/claude_agent/control_protocol.rb +717 -0
- data/lib/claude_agent/errors.rb +103 -0
- data/lib/claude_agent/hooks.rb +228 -0
- data/lib/claude_agent/mcp/server.rb +166 -0
- data/lib/claude_agent/mcp/tool.rb +137 -0
- data/lib/claude_agent/message_parser.rb +262 -0
- data/lib/claude_agent/messages.rb +421 -0
- data/lib/claude_agent/options.rb +264 -0
- data/lib/claude_agent/permissions.rb +164 -0
- data/lib/claude_agent/query.rb +90 -0
- data/lib/claude_agent/sandbox_settings.rb +139 -0
- data/lib/claude_agent/spawn.rb +235 -0
- data/lib/claude_agent/transport/base.rb +61 -0
- data/lib/claude_agent/transport/subprocess.rb +432 -0
- data/lib/claude_agent/types.rb +193 -0
- data/lib/claude_agent/version.rb +5 -0
- data/lib/claude_agent.rb +28 -0
- data/sig/claude_agent.rbs +912 -0
- data/sig/manifest.yaml +5 -0
- metadata +97 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
# Ruby Conventions
|
|
2
|
+
|
|
3
|
+
Idiomatic Ruby patterns for well-structured, maintainable gems.
|
|
4
|
+
|
|
5
|
+
## File Organization
|
|
6
|
+
|
|
7
|
+
### Directory Structure
|
|
8
|
+
|
|
9
|
+
Organize by concern with clear architectural layers:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
lib/
|
|
13
|
+
├── gem_name.rb # Entry point, requires everything
|
|
14
|
+
├── gem_name/
|
|
15
|
+
│ ├── version.rb # Version constant only
|
|
16
|
+
│ ├── error.rb # Error hierarchy
|
|
17
|
+
│ ├── cli/ # CLI layer (orchestration)
|
|
18
|
+
│ │ ├── base.rb
|
|
19
|
+
│ │ └── feature.rb
|
|
20
|
+
│ ├── commands/ # Command builders (what to execute)
|
|
21
|
+
│ │ ├── base.rb
|
|
22
|
+
│ │ └── feature.rb
|
|
23
|
+
│ ├── configuration/ # Config loading and validation
|
|
24
|
+
│ │ ├── validation.rb
|
|
25
|
+
│ │ └── validator/
|
|
26
|
+
│ └── utils.rb # Utility module
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Three-Tier Architecture
|
|
30
|
+
|
|
31
|
+
Separate concerns into distinct layers:
|
|
32
|
+
|
|
33
|
+
| Layer | Purpose | Example |
|
|
34
|
+
|-------|---------|---------|
|
|
35
|
+
| CLI | Orchestrates operations, user interaction | `Cli::App#deploy` |
|
|
36
|
+
| Commands | Builds command arrays, no execution | `Commands::App#run` |
|
|
37
|
+
| Configuration | Loads, validates, provides config | `Configuration::Role` |
|
|
38
|
+
|
|
39
|
+
### Entry Point Pattern
|
|
40
|
+
|
|
41
|
+
The main file loads dependencies and defines the top-level interface:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
# lib/gem_name.rb
|
|
45
|
+
require_relative "gem_name/version"
|
|
46
|
+
require_relative "gem_name/error"
|
|
47
|
+
require_relative "gem_name/configuration"
|
|
48
|
+
# ... other requires
|
|
49
|
+
|
|
50
|
+
module GemName
|
|
51
|
+
class Error < StandardError; end
|
|
52
|
+
class ConfigurationError < Error; end
|
|
53
|
+
|
|
54
|
+
class << self
|
|
55
|
+
def query(prompt, **options)
|
|
56
|
+
# Convenience method for common use case
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Module & Class Patterns
|
|
63
|
+
|
|
64
|
+
### Mixins over Deep Inheritance
|
|
65
|
+
|
|
66
|
+
Compose behavior using modules:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
class App < Base
|
|
70
|
+
include Assets, Containers, Logging, Proxy
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### ActiveSupport::Concern
|
|
75
|
+
|
|
76
|
+
Structure mixins with clear class/instance separation:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
module Validation
|
|
80
|
+
extend ActiveSupport::Concern
|
|
81
|
+
|
|
82
|
+
class_methods do
|
|
83
|
+
def validation_doc
|
|
84
|
+
@validation_doc ||= load_validation_doc
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def validate!(config, context:, with: Validator)
|
|
89
|
+
with.new(config, context: context).validate!
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Delegation
|
|
95
|
+
|
|
96
|
+
Reduce boilerplate with `delegate`:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
class Configuration
|
|
100
|
+
delegate :service, :hooks_path, to: :raw_config, allow_nil: true
|
|
101
|
+
delegate :escape_shell_value, :argumentize, to: Utils
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class Boot
|
|
105
|
+
delegate :execute, :capture_with_info, to: :sshkit
|
|
106
|
+
delegate :assets?, :running_proxy?, to: :role
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Factory Methods
|
|
111
|
+
|
|
112
|
+
Prefer factory class methods over complex constructors:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
class << self
|
|
116
|
+
def create_from(file:, version: nil)
|
|
117
|
+
new(load_file(file), version: version)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def load_file(path)
|
|
123
|
+
# ...
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Inquiry Objects
|
|
129
|
+
|
|
130
|
+
Use ActiveSupport's `inquiry` for readable predicates:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
def initialize(name, config:)
|
|
134
|
+
@name = name.inquiry
|
|
135
|
+
@config = config
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Later:
|
|
139
|
+
role.name.web? # => true if name is "web"
|
|
140
|
+
role.name.worker? # => true if name is "worker"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Error Handling
|
|
144
|
+
|
|
145
|
+
### Simple Hierarchy
|
|
146
|
+
|
|
147
|
+
Inherit directly from `StandardError` with clear namespacing:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
module GemName
|
|
151
|
+
class Error < StandardError; end
|
|
152
|
+
class ConfigurationError < Error; end
|
|
153
|
+
class ProcessError < Error; end
|
|
154
|
+
|
|
155
|
+
module Cli
|
|
156
|
+
class BootError < StandardError; end
|
|
157
|
+
class LockError < StandardError; end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Context in Errors
|
|
163
|
+
|
|
164
|
+
Include relevant context as attributes:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
class ProcessError < Error
|
|
168
|
+
attr_reader :exit_code, :stderr
|
|
169
|
+
|
|
170
|
+
def initialize(message, exit_code: nil, stderr: nil)
|
|
171
|
+
@exit_code = exit_code
|
|
172
|
+
@stderr = stderr
|
|
173
|
+
super(message)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Method Organization
|
|
179
|
+
|
|
180
|
+
### Public/Private Separation
|
|
181
|
+
|
|
182
|
+
Keep implementation details private. Use Rails-style indentation under `private`:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
class Command
|
|
186
|
+
# Public interface
|
|
187
|
+
def execute(host:)
|
|
188
|
+
build_command(host).tap { |cmd| validate!(cmd) }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
def build_command(host)
|
|
193
|
+
# Implementation detail
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def validate!(cmd)
|
|
197
|
+
# Implementation detail
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Group Related Helpers
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
private
|
|
206
|
+
# --- Command Building ---
|
|
207
|
+
|
|
208
|
+
def combine(*commands, by: "&&")
|
|
209
|
+
commands.compact.flatten.join(" #{by} ")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def chain(*commands)
|
|
213
|
+
combine(*commands, by: ";")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def pipe(*commands)
|
|
217
|
+
combine(*commands, by: "|")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# --- Validation ---
|
|
221
|
+
|
|
222
|
+
def valid_name?(name)
|
|
223
|
+
name.match?(/\A[a-z0-9_-]+\z/)
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Naming Conventions
|
|
228
|
+
|
|
229
|
+
| Element | Convention | Example |
|
|
230
|
+
|--------------|----------------------------|-------------------------------------------|
|
|
231
|
+
| Predicates | `?` suffix | `valid?`, `running?`, `configured?` |
|
|
232
|
+
| Mutators | `!` suffix | `validate!`, `reset!`, `configure!` |
|
|
233
|
+
| Setters | `=` suffix | `name=`, `options=` |
|
|
234
|
+
| Converters | `to_*` prefix | `to_h`, `to_json`, `to_cli_args` |
|
|
235
|
+
| Builders | `build_*` prefix | `build_command`, `build_options` |
|
|
236
|
+
| Initializers | `initialize_*` prefix | `initialize_env`, `initialize_proxy` |
|
|
237
|
+
| Validators | `ensure_*` or `validate_*` | `ensure_required_keys`, `validate_config` |
|
|
238
|
+
|
|
239
|
+
## Configuration & Validation
|
|
240
|
+
|
|
241
|
+
### Validate at Construction
|
|
242
|
+
|
|
243
|
+
Don't defer validation:
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
def initialize(name, config:)
|
|
247
|
+
@name = name.inquiry
|
|
248
|
+
@config = config
|
|
249
|
+
|
|
250
|
+
validate! \
|
|
251
|
+
config,
|
|
252
|
+
context: "accessories/#{name}",
|
|
253
|
+
with: Validator::Accessory
|
|
254
|
+
|
|
255
|
+
@env = initialize_env
|
|
256
|
+
@proxy = initialize_proxy if running_proxy?
|
|
257
|
+
end
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Validator Classes
|
|
261
|
+
|
|
262
|
+
Dedicated validator with context tracking:
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
class Validator
|
|
266
|
+
attr_reader :config, :example, :context
|
|
267
|
+
|
|
268
|
+
def initialize(config, example:, context:)
|
|
269
|
+
@config = config
|
|
270
|
+
@example = example
|
|
271
|
+
@context = context
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def validate!
|
|
275
|
+
ensure_required_keys_present
|
|
276
|
+
validate_against_example
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
private
|
|
280
|
+
def with_context(key)
|
|
281
|
+
"#{context}/#{key}"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def error(message)
|
|
285
|
+
raise ConfigurationError, "[#{context}] #{message}"
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Validation Mixin
|
|
291
|
+
|
|
292
|
+
Reusable validation behavior:
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
module Validation
|
|
296
|
+
extend ActiveSupport::Concern
|
|
297
|
+
|
|
298
|
+
class_methods do
|
|
299
|
+
def validation_config_key
|
|
300
|
+
name.demodulize.underscore
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def validate!(config, context: nil, with: Validator)
|
|
305
|
+
context ||= self.class.validation_config_key
|
|
306
|
+
with.new(config, example: validation_example, context: context).validate!
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
class Accessory
|
|
311
|
+
include Validation
|
|
312
|
+
|
|
313
|
+
def initialize(name, config:)
|
|
314
|
+
@name = name
|
|
315
|
+
validate!(config, context: "accessories/#{name}")
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Lazy Initialization
|
|
321
|
+
|
|
322
|
+
Use `||=` for computed properties:
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
def tags
|
|
326
|
+
@tags ||= raw_config.fetch("tags", []).map { |t| Tag.new(t) }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def env_tags
|
|
330
|
+
@env_tags ||= if (tags = raw_config.env["tags"])
|
|
331
|
+
tags.map { |name, config| Env::Tag.new(name, config: config) }
|
|
332
|
+
else
|
|
333
|
+
[]
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Command Pattern
|
|
339
|
+
|
|
340
|
+
### Return Data, Don't Execute
|
|
341
|
+
|
|
342
|
+
Build command representations rather than executing directly:
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
def run(hostname: nil)
|
|
346
|
+
docker :run,
|
|
347
|
+
"--detach",
|
|
348
|
+
"--restart unless-stopped",
|
|
349
|
+
"--name", container_name,
|
|
350
|
+
*env_args(host),
|
|
351
|
+
image,
|
|
352
|
+
cmd
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Returns: [:docker, :run, "--detach", ..., "image:tag", "cmd"]
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
This enables:
|
|
359
|
+
- Testing without execution
|
|
360
|
+
- Command composition
|
|
361
|
+
- Auditing what will run
|
|
362
|
+
- SSH tunneling via execution layer
|
|
363
|
+
|
|
364
|
+
### Private Helpers for Command Building
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
private
|
|
368
|
+
def combine(*commands, by: "&&")
|
|
369
|
+
commands
|
|
370
|
+
.compact
|
|
371
|
+
.collect { |command| Array(command) + [by] }.flatten
|
|
372
|
+
.tap { |commands| commands.pop }
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def docker(*args)
|
|
376
|
+
args.compact.unshift(:docker)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def pipe(*commands)
|
|
380
|
+
combine(*commands, by: "|")
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def chain(*commands)
|
|
384
|
+
combine(*commands, by: ";")
|
|
385
|
+
end
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Utility Modules
|
|
389
|
+
|
|
390
|
+
### Extend Self Pattern
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
module Utils
|
|
394
|
+
extend self
|
|
395
|
+
|
|
396
|
+
def escape_shell_value(value)
|
|
397
|
+
Shellwords.escape(value.to_s)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def argumentize(argument, attributes, sensitive: false)
|
|
401
|
+
Array(attributes).flat_map do |key, value|
|
|
402
|
+
[argument, sensitive ? Sensitive.new(value) : value]
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def filter_specific_items(filters, items)
|
|
407
|
+
filters.empty? ? items : items.select { |item| filters.include?(item) }
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Usage: Utils.escape_shell_value(val)
|
|
412
|
+
# Or via delegation: delegate :escape_shell_value, to: Utils
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## Testing Patterns
|
|
416
|
+
|
|
417
|
+
### Test Framework
|
|
418
|
+
|
|
419
|
+
Use Minitest with ActiveSupport::TestCase:
|
|
420
|
+
|
|
421
|
+
```ruby
|
|
422
|
+
class ConfigurationTest < ActiveSupport::TestCase
|
|
423
|
+
setup do
|
|
424
|
+
@deploy = { service: "app", image: "app:latest" }
|
|
425
|
+
@config = Configuration.new(@deploy)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
teardown do
|
|
429
|
+
# Clean up
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
test "service name valid" do
|
|
433
|
+
assert_nothing_raised do
|
|
434
|
+
Configuration.new(@deploy.merge(service: "valid-name"))
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
test "raises on invalid service name" do
|
|
439
|
+
assert_raises(ConfigurationError) do
|
|
440
|
+
Configuration.new(@deploy.merge(service: "INVALID"))
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Custom Test Base Classes
|
|
447
|
+
|
|
448
|
+
Share setup across tests:
|
|
449
|
+
|
|
450
|
+
```ruby
|
|
451
|
+
class CliTestCase < ActiveSupport::TestCase
|
|
452
|
+
setup do
|
|
453
|
+
@original_env = ENV.to_h
|
|
454
|
+
ENV["VERSION"] = "test"
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
teardown do
|
|
458
|
+
ENV.replace(@original_env)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
private
|
|
462
|
+
def stub_setup
|
|
463
|
+
# Shared test helpers
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
class CommandsTestCase < ActiveSupport::TestCase
|
|
468
|
+
setup do
|
|
469
|
+
@config = Configuration.new(default_config)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
private
|
|
473
|
+
def default_config
|
|
474
|
+
{ service: "app", image: "app:latest" }
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Mocking with Mocha
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
require "mocha/minitest"
|
|
483
|
+
|
|
484
|
+
class ClientTest < ActiveSupport::TestCase
|
|
485
|
+
test "spawns subprocess with correct args" do
|
|
486
|
+
Process.expects(:spawn).with(
|
|
487
|
+
"claude", "--print", "--output-format", "json",
|
|
488
|
+
has_entries(chdir: "/tmp")
|
|
489
|
+
).returns(123)
|
|
490
|
+
|
|
491
|
+
client.start
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
test "handles timeout" do
|
|
495
|
+
transport = Transport.new
|
|
496
|
+
transport.stubs(:read_line).raises(Timeout::Error)
|
|
497
|
+
|
|
498
|
+
assert_raises(TimeoutError) { transport.receive }
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Stubbing Methods
|
|
504
|
+
|
|
505
|
+
```ruby
|
|
506
|
+
test "uses custom model when specified" do
|
|
507
|
+
options = Options.new(model: "opus")
|
|
508
|
+
options.stubs(:validate!).returns(true)
|
|
509
|
+
|
|
510
|
+
assert_equal "opus", options.model
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
test "retries on connection failure" do
|
|
514
|
+
sequence = sequence("retry")
|
|
515
|
+
|
|
516
|
+
client.expects(:connect).in_sequence(sequence).raises(ConnectionError)
|
|
517
|
+
client.expects(:connect).in_sequence(sequence).returns(true)
|
|
518
|
+
|
|
519
|
+
client.connect_with_retry
|
|
520
|
+
end
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
## Code Style
|
|
524
|
+
|
|
525
|
+
### Frozen String Literals
|
|
526
|
+
|
|
527
|
+
Every file starts with:
|
|
528
|
+
|
|
529
|
+
```ruby
|
|
530
|
+
# frozen_string_literal: true
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Hash Formatting
|
|
534
|
+
|
|
535
|
+
```ruby
|
|
536
|
+
# Single line for few keys
|
|
537
|
+
{ name: "value", type: :string }
|
|
538
|
+
|
|
539
|
+
# Multi-line for many keys or nested structures
|
|
540
|
+
{
|
|
541
|
+
name: "value",
|
|
542
|
+
type: :string,
|
|
543
|
+
options: {
|
|
544
|
+
required: true,
|
|
545
|
+
default: nil
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### Method Chaining
|
|
551
|
+
|
|
552
|
+
```ruby
|
|
553
|
+
# Fluent interfaces return self
|
|
554
|
+
def configure(key, value)
|
|
555
|
+
@config[key] = value
|
|
556
|
+
self
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# Usage
|
|
560
|
+
builder.configure(:timeout, 30).configure(:retries, 3)
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Guard Clauses
|
|
564
|
+
|
|
565
|
+
Prefer early returns:
|
|
566
|
+
|
|
567
|
+
```ruby
|
|
568
|
+
def process(input)
|
|
569
|
+
return if input.nil?
|
|
570
|
+
return default_value if input.empty?
|
|
571
|
+
|
|
572
|
+
# Main logic
|
|
573
|
+
end
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Line Continuation
|
|
577
|
+
|
|
578
|
+
Use backslash for method calls spanning lines:
|
|
579
|
+
|
|
580
|
+
```ruby
|
|
581
|
+
validate! \
|
|
582
|
+
config,
|
|
583
|
+
example: validation_yml["accessories"]["mysql"],
|
|
584
|
+
context: "accessories/#{name}",
|
|
585
|
+
with: Validator::Accessory
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
## Advanced Patterns
|
|
589
|
+
|
|
590
|
+
### Global Coordinator (Use Sparingly)
|
|
591
|
+
|
|
592
|
+
For CLI applications that need shared state:
|
|
593
|
+
|
|
594
|
+
```ruby
|
|
595
|
+
# lib/gem_name/cli.rb
|
|
596
|
+
COORDINATOR = Coordinator.new
|
|
597
|
+
|
|
598
|
+
class Cli::Base
|
|
599
|
+
def initialize(*)
|
|
600
|
+
super
|
|
601
|
+
initialize_coordinator unless COORDINATOR.configured?
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### Factory Methods in Coordinator
|
|
607
|
+
|
|
608
|
+
```ruby
|
|
609
|
+
class Coordinator
|
|
610
|
+
def app(role: nil, host: nil)
|
|
611
|
+
Commands::App.new(config, role: role, host: host)
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def builder
|
|
615
|
+
@builder ||= Commands::Builder.new(config)
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def configured?
|
|
619
|
+
@config.present?
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
```
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Git Etiquette
|
|
2
|
+
|
|
3
|
+
## Commit Messages
|
|
4
|
+
|
|
5
|
+
- Never reference Claude, Anthropic, or AI agents in commit messages
|
|
6
|
+
- Always create atomic commits that do not leave the app in a broken state
|
|
7
|
+
|
|
8
|
+
## Conventional Commits Format
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
<type>[optional scope]: <description>
|
|
12
|
+
|
|
13
|
+
[optional body]
|
|
14
|
+
|
|
15
|
+
[optional footer(s)]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Commit Types
|
|
19
|
+
|
|
20
|
+
| Label | Purpose |
|
|
21
|
+
|-------------|------------------------------------------------------------------|
|
|
22
|
+
| `feat:` | New feature for the user (MINOR version bump) |
|
|
23
|
+
| `fix:` | Bug fix for the user (PATCH version bump) |
|
|
24
|
+
| `docs:` | Documentation changes |
|
|
25
|
+
| `style:` | Formatting, missing semicolons, etc. (no production code change) |
|
|
26
|
+
| `refactor:` | Refactoring production code (e.g., renaming a variable) |
|
|
27
|
+
| `perf:` | Performance improvements |
|
|
28
|
+
| `test:` | Adding/refactoring tests (no production code change) |
|
|
29
|
+
| `build:` | Build system or external dependency changes |
|
|
30
|
+
| `ci:` | CI configuration changes |
|
|
31
|
+
| `chore:` | Other tasks (no production code change) |
|
|
32
|
+
|
|
33
|
+
## Scope (Optional)
|
|
34
|
+
|
|
35
|
+
Use scope to specify what area of the codebase is affected:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
feat(client): add session resumption support
|
|
39
|
+
fix(transport): handle CLI process timeout
|
|
40
|
+
refactor(mcp): extract schema normalization to module
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Breaking Changes
|
|
44
|
+
|
|
45
|
+
Indicate breaking changes using either method:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Method 1: Add ! before the colon
|
|
49
|
+
feat!: change Options class to use keyword arguments
|
|
50
|
+
feat(hooks)!: rename callback parameter to handler
|
|
51
|
+
|
|
52
|
+
# Method 2: BREAKING CHANGE footer
|
|
53
|
+
feat: restructure message content blocks
|
|
54
|
+
|
|
55
|
+
BREAKING CHANGE: ToolUseBlock now returns input as Hash instead of JSON string
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Multi-line Commits
|
|
59
|
+
|
|
60
|
+
For complex changes, add a body and/or footer:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
fix(protocol): resolve nil error in message parsing
|
|
64
|
+
|
|
65
|
+
The parser was failing when content blocks had no text field.
|
|
66
|
+
Added nil check and default to empty string.
|
|
67
|
+
|
|
68
|
+
Fixes: #42
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Examples
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
feat: add streaming support to Query interface
|
|
75
|
+
feat(client): add session resumption support
|
|
76
|
+
fix: resolve timeout error in subprocess transport
|
|
77
|
+
fix(mcp)!: change tool schema format
|
|
78
|
+
docs: update README with hook examples
|
|
79
|
+
style: fix indentation in control_protocol.rb
|
|
80
|
+
refactor: extract JSON parsing to MessageParser
|
|
81
|
+
perf: reduce memory allocation in message streaming
|
|
82
|
+
test: add specs for permission callbacks
|
|
83
|
+
build: bump minimum Ruby version to 3.2
|
|
84
|
+
ci: add integration test workflow
|
|
85
|
+
chore: update rubocop configuration
|
|
86
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Pull Request Guidelines
|
|
2
|
+
|
|
3
|
+
## Description Format
|
|
4
|
+
|
|
5
|
+
Use the What/Why/How structure:
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
## What
|
|
9
|
+
Brief description of the changes (1-2 sentences).
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
Business or engineering goal this addresses. Link to GitHub issue if applicable.
|
|
13
|
+
|
|
14
|
+
## How
|
|
15
|
+
Significant implementation decisions or trade-offs (omit for simple changes).
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Writing Guidelines
|
|
19
|
+
|
|
20
|
+
- **Be explicit** - Don't just say "#123", explain what was fixed
|
|
21
|
+
- **Why > What** - Reviewers can read the code; explain the reasoning
|
|
22
|
+
- **Keep it scannable** - Use bullet points over paragraphs
|
|
23
|
+
- **Link issues after context** - Reference GitHub issues, but explain first
|
|
24
|
+
|
|
25
|
+
## Pre-flight Criteria
|
|
26
|
+
|
|
27
|
+
Before creating a PR, verify:
|
|
28
|
+
|
|
29
|
+
1. All changes are committed (no uncommitted work)
|
|
30
|
+
2. Branch is pushed to remote with `-u` flag
|
|
31
|
+
3. PR leaves the app in a stable state (not broken)
|