antigravity-sdk 0.4.2 → 0.5.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2a5cad1edda5eb5efaa58d7d5104ba433f8469a923bb5f862b9f1bcb4219021
4
- data.tar.gz: 75c9924910ef8aab407444abfecfa0f7e989473d8be251691e4e07c9d04499aa
3
+ metadata.gz: 25e15da1171b05a2bcc0be36f79e4f1f6e6e0e6135d520d3776282007071ee05
4
+ data.tar.gz: 3ddfcb833bb6d1339573b81e5910f33c37cd4113bfb28c753367386f0d67d88e
5
5
  SHA512:
6
- metadata.gz: ac637d9792051869f94b4a8ebc716fd604b2db18886e19b105190a9636c7e7bce418aeece52a360b139b1c534e560605b0dc578fab10b3e06ac913dc454663eb
7
- data.tar.gz: c8b6e4bee541853abd1f993d5423cc3426c830a24aba58f6790ec30e093279163b83e258a824811380ac5a603cba749dd7a099dee5ad4a2160fe25a7373c6e51
6
+ metadata.gz: 70112ba04d101d659b82372f4f49f03a301d9d1e8fb4c89de871ccf2abdee7bed7017b3fe8440eff5c935902f0c545238081d2439e7aff1e4ca5a63d76f87e83
7
+ data.tar.gz: feb64a14685fefe355772238556f05e27a08aad6f9ad44ae74db4fd0c16e1821eda88f78dbcc24cf990cb90d744139697b06eb883da4fe1f447d67a9d486de7c
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.5.4
@@ -5,16 +5,17 @@ module Antigravity
5
5
 
6
6
  attr_accessor :model, :system_instruction, :api_key
7
7
  attr_reader :tools, :skills, :hooks, :sidecars, :client, :logger_guard,
8
- :workspace, :connection, :conversation
8
+ :workspace, :connection, :conversation, :policy, :policies, :born_at
9
9
 
10
10
  def initialize(model: nil, system_instruction: nil, tools: [],
11
- skills: [], workspace: nil, auto_logger: true, log_file: nil, &block)
11
+ skills: [], policies: [], policy: nil, workspace: nil, auto_logger: true, log_file: nil, &block)
12
12
  @model = model || Antigravity.config.default_model
13
13
  @api_key = Antigravity.config.api_key
14
14
  @system_instruction = system_instruction
15
- @workspace = workspace ? File.expand_path(workspace) : nil
15
+ @workspace = resolve_workspace(workspace)
16
16
  @tools = tools.dup
17
17
  @skills = []
18
+ @policies = []
18
19
  @sidecars = []
19
20
  @hooks = Hooks.new
20
21
  @client = Client.new
@@ -22,6 +23,7 @@ module Antigravity
22
23
  @connection = nil
23
24
  @conversation = nil
24
25
  @connected = false
26
+ @born_at = Time.now
25
27
 
26
28
  # Register pre-provided tools into the tool runner
27
29
  @tool_runner = ToolRunner.new
@@ -30,6 +32,15 @@ module Antigravity
30
32
  # Load skills provided at construction (local paths or GitHub URLs)
31
33
  add_skills(skills) unless Array(skills).empty?
32
34
 
35
+ # Resolve policy: sugar (symbol → preset, Policy object → use directly)
36
+ if policy
37
+ resolved = policy.is_a?(Symbol) ? Policy.preset(policy) : policy
38
+ enforce(resolved)
39
+ end
40
+
41
+ # Load policies
42
+ policies.each { |p| enforce(p) }
43
+
33
44
  # Automagic Logger attachment unless disabled via ENV["ANTIGRAVITY_LOGGER"]=false or auto_logger: false
34
45
  if auto_logger && logger_enabled?
35
46
  attach_logger(log_file)
@@ -38,6 +49,13 @@ module Antigravity
38
49
  yield(self) if block_given?
39
50
  end
40
51
 
52
+ def enforce(policy)
53
+ @policies << policy
54
+ before_tool_call do |tool_name, args|
55
+ policy.evaluate(tool_name, args)
56
+ end
57
+ end
58
+
41
59
  # --- Class Methods ---
42
60
 
43
61
  # Block form: opens connection, yields agent, auto-closes.
@@ -51,11 +69,21 @@ module Antigravity
51
69
  end
52
70
  end
53
71
 
72
+ def workspace=(path)
73
+ @workspace = resolve_workspace(path)
74
+ end
75
+
54
76
  # --- Connection Lifecycle ---
55
77
 
56
78
  def connect!
57
79
  return self if @connected
58
80
 
81
+ # Auto-attach lifecycle logger if enabled via env
82
+ if lifecycle_logger_enabled? && !@lifecycle_attached
83
+ LifecycleLogger.attach!(self, verbose: ENV['ANTIGRAVITY_LIFECYCLE_VERBOSE'] == '1')
84
+ @lifecycle_attached = true
85
+ end
86
+
59
87
  @connection = Connection::LocalConnection.new
60
88
  @connection.connect!
61
89
 
@@ -66,8 +94,28 @@ module Antigravity
66
94
  )
67
95
 
68
96
  harness_config = build_harness_config
97
+
98
+ # Emit indexing hooks — workspace indexing happens during session init
99
+ index_t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) if @workspace
100
+ hooks.emit(:indexing_start, { workspace: @workspace }) if @workspace
101
+
69
102
  @conversation.initialize_session!(harness_config: harness_config)
70
103
  @connected = true
104
+
105
+ if @workspace
106
+ index_elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - index_t0
107
+ hooks.emit(:indexing_done, { workspace: @workspace, elapsed: index_elapsed.round(2) })
108
+ end
109
+
110
+ # Emit session_start hook
111
+ hooks.emit(:session_start, {
112
+ model: @model,
113
+ conversation_id: conversation_id,
114
+ workspace: @workspace,
115
+ skills_count: @skills.length,
116
+ tools_count: @tools.length,
117
+ })
118
+
71
119
  self
72
120
  end
73
121
 
@@ -76,6 +124,14 @@ module Antigravity
76
124
  end
77
125
 
78
126
  def close!
