@zalom/plastic 1.0.0-alpha.18 → 1.0.0-alpha.19

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,53 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ # PreToolUse gate for Bash (intent 27a): close the Bash-edit bypass. When auto
6
+ # mode is armed and the active intent has not reached How (plan.md + checklist.md),
7
+ # block Bash commands that WRITE project code outside the store.
8
+ #
9
+ # Reads the Claude Code PreToolUse payload as JSON on STDIN:
10
+ # { "tool_input": { "command": "..." }, "cwd": "..." }
11
+ # Empty / unparseable / no command => exit 0 (allow). Conservatism is the prime
12
+ # directive: when a command's write intent is ambiguous, ALLOW.
13
+ #
14
+ # Exit 0 = allow. Exit 2 = block (reason on stderr, shown to the agent).
15
+
16
+ require "json"
17
+ require_relative "lib/bridge"
18
+
19
+ raw = $stdin.read rescue nil
20
+ exit 0 if raw.nil? || raw.strip.empty?
21
+
22
+ payload = JSON.parse(raw) rescue nil
23
+ exit 0 unless payload.is_a?(Hash)
24
+
25
+ command = payload.dig("tool_input", "command") || payload.dig("tool_params", "command")
26
+ exit 0 if command.nil? || command.to_s.strip.empty?
27
+
28
+ cwd = payload["cwd"]
29
+ cwd = Dir.pwd if cwd.nil? || cwd.to_s.empty?
30
+
31
+ # --- Load bridge (mirror hook-code-gate resolution) ---
32
+ session = ENV["CLAUDE_SESSION_ID"]
33
+ bridge_data = nil
34
+
35
+ if session && !session.empty?
36
+ bridge_data = Bridge.read(session) if File.exist?(Bridge.path(session))
37
+ end
38
+
39
+ unless bridge_data
40
+ candidates = Dir.glob("/tmp/plastic-*.json").reject { |f| f.end_with?(".tmp") }
41
+ if candidates.any?
42
+ newest = candidates.max_by { |f| File.mtime(f) }
43
+ bridge_data = JSON.parse(File.read(newest)) rescue nil
44
+ end
45
+ end
46
+
47
+ exit 0 unless bridge_data
48
+
49
+ reason = Bridge.bash_gate_decision(bridge_data, command, cwd: cwd)
50
+ exit 0 unless reason
51
+
52
+ $stderr.puts "PLASTIC GATE — #{reason}"
53
+ exit 2
@@ -172,53 +172,57 @@ if File.exist?(cache_file)
172
172
  end
173
173
  end
174
174
 
175
- # --- Early exit only if no PLASTIC.md ---
175
+ # --- Assemble session context ---
176
176
 
177
177
  all_active = active + project_active
178
178
  all_future = future + project_future
179
- exit 0 unless plastic_md
180
179
 
181
180
  # --- Build context ---
181
+ # PLASTIC.md conventions and intent context render only when conventions are
182
+ # installed. Deprecation warnings and update notices render regardless, so a
183
+ # partial install (or a missing PLASTIC.md) still surfaces critical warnings.
182
184
 
183
185
  parts = []
184
186
 
185
- # Conventions always loaded first
186
- parts << plastic_md
187
- parts << "\n---\n"
188
-
189
- if current_project
190
- slug = current_project["slug"]
191
- banner = "Project: #{slug} | Store: ~/.plastic/projects/#{slug}/store/"
192
- if project_active.any? && project_active.first =~ /\[([^\]]+)\].*store\/([\w-]+)\//
193
- intent_name, dir_name = $1, $2
194
- intent_id = dir_name.split("--").first
195
- banner += "\nActive: [#{intent_id} — #{intent_name}] | Artifacts → store/#{dir_name}/"
187
+ if plastic_md
188
+ # Conventions always loaded first
189
+ parts << plastic_md
190
+ parts << "\n---\n"
191
+
192
+ if current_project
193
+ slug = current_project["slug"]
194
+ banner = "Project: #{slug} | Store: ~/.plastic/projects/#{slug}/store/"
195
+ if project_active.any? && project_active.first =~ /\[([^\]]+)\].*store\/([\w-]+)\//
196
+ intent_name, dir_name = $1, $2
197
+ intent_id = dir_name.split("--").first
198
+ banner += "\nActive: [#{intent_id} — #{intent_name}] | Artifacts → store/#{dir_name}/"
199
+ end
200
+ parts << banner + "\n"
201
+ else
202
+ parts << "PLASTIC — Global store loaded from ~/.plastic/\n"
196
203
  end
197
- parts << banner + "\n"
198
- else
199
- parts << "PLASTIC — Global store loaded from ~/.plastic/\n"
200
- end
201
204
 
202
- if all_active.any?
203
- parts << "Active intents:\n"
204
- all_active.each { |a| parts << a }
205
- if bridge_data
206
- stage = bridge_data["build"]["stage"]
207
- missing = bridge_data["build"]["missing"]
208
- missing_str = missing.empty? ? "none" : missing.join(", ")
209
- parts << "Stage: #{stage} | Next: #{missing_str}"
205
+ if all_active.any?
206
+ parts << "Active intents:\n"
207
+ all_active.each { |a| parts << a }
208
+ if bridge_data
209
+ stage = bridge_data["build"]["stage"]
210
+ missing = bridge_data["build"]["missing"]
211
+ missing_str = missing.empty? ? "none" : missing.join(", ")
212
+ parts << "Stage: #{stage} | Next: #{missing_str}"
213
+ end
214
+ parts << ""
215
+ else
216
+ parts << "No active intents. #{future.length} future intents available.\n"
210
217
  end
211
- parts << ""
212
- else
213
- parts << "No active intents. #{future.length} future intents available.\n"
214
- end
215
218
 
216
- if stale.any?
217
- parts << "\nStale future intents (untouched for days):\n"
218
- stale.each do |s|
219
- parts << "- #{s[:name]} (#{s[:age]} days) — consider: activate, abandon, or defer to agent"
219
+ if stale.any?
220
+ parts << "\nStale future intents (untouched for days):\n"
221
+ stale.each do |s|
222
+ parts << "- #{s[:name]} (#{s[:age]} days) — consider: activate, abandon, or defer to agent"
223
+ end
224
+ parts << "\nWhen appropriate, ask the user what to do with stale intents."
220
225
  end
221
- parts << "\nWhen appropriate, ask the user what to do with stale intents."
222
226
  end
223
227
 
224
228
  if active_deprecations.any?
@@ -252,6 +256,10 @@ if update_notice
252
256
  parts.unshift("! #{update_notice}\n")
253
257
  end
254
258
 
259
+ # Emit nothing when there is genuinely nothing to surface (no conventions,
260
+ # no deprecations, no update notice).
261
+ exit 0 if parts.join.strip.empty?
262
+
255
263
  payload = {
256
264
  "hookSpecificOutput" => {
257
265
  "hookEventName" => "SessionStart",