@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.
- package/README.md +48 -6
- package/bin/plastic.js +57 -0
- package/bin/test +28 -0
- package/hooks/bash-gate +3 -0
- package/hooks/hooks.json +10 -0
- package/package.json +2 -2
- package/scripts/doctor.rb +741 -729
- package/scripts/hook-bash-gate +53 -0
- package/scripts/hook-session-start +42 -34
- package/scripts/install.rb +90 -695
- package/scripts/lib/bridge.rb +103 -0
- package/scripts/lib/installer_core.rb +760 -0
- package/scripts/uninstall.rb +53 -0
- package/scripts/update.rb +142 -0
- package/scripts/versions.rb +141 -0
- package/skills/brainstorming/SKILL.md +1 -1
- package/skills/executing-plan/SKILL.md +2 -2
- package/skills/install/SKILL.md +5 -5
- package/skills/uninstall/SKILL.md +2 -2
- package/skills/update/SKILL.md +35 -91
- package/skills/versions/SKILL.md +65 -0
- package/templates/outcome.md +13 -0
- package/templates/spec.md +25 -0
- package/bin/install.js +0 -29
|
@@ -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
|
-
# ---
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
parts <<
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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",
|