127
+ # Emit session_end hook before teardown
128
+ if @connected
129
+ hooks.emit(:session_end, {
130
+ turn_count: turn_count,
131
+ conversation_id: conversation_id,
132
+ })
133
+ end
134
+
79
135
  @connected = false
80
136
  @connection&.disconnect!
81
137
  @connection = nil
@@ -85,6 +141,7 @@ module Antigravity
85
141
  # --- Chat ---
86
142
 
87
143
  def prompt(message, timeout: Antigravity.config.timeout_llm, &block)
144
+ connect! unless @connected
88
145
  emit_sidecar_event(:prompt_started, prompt: message)
89
146
  hooks.run_pre_prompt(message)
90
147
 
@@ -118,6 +175,21 @@ module Antigravity
118
175
  @conversation.session_summary(model: @model)
119
176
  end
120
177
 
178
+ # Seconds since agent was created
179
+ def uptime
180
+ Time.now - @born_at
181
+ end
182
+
183
+ # Human-readable uptime: "1m 23.4s", "7.3s"
184
+ def uptime_human
185
+ secs = uptime
186
+ if secs >= 60
187
+ "#{(secs / 60).to_i}m #{(secs % 60).round(1)}s"
188
+ else
189
+ "#{secs.round(1)}s"
190
+ end
191
+ end
192
+
121
193
  # --- Tool Registration ---
122
194
 
123
195
  def register_tool(tool_or_name = nil, description: "", &block)
@@ -223,6 +295,16 @@ module Antigravity
223
295
  true
224
296
  end
225
297
 
298
+ def lifecycle_logger_enabled?
299
+ # Explicit opt-in (strip handles trailing spaces in .env files)
300
+ return true if ENV['ANTIGRAVITY_LIFECYCLE']&.strip == '1'
301
+ # Rails/Rack test or development
302
+ return true if %w[test development].include?(ENV['RAILS_ENV']&.strip&.downcase)
303
+ return true if %w[test development].include?(ENV['RACK_ENV']&.strip&.downcase)
304
+ # Explicit opt-out
305
+ false
306
+ end
307
+
226
308
  def build_harness_config
227
309
  api_key = ENV.fetch('GEMINI_API_KEY') {
228
310
  raise ConfigError, 'GEMINI_API_KEY environment variable is required'
@@ -308,5 +390,22 @@ module Antigravity
308
390
  @skills << skill
309
391
  skill
310
392
  end
393
+
394
+ def resolve_workspace(val)
395
+ return nil if val.nil? || val == false
396
+
397
+ raw_path = case val
398
+ when :here, :current, true, '.'
399
+ '.'
400
+ else
401
+ val.to_s
402
+ end
403
+
404
+ expanded = File.expand_path(raw_path)
405
+ expanded += '/' unless expanded.end_with?('/')
406
+
407
+ $stderr.puts "📁 Setting workspace to \e[34m#{expanded}\e[0m" rescue nil
408
+ expanded
409
+ end
311
410
  end
312
411
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Lightweight ANSI color helpers for terminal output.
4
+ # No external dependencies. Safe for piped/non-TTY output.
5
+ module Antigravity
6
+ module Colors
7
+ CODES = {
8
+ reset: "\e[0m",
9
+ bold: "\e[1m",
10
+ dim: "\e[2m",
11
+ italic: "\e[3m",
12
+ # Foreground
13
+ gray: "\e[90m",
14
+ red: "\e[31m",
15
+ green: "\e[32m",
16
+ yellow: "\e[33m",
17
+ blue: "\e[34m",
18
+ magenta: "\e[35m",
19
+ cyan: "\e[36m",
20
+ white: "\e[37m",
21
+ # Bright
22
+ bright_green: "\e[92m",
23
+ bright_yellow: "\e[93m",
24
+ bright_cyan: "\e[96m",
25
+ }.freeze
26
+
27
+ def self.colorize(text, *styles)
28
+ return text.to_s unless $stdout.tty?
29
+ prefix = styles.map { |s| CODES[s] || "" }.join
30
+ "#{prefix}#{text}#{CODES[:reset]}"
31
+ end
32
+
33
+ def self.gray(text) = colorize(text, :gray)
34
+ def self.dim(text) = colorize(text, :dim)
35
+ def self.green(text) = colorize(text, :green)
36
+ def self.yellow(text) = colorize(text, :yellow)
37
+ def self.red(text) = colorize(text, :red)
38
+ def self.cyan(text) = colorize(text, :cyan)
39
+ def self.blue(text) = colorize(text, :blue)
40
+ def self.magenta(text) = colorize(text, :magenta)
41
+ def self.bold(text) = colorize(text, :bold)
42
+ end
43
+ end
@@ -87,7 +87,7 @@ module Antigravity
87
87
 
88
88
  unless wheel
89
89
  # Try all versions for the platform
90
- versions = data['releases']&.keys&.sort_by { |v| Gem::Version.new(v) rescue v }&.reverse
90
+ versions = data['releases']&.keys&.sort_by { |v| Gem::Version.correct?(v) ? Gem::Version.new(v) : Gem::Version.new('0') }&.reverse
91
91
  versions&.each do |ver|
92
92
  files = data.dig('releases', ver) || []
93
93
  wheel = files.find { |u| u['filename']&.include?(platform) && u['filename']&.end_with?('.whl') }
@@ -134,7 +134,11 @@ module Antigravity
134
134
  # Wheels are just ZIP files. Extract the binary.
135
135
  # Look for: google/antigravity/bin/localharness (or language_server)
136
136
  extract_dir = Dir.mktmpdir('agy-extract-')
137
- system('unzip', '-q', '-o', wheel_path, '-d', extract_dir)
137
+ success = system('unzip', '-q', '-o', wheel_path, '-d', extract_dir)
138
+ unless success
139
+ FileUtils.rm_rf(extract_dir)
140
+ raise HarnessNotFoundError, "Failed to extract wheel using unzip command"
141
+ end
138
142
 
139
143
  # Search for the binary inside (prefer localharness over language_server)
140
144
  candidates = Dir.glob("#{extract_dir}/**/localharness") +
@@ -121,8 +121,7 @@ module Antigravity
121
121
  end
122
122
 
123
123
  def perform_handshake!
124
- storage_dir = File.join(Dir.tmpdir, "antigravity-ruby-#{$$}")
125
- FileUtils.mkdir_p(storage_dir)
124
+ storage_dir = Dir.mktmpdir('antigravity-ruby-')
126
125
 
127
126
  # Send InputConfig via stdin
128
127
  input_config = Protocol.encode_input_config(
@@ -59,6 +59,11 @@ module Antigravity
59
59
  @turn_count += 1
60
60
  @last_turn_usage = empty_usage
61
61
 
62
+ # GHI #18: Drain any stale messages from the WebSocket buffer before sending
63
+ # a new prompt. This prevents leftover FULLY_IDLE from previous turns or init
64
+ # from being consumed by collect_response.
65
+ drain_stale_messages
66
+
62
67
  # Send user input (protobuf InputEvent with user_input string field)
63
68
  input_event = {
64
69
  userInput: prompt
@@ -87,6 +92,20 @@ module Antigravity
87
92
 
88
93
  private
89
94
 
95
+ # GHI #18: Non-blocking drain of stale WebSocket messages (FULLY_IDLE leftovers).
96
+ # During gaps between turns (e.g. voice transcription taking 5-10s), the harness
97
+ # may send trajectory updates that would confuse the next collect_response call.
98
+ def drain_stale_messages
99
+ drained = 0
100
+ loop do
101
+ msg = @ws.receive_json(timeout: 0.05, idle_timeout: 0.05) rescue nil
102
+ break unless msg
103
+ drained += 1
104
+ @hooks&.emit(:ws_message, { _debug: 'drained_stale_message', message_keys: msg.keys, drained_count: drained })
105
+ end
106
+ @hooks&.emit(:ws_message, { _debug: 'drain_complete', count: drained }) if drained > 0
107
+ end
108
+
90
109
  def collect_response(timeout: Antigravity.config.timeout_llm, &block)
91
110
  text_parts = []
92
111
  thinking_parts = []
@@ -94,11 +113,14 @@ module Antigravity
94
113
  tool_calls_count = 0
95
114
  finished = false
96
115
  finished_at = nil
116
+ seen_any_step = false # GHI #18: track if we've seen real work from this turn
117
+ turn_started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
97
118
 
98
119
  @ws.each_message(timeout: timeout) do |msg|
99
120
  @hooks&.emit(:ws_message, msg)
100
121
 
101
122
  if (step = msg[:stepUpdate])
123
+ seen_any_step = true
102
124
  step_record = parse_step(step)
103
125
  steps << step_record
104
126
 
@@ -109,10 +131,9 @@ module Antigravity
109
131
 
110
132
  if step[:textDelta] && !step[:textDelta].empty? && is_model_step && is_target_user && !is_error_step
111
133
  text_parts << step[:textDelta]
112
- chunk = Message.new(
134
+ chunk = Chunk.new(
113
135
  content: step[:textDelta],
114
- role: :assistant,
115
- delta: true
136
+ role: :assistant
116
137
  )
117
138
  block&.call(chunk)
118
139
  end
@@ -144,30 +165,48 @@ module Antigravity
144
165
  # Top-level tool call (custom tools are sent as separate messages, not in stepUpdate)
145
166
  if (tool_call = msg[:toolCall])
146
167
  tool_calls_count += 1
168
+ seen_any_step = true
147
169
  handle_tool_call(tool_call)
148
170
  end
149
171
 
150
- # Usage update
172
+ # Usage update — do NOT set seen_any_step here!
173
+ # usageUpdate can leak from the previous turn and trick the GHI #18
174
+ # stale-FULLY_IDLE guard into accepting a stale FULLY_IDLE as real.
151
175
  if (usage = msg[:usageUpdate])
152
176
  update_usage(usage)
153
177
  end
154
178
 
155
179
  # Trajectory state: STATE_FULLY_IDLE / STATE_CANCELLED = turn complete (authoritative signal from harness)
156
- # This is the MOST authoritative signal always stop, even with empty text.
180
+ # GHI #18 + #24 FIX: Only honor FULLY_IDLE if we've seen at least one stepUpdate or toolCall
181
+ # (NOT usageUpdate — it leaks across turns) from this turn, OR if enough time has elapsed
182
+ # (2s) that this can't be a stale leftover. A stale FULLY_IDLE from a previous turn sitting
183
+ # in the WebSocket buffer was causing collect_response to return immediately with 0B text.
157
184
  if (traj = msg[:trajectoryStateUpdate])
158
185
  if traj[:state].to_s =~ /FULLY_IDLE|CANCELLED/
159
- finished = true
160
- finished_at = Time.now
186
+ if seen_any_step
187
+ finished = true
188
+ finished_at = Time.now
189
+ else
190
+ # Stale FULLY_IDLE — skip it (likely leftover from previous turn or init)
191
+ @hooks&.emit(:ws_message, { _debug: 'skipped_stale_fully_idle', seen_any_step: seen_any_step })
192
+ end
161
193
  end
162
194
  end
163
195
 
164
196
  # Stop conditions (in priority order):
165
- # 1. Session end — always stop
166
- # 2. Model response DONE or trajectory FULLY_IDLE / CANCELLEDturn complete
167
- if msg.key?(:sessionEndResponse) || finished
197
+ # 1. Session end — always hard-stop immediately
198
+ # 2. Finished (DONE or FULLY_IDLE) WITH textshort drain for trailing usage
199
+ # 3. Finished WITHOUT text but with steps — model was working (thinking),
200
+ # text is likely still in-flight. Drain longer (3s) to catch it. (GHI #24)
201
+ # 4. Not finished but text started — idle timeout for more text
202
+ if msg.key?(:sessionEndResponse)
168
203
  :stop
204
+ elsif finished && !text_parts.empty?
205
+ [:idle_timeout, 0.5]
206
+ elsif finished && text_parts.empty?
207
+ # FULLY_IDLE arrived but no text yet — wait longer for trailing text
208
+ [:idle_timeout, 3.0]
169
209
  elsif !text_parts.empty?
170
- # If assistant has sent text response, allow 3s idle timeout for trailing metadata
171
210
  [:idle_timeout, 3.0]
172
211
  end
173
212
  end
@@ -208,10 +247,40 @@ module Antigravity
208
247
  # Symbolize keys for Ruby kwargs
209
248
  kwargs = args.transform_keys(&:to_sym)
210
249
 
211
- begin
212
- result = @tool_runner.execute(tool_name, **kwargs)
213
- rescue ToolNotFoundError => e
214
- result = { error: e.message }
250
+ # Emit tool_call hook BEFORE execution
251
+ @hooks&.emit(:tool_call, { tool_name: tool_name, params: args, tool_id: tool_id })
252
+
253
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
254
+ policy_check = @hooks ? @hooks.run_pre_tool(tool_name, args) : { allowed: true }
255
+
256
+ if !policy_check[:allowed]
257
+ reason = policy_check[:reason]
258
+ # Same format as client.rb sidecar emission
259
+ @hooks&.emit(:tool_blocked, { tool: tool_name, reason: reason })
260
+ result = "❌ TOOL BLOCKED: #{reason}"
261
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
262
+
263
+ # We run the post_tool hook here as well to satisfy AgentLogger's pairing
264
+ result = @hooks ? @hooks.run_post_tool(tool_name, args, result) : result
265
+ @hooks&.emit(:tool_result, { tool_name: tool_name, result: result.to_s, duration: duration, tool_id: tool_id })
266
+ else
267
+ begin
268
+ raw_result = @tool_runner.execute(tool_name, **kwargs)
269
+
270
+ # Run post_tool filters/maskers
271
+ result = @hooks ? @hooks.run_post_tool(tool_name, args, raw_result) : raw_result
272
+
273
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
274
+
275
+ # Emit tool_result hook AFTER execution
276
+ @hooks&.emit(:tool_result, { tool_name: tool_name, result: result.to_s, duration: duration, tool_id: tool_id })
277
+ rescue ToolNotFoundError => e
278
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
279
+ result = { error: e.message }
280
+
281
+ # Emit tool_error hook on failure
282
+ @hooks&.emit(:tool_error, { tool_name: tool_name, error: e.message, duration: duration, tool_id: tool_id })
283
+ end
215
284
  end
216
285
 
217
286
  # Send tool response back (protobuf InputEvent.tool_response format)
@@ -22,8 +22,7 @@ module Antigravity
22
22
  return true
23
23
  end
24
24
 
25
- cmd = "#{@bin_path} --port=#{@port}"
26
- @stdin, @stdout, @stderr, wait_thr = Open3.popen3(cmd)
25
+ @stdin, @stdout, @stderr, wait_thr = Open3.popen3(@bin_path, "--port=#{@port}")
27
26
  @pid = wait_thr.pid
28
27
 
29
28
  at_exit { stop! }
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Auto-attachable lifecycle logger that prints colorful, compact status lines
4
+ # on every hook event. Inspired by Cloud Code's status bar.
5
+ #
6
+ # Usage:
7
+ # agent = Antigravity::Agent.new
8
+ # Antigravity::LifecycleLogger.attach!(agent)
9
+ #
10
+ # Or auto-attach via env:
11
+ # ANTIGRAVITY_LIFECYCLE=1
12
+ # RAILS_ENV=test (auto-attaches in test/development)
13
+ #
14
+ module Antigravity
15
+ class LifecycleLogger
16
+ C = Antigravity::Colors
17
+
18
+ # Compact status string — the "Cloud Code status bar" equivalent.
19
+ # Example: "T3 | 1.2k tok | 4 tools | 2.3s"
20
+ def self.status_line(agent)
21
+ turns = agent.turn_count rescue 0
22
+ summary = agent.session_summary rescue {}
23
+ tokens = summary[:total_tokens] || 0
24
+ tok_str = tokens > 999 ? "#{(tokens / 1000.0).round(1)}k" : tokens.to_s
25
+ tok_str = "🪙#{tok_str}"
26
+ model = summary[:model] || agent.model || '?'
27
+ conv_id = (summary[:conversation_id] || '?')[0..7]
28
+
29
+ C.dim("T#{turns} | #{tok_str} tok | #{model} | #{conv_id}")
30
+ end
31
+
32
+ def self.attach!(agent, verbose: false)
33
+ new(verbose: verbose).attach(agent)
34
+ end
35
+
36
+ def initialize(verbose: false)
37
+ @verbose = verbose
38
+ @session_start_time = nil
39
+ @turn_start_time = nil
40
+ @turn_count = 0
41
+ end
42
+
43
+ def attach(agent)
44
+ attach_session_hooks(agent)
45
+ attach_turn_hooks(agent)
46
+ attach_tool_hooks(agent)
47
+ end
48
+
49
+ private
50
+
51
+ def attach_session_hooks(agent)
52
+ logger = self
53
+
54
+ agent.hooks.on(:session_start) do |info|
55
+ logger.instance_variable_set(:@session_start_time, Process.clock_gettime(Process::CLOCK_MONOTONIC))
56
+ model = info[:model] || agent.model || '?'
57
+ conv_id = (info[:conversation_id] || '?')[0..11]
58
+ puts C.gray("\n🪝🟢 #{C.dim("session_start")} | model=#{C.cyan(model)} | conv=#{C.cyan(conv_id)}")
59
+ end
60
+
61
+ agent.hooks.on(:indexing_start) do |info|
62
+ ws = info[:workspace] || '?'
63
+ puts C.gray("🪝 📂 #{C.dim("indexing")} | #{C.blue(ws)}")
64
+ end
65
+
66
+ agent.hooks.on(:indexing_done) do |info|
67
+ ws = info[:workspace] || '?'
68
+ elapsed = info[:elapsed] ? "#{info[:elapsed]}s" : '?'
69
+ puts C.gray("🪝 ✅ #{C.dim("indexed")} | #{C.blue(ws)} | #{C.green(elapsed)}")
70
+ end
71
+
72
+ agent.hooks.on(:session_end) do |info|
73
+ elapsed = if logger.instance_variable_get(:@session_start_time)
74
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - logger.instance_variable_get(:@session_start_time)).round(1)
75
+ else
76
+ '?'
77
+ end
78
+ turns = info[:turn_count] || agent.turn_count rescue 0
79
+ summary = agent.session_summary rescue {}
80
+ tokens = summary[:total_tokens] || 0
81
+ tok_str = tokens > 999 ? "#{(tokens / 1000.0).round(1)}k" : tokens.to_s
82
+ puts C.gray("🪝🔴 #{C.dim("session_end")} | #{C.bold("#{turns} turns")} | 🪙#{tok_str} | #{elapsed}s")
83
+ end
84
+ end
85
+
86
+ def attach_turn_hooks(agent)
87
+ logger = self
88
+
89
+ agent.hooks.before_prompt do |text|
90
+ logger.instance_variable_set(:@turn_start_time, Process.clock_gettime(Process::CLOCK_MONOTONIC))
91
+ count = logger.instance_variable_get(:@turn_count) + 1
92
+ logger.instance_variable_set(:@turn_count, count)
93
+ preview = text.to_s[0..60].gsub("\n", ' ')
94
+ preview += '...' if text.to_s.length > 60
95
+ status = self.class.status_line(agent) rescue C.dim("T#{count}")
96
+ puts C.gray("🪝 ➡️ #{C.dim("pre_turn")} T#{count} | #{C.yellow("\"#{preview}\"")} | #{status}")
97
+ end
98
+
99
+ agent.hooks.after_response do |response|
100
+ elapsed = if logger.instance_variable_get(:@turn_start_time)
101
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - logger.instance_variable_get(:@turn_start_time)).round(2)
102
+ else
103
+ '?'
104
+ end
105
+ content = response.respond_to?(:content) ? response.content.to_s : response.to_s
106
+ chars = content.length
107
+ lines = content.count("\n") + 1
108
+ preview = content[0..60]&.gsub("\n", ' ') || '(empty)'
109
+ preview += '...' if chars > 60
110
+ thinking_len = response.respond_to?(:thinking) ? (response.thinking&.length || 0) : 0
111
+ tool_count = response.respond_to?(:tool_calls_count) ? (response.tool_calls_count || 0) : 0
112
+ status = self.class.status_line(agent) rescue ''
113
+
114
+ parts = ["#{C.green("#{chars}B")} #{C.dim("#{lines}L")}"]
115
+ parts << "#{C.magenta("#{thinking_len}B")} think" if thinking_len > 0
116
+ parts << "#{C.cyan("#{tool_count}")} tools" if tool_count > 0
117
+ parts << "#{C.blue("#{elapsed}s")}"
118
+
119
+ puts C.gray("\n🪝 ⬅️ #{C.dim("post_turn")} T#{logger.instance_variable_get(:@turn_count)} | #{parts.join(' | ')} | #{status}")
120
+ if logger.instance_variable_get(:@verbose)
121
+ puts C.dim(" \"#{preview}\"")
122
+ end
123
+ end
124
+ end
125
+
126
+ def attach_tool_hooks(agent)
127
+ logger = self
128
+
129
+ agent.hooks.on(:tool_call) do |info|
130
+ name = info[:tool_name] || info[:name] || '?'
131
+ params_preview = (info[:params] || {}).keys.join(', ')
132
+ puts C.gray(" 🪝 🔧 #{C.dim("tool_call")} | #{C.cyan(name)}(#{C.dim(params_preview)})")
133
+ end
134
+
135
+ agent.hooks.on(:tool_result) do |info|
136
+ name = info[:tool_name] || info[:name] || '?'
137
+ result_str = info[:result].to_s
138
+ result_len = result_str.bytesize rescue 0
139
+ preview = result_str[0..120].gsub("\n", ' ')
140
+ preview += '...' if result_str.length > 120
141
+ duration = info[:duration] ? "#{info[:duration].round(2)}s" : nil
142
+ parts = [C.cyan(name), "#{result_len}B"]
143
+ parts << duration if duration
144
+ puts C.gray(" 🪝 ✅ #{C.dim("tool_done")} | #{parts.join(' | ')}")
145
+ puts C.dim(" → #{C.yellow(preview)}") if result_len > 0
146
+ end
147
+
148
+ agent.hooks.on(:tool_blocked) do |info|
149
+ name = info[:tool] || '?'
150
+ reason = info[:reason] || 'policy'
151
+ puts C.red(" 🪝 🚫 #{C.dim("tool_deny")} | #{C.red(name)} — #{reason}")
152
+ end
153
+
154
+ agent.hooks.on(:tool_error) do |info|
155
+ name = info[:tool_name] || info[:name] || '?'
156
+ error = info[:error] || info[:message] || '?'
157
+ puts C.red(" 🪝 💥 #{C.dim("tool_error")} | #{C.red(name)} — #{error.to_s[0..80]}")
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ==========================================================================
4
+ # Policy Constants — curated lists of commands, tools, and file patterns.
5
+ #
6
+ # 🛠️ MAINTAINER NOTES:
7
+ # - One entry per line for easy review in PRs.
8
+ # - Add new entries alphabetically within each group.
9
+ # - Multi-word commands use explicit quotes (no backslash escapes!).
10
+ # - Run `bundle exec rspec spec/antigravity/policy_spec.rb` after edits.
11
+ # ==========================================================================
12
+
13
+ module Antigravity
14
+ class Policy
15
+
16
+ # ------------------------------------------------------------------
17
+ # 💀 Catastrophic commands — hard-denied in ALL presets, no exceptions.
18
+ # ------------------------------------------------------------------
19
+ CATASTROPHIC_CMDS = [
20
+ 'dd if=/dev/urandom',
21
+ 'dd if=/dev/zero',
22
+ '> /dev/sd',
23
+ 'halt',
24
+ 'mkfs',
25
+ 'reboot',
26
+ 'rm -rf /*',
27
+ 'rm -rf /',
28
+ 'rm -rf ~',
29
+ 'shutdown',
30
+ ].freeze
31
+
32
+ # ------------------------------------------------------------------
33
+ # ⚠️ Risky commands — confirmed in :default, hard-denied in :cautious.
34
+ # Single-word or short patterns that match substring in command_line.
35
+ # ------------------------------------------------------------------
36
+ RISKY_CMDS = [
37
+ 'chmod -R 777',
38
+ 'chown -R',
39
+ 'kill -9',
40
+ 'killall',
41
+ 'pkill',
42
+ 'rm',
43
+ 'xargs',
44
+ ].freeze
45
+
46
+ # ------------------------------------------------------------------
47
+ # 🔥 Destructive git — nuke local changes, rewrite history.
48
+ # Confirmed in :default/:turbo, hard-denied in :cautious/:test.
49
+ # ------------------------------------------------------------------
50
+ DESTRUCTIVE_GIT_CMDS = [
51
+ 'git checkout .',
52
+ 'git checkout -- .',
53
+ 'git clean -fd',
54
+ 'git clean -fdx',
55
+ 'git push --force',
56
+ 'git push -f',
57
+ 'git reset --hard',
58
+ 'git stash drop',
59
+ ].freeze
60
+
61
+ # ------------------------------------------------------------------
62
+ # ✅ Safe read-only shell commands — allowed even in :cautious.
63
+ # ------------------------------------------------------------------
64
+ SAFE_CMDS = %i[
65
+ cd
66
+ date
67
+ echo
68
+ hostname
69
+ ls
70
+ md5
71
+ md5sum
72
+ pwd
73
+ uname
74
+ wc
75
+ which
76
+ whoami
77
+ ].freeze
78
+
79
+ # ⚠️ File-reading shell commands — can bypass view_file deny rules!
80
+ # Allowed in :default/:turbo/:test, but NOT in :cautious.
81
+ # If you deny `view_file` for a path, `cat` can circumvent it.
82
+ READ_CMDS = %i[
83
+ cat
84
+ head
85
+ strings
86
+ tail
87
+ ].freeze
88
+
89
+ # Safe git subcommands (read-only, no mutations)
90
+ SAFE_GIT_CMDS = [
91
+ 'git branch',
92
+ 'git diff',
93
+ 'git log',
94
+ 'git remote',
95
+ 'git status',
96
+ ].freeze
97
+
98
+ # ------------------------------------------------------------------
99
+ # 🔐 Sensitive file globs — writes to these require confirmation.
100
+ # ------------------------------------------------------------------
101
+ SENSITIVE_FILES = [
102
+ '.env',
103
+ '.env.*',
104
+ '*.key',
105
+ '*.pem',
106
+ '*.secret',
107
+ 'id_rsa*',
108
+ ].freeze
109
+
110
+ # ------------------------------------------------------------------
111
+ # 📂 Sandbox directories — always writable, even in production.
112
+ # Throwaway / output dirs where agents can freely write.
113
+ # ------------------------------------------------------------------
114
+ SANDBOX_DIRS = [
115
+ 'out/*',
116
+ 'scratch/*',
117
+ ].freeze
118
+
119
+ # ------------------------------------------------------------------
120
+ # 🔧 Tool classifications
121
+ # ------------------------------------------------------------------
122
+
123
+ # Read-only harness tools (always safe)
124
+ READONLY_TOOLS = %i[
125
+ find
126
+ grep_search
127
+ list_dir
128
+ read_url_content
129
+ search_web
130
+ view_file
131
+ ].freeze
132
+
133
+ # Write harness tools
134
+ WRITE_TOOLS = %i[
135
+ file_edit
136
+ write_to_file
137
+ ].freeze
138
+
139
+ # ------------------------------------------------------------------
140
+ # 🗺️ Environment → preset mapping (for Policy.auto)
141
+ # ------------------------------------------------------------------
142
+ PRESET_NAMES = %i[auto cautious default test turbo].freeze
143
+
144
+ ENV_MAP = {
145
+ 'dev' => :turbo,
146
+ 'development' => :turbo,
147
+ 'prod' => :cautious,
148
+ 'production' => :cautious,
149
+ 'staging' => :default,
150
+ 'test' => :test,
151
+ }.freeze
152
+
153
+ end
154
+ end
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'policy/constants'
4
+
5
+ module Antigravity
6
+ # ==========================================================================
7
+ # Antigravity::Policy — Declarative tool-access control for agents.
8
+ #
9
+ # ⚠️ ORDER DOES NOT MATTER!
10
+ #
11
+ # The DSL is declarative, like SQL — not imperative like a script.
12
+ # Rules are evaluated by PRECEDENCE, not by insertion order.
13
+ # You can write `allow` before `deny` or vice versa — same result.
14
+ #
15
+ # Precedence (highest wins):
16
+ # 1. Tool specificity: specific tool > wildcard (nil)
17
+ # 2. Condition specificity: has `when:` > no `when:`
18
+ # 3. Restrictiveness: deny > confirm > allow
19
+ #
20
+ # Example — these two policies behave identically:
21
+ #
22
+ # Policy.define do Policy.define do
23
+ # allow :run_command deny :run_command, when: cmd('rm')
24
+ # deny :run_command, allow :run_command
25
+ # when: cmd('rm') end
26
+ # end
27
+ #
28
+ # In both cases, `rm` is denied (conditional deny beats unconditional
29
+ # allow), and everything else is allowed.
30
+ #
31
+ # See policy/constants.rb for curated command/file/tool lists.
32
+ # ==========================================================================
33
+ class Policy
34
+
35
+ # ------------------------------------------------------------------
36
+ # Rule — a single allow/deny/confirm entry in a policy.
37
+ # ------------------------------------------------------------------
38
+ class Rule
39
+ attr_reader :action, :tool_name, :condition, :handler
40
+
41
+ def initialize(action, tool_name = nil, condition: nil, handler: nil)
42
+ @action = action
43
+ @tool_name = tool_name
44
+ @condition = condition
45
+ @handler = handler
46
+ end
47
+
48
+ def matches?(tool, args)
49
+ return false if @tool_name && @tool_name.to_sym != tool.to_sym
50
+ return false if @condition && !@condition.call(name: tool, args: args)
51
+ true
52
+ end
53
+
54
+ # Precedence order (higher = wins):
55
+ # 1. Tool specificity: Specific tool > Wildcard (nil)
56
+ # 2. Condition specificity: Has predicate > No predicate
57
+ # 3. Action restrictiveness: Deny > Confirm > Allow
58
+ def precedence
59
+ specificity = @tool_name ? 1 : 0
60
+ condition_score = @condition ? 1 : 0
61
+ action_score = case @action
62
+ when :deny then 3
63
+ when :confirm then 2
64
+ when :allow then 1
65
+ else 0
66
+ end
67
+ [specificity, condition_score, action_score]
68
+ end
69
+ end
70
+
71
+ # ------------------------------------------------------------------
72
+ # Constructor & factory methods
73
+ # ------------------------------------------------------------------
74
+
75
+ def initialize(&block)
76
+ @rules = []
77
+ @confirm_handler = nil
78
+ instance_eval(&block) if block_given?
79
+ end
80
+
81
+ def self.define(&block)
82
+ new(&block)
83
+ end
84
+
85
+ def self.allow_all
86
+ new { allow_all }
87
+ end
88
+
89
+ def self.deny_all
90
+ new { deny_all }
91
+ end
92
+
93
+ # ------------------------------------------------------------------
94
+ # Built-in presets
95
+ # ------------------------------------------------------------------
96
+
97
+ # Resolve a preset by name (symbol).
98
+ # @param name [Symbol] :cautious, :default, :turbo, :test, or :auto
99
+ # @return [Policy]
100
+ def self.preset(name)
101
+ case name.to_sym
102
+ when :cautious then cautious
103
+ when :default then default
104
+ when :turbo then turbo
105
+ when :test then test
106
+ when :auto then auto
107
+ else
108
+ raise ArgumentError, "Unknown preset :#{name}. Choose from: #{PRESET_NAMES.map { |n| ":#{n}" }.join(', ')}"
109
+ end
110
+ end
111
+
112
+ # 🔒 Cautious — read-only free, confirm everything else, hard-deny destructive.
113
+ # Best for: untrusted environments, production agents.
114
+ # NOTE: cat/head/tail/ls NOT in safe list — they can bypass view_file deny rules.
115
+ def self.cautious
116
+ define do
117
+ deny_all
118
+ READONLY_TOOLS.each { |t| allow t }
119
+ allow :run_command, when: cmd(*SAFE_CMDS, *SAFE_GIT_CMDS)
120
+ deny :run_command, when: cmd(*CATASTROPHIC_CMDS)
121
+ deny :run_command, when: cmd(*RISKY_CMDS)
122
+ deny :run_command, when: cmd(*DESTRUCTIVE_GIT_CMDS)
123
+ WRITE_TOOLS.each { |t| confirm t }
124
+ # 📂 Sandbox dirs: always writable, even in production
125
+ WRITE_TOOLS.each { |t| allow t, when: path(*SANDBOX_DIRS) }
126
+ confirm :run_command
127
+ end
128
+ end
129
+
130
+ # ⚖️ Default — balanced: allow reads + writes, confirm dangerous shell, protect sensitive files.
131
+ # Best for: day-to-day development, pair programming with an agent.
132
+ def self.default
133
+ define do
134
+ deny_all
135
+ READONLY_TOOLS.each { |t| allow t }
136
+ WRITE_TOOLS.each { |t| allow t }
137
+ WRITE_TOOLS.each { |t| confirm t, when: path(*SENSITIVE_FILES) }
138
+ allow :run_command
139
+ deny :run_command, when: cmd(*CATASTROPHIC_CMDS)
140
+ confirm :run_command, when: cmd(*RISKY_CMDS)
141
+ confirm :run_command, when: cmd(*DESTRUCTIVE_GIT_CMDS)
142
+ end
143
+ end
144
+
145
+ # 🚀 Turbo — wide open with seatbelts: allow everything, only hard-deny catastrophic.
146
+ # Best for: trusted dev environments, rapid prototyping.
147
+ def self.turbo
148
+ define do
149
+ allow_all
150
+ deny :run_command, when: cmd(*CATASTROPHIC_CMDS)
151
+ confirm :run_command, when: cmd(*DESTRUCTIVE_GIT_CMDS)
152
+ WRITE_TOOLS.each { |t| confirm t, when: path(*SENSITIVE_FILES) }
153
+ end
154
+ end
155
+
156
+ # 🧪 Test — permissive for test runners, but sandboxed.
157
+ # Best for: CI, test suites, RAILS_ENV=test.
158
+ def self.test
159
+ define do
160
+ allow_all
161
+ deny :run_command, when: cmd(*CATASTROPHIC_CMDS)
162
+ deny :run_command, when: cmd(*DESTRUCTIVE_GIT_CMDS)
163
+ confirm :run_command, when: cmd(*RISKY_CMDS)
164
+ WRITE_TOOLS.each { |t| confirm t, when: path(*SENSITIVE_FILES) }
165
+ end
166
+ end
167
+
168
+ # 🔮 Auto — reads RAILS_ENV, RACK_ENV, or ANTIGRAVITY_ENV and picks a preset.
169
+ # Falls back to :default if unrecognized or unset.
170
+ def self.auto
171
+ env = ENV['ANTIGRAVITY_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV']
172
+ preset_name = ENV_MAP.fetch(env.to_s.downcase, :default)
173
+ send(preset_name)
174
+ end
175
+
176
+ # ------------------------------------------------------------------
177
+ # DSL methods
178
+ # ------------------------------------------------------------------
179
+
180
+ def allow(tool_name = nil, **kwargs)
181
+ @rules << Rule.new(:allow, tool_name, condition: kwargs[:when])
182
+ end
183
+
184
+ def deny(tool_name = nil, **kwargs)
185
+ @rules << Rule.new(:deny, tool_name, condition: kwargs[:when])
186
+ end
187
+
188
+ def confirm(tool_name = nil, **kwargs, &block)
189
+ @rules << Rule.new(:confirm, tool_name, condition: kwargs[:when], handler: block)
190
+ end
191
+
192
+ def allow_all
193
+ allow(nil)
194
+ end
195
+
196
+ def deny_all
197
+ deny(nil)
198
+ end
199
+
200
+ def on_confirm(&block)
201
+ @confirm_handler = block
202
+ end
203
+
204
+ # ------------------------------------------------------------------
205
+ # Predicate helpers
206
+ # ------------------------------------------------------------------
207
+
208
+ def cmd(*patterns)
209
+ ->(ctx) do
210
+ args = ctx[:args]
211
+ cmd_arg = args[:command_line] || args['command_line'] || args[:CommandLine] || args['CommandLine']
212
+ return false unless cmd_arg
213
+
214
+ cmd_arg = cmd_arg.to_s
215
+ patterns.any? { |p| cmd_arg.include?(p.to_s) }
216
+ end
217
+ end
218
+
219
+ def path(*globs)
220
+ ->(ctx) do
221
+ args = ctx[:args]
222
+ path_arg = args[:path] || args['path'] ||
223
+ args[:file] || args['file'] ||
224
+ args[:target] || args['target'] ||
225
+ args[:file_path] || args['file_path'] ||
226
+ args[:target_file] || args['target_file']
227
+ return false unless path_arg
228
+
229
+ path_arg = path_arg.to_s
230
+ globs.any? { |g| File.fnmatch?(g.to_s, path_arg) }
231
+ end
232
+ end
233
+
234
+ def args_match(**matchers)
235
+ ->(ctx) do
236
+ args = ctx[:args]
237
+ matchers.any? do |k, v|
238
+ val = args[k.to_sym] || args[k.to_s]
239
+ val && v.match?(val.to_s)
240
+ end
241
+ end
242
+ end
243
+
244
+ # ------------------------------------------------------------------
245
+ # Evaluation engine
246
+ # ------------------------------------------------------------------
247
+
248
+ def evaluate(tool_name, args = {})
249
+ matching_rules = @rules.select { |r| r.matches?(tool_name, args) }
250
+ best_rule = matching_rules.max_by(&:precedence)
251
+
252
+ if best_rule
253
+ if best_rule.action == :confirm
254
+ handler = best_rule.handler || @confirm_handler
255
+ if handler
256
+ ctx = { name: tool_name, args: args }
257
+ result = handler.call(ctx)
258
+ { status: result ? :allow : :deny }
259
+ else
260
+ { status: :deny }
261
+ end
262
+ elsif best_rule.action == :deny
263
+ { status: :deny, reason: "Denied by policy" }
264
+ else
265
+ { status: :allow }
266
+ end
267
+ else
268
+ { status: :deny } # Default to deny if no rules match
269
+ end
270
+ end
271
+ end
272
+ end
@@ -71,7 +71,11 @@ module Antigravity
71
71
 
72
72
  content = File.read(skill_file, encoding: 'UTF-8')
73
73
  if content =~ /\A(---\s*\n.*?\n?)^(---\s*$\n?)/m
74
- front_matter = YAML.safe_load(Regexp.last_match(1)) || {}
74
+ front_matter = begin
75
+ YAML.safe_load(Regexp.last_match(1)) || {}
76
+ rescue Psych::SyntaxError => e
77
+ raise ArgumentError, "Invalid YAML frontmatter in #{skill_file}: #{e.message}"
78
+ end
75
79
  @name = front_matter["name"] || File.basename(@path)
76
80
  @description = front_matter["description"] || ""
77
81
  @metadata = front_matter.fetch("metadata", {})
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Antigravity
4
- VERSION = File.read(File.expand_path("../../VERSION", __dir__)).strip rescue "0.1.0"
4
+ VERSION = begin
5
+ version_file = [
6
+ File.expand_path("../../VERSION", __dir__),
7
+ File.expand_path("../VERSION", __dir__)
8
+ ].find { |f| File.exist?(f) }
9
+ version_file ? File.read(version_file).strip : "0.5.0"
10
+ end
5
11
  end
data/lib/antigravity.rb CHANGED
@@ -14,6 +14,7 @@ require_relative "antigravity/config"
14
14
  require_relative "antigravity/message"
15
15
  require_relative "antigravity/protocol"
16
16
  require_relative "antigravity/harness"
17
+ require_relative "antigravity/policy"
17
18
  require_relative "antigravity/hooks"
18
19
  require_relative "antigravity/guards"
19
20
  require_relative "antigravity/sidecar"
@@ -26,6 +27,8 @@ require_relative "antigravity/connection/binary_fetcher"
26
27
  require_relative "antigravity/connection/websocket_client"
27
28
  require_relative "antigravity/connection/local_connection"
28
29
  require_relative "antigravity/conversation"
30
+ require_relative "antigravity/colors"
31
+ require_relative "antigravity/lifecycle_logger"
29
32
  require_relative "antigravity/agent"
30
33
 
31
34
  module Antigravity
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: antigravity-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Riccardo Carlesso
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '1.5'
54
+ - !ruby/object:Gem::Dependency
55
+ name: dotenv
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.0'
54
68
  description: An elegant, Ruby-like SDK for building autonomous AI agents with Google
55
69
  Antigravity.
56
70
  email:
@@ -59,10 +73,12 @@ executables: []
59
73
  extensions: []
60
74
  extra_rdoc_files: []
61
75
  files:
76
+ - VERSION
62
77
  - lib/antigravity.rb
63
78
  - lib/antigravity/agent.rb
64
79
  - lib/antigravity/base.rb
65
80
  - lib/antigravity/client.rb
81
+ - lib/antigravity/colors.rb
66
82
  - lib/antigravity/config.rb
67
83
  - lib/antigravity/connection/binary_fetcher.rb
68
84
  - lib/antigravity/connection/local_connection.rb
@@ -73,7 +89,10 @@ files:
73
89
  - lib/antigravity/guards.rb
74
90
  - lib/antigravity/harness.rb
75
91
  - lib/antigravity/hooks.rb
92
+ - lib/antigravity/lifecycle_logger.rb
76
93
  - lib/antigravity/message.rb
94
+ - lib/antigravity/policy.rb
95
+ - lib/antigravity/policy/constants.rb
77
96
  - lib/antigravity/protocol.rb
78
97
  - lib/antigravity/sidecar.rb
79
98
  - lib/antigravity/skill.rb