@zalom/plastic 1.0.0-alpha.15 → 1.0.0-alpha.17
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/PLASTIC.md +18 -18
- package/README.md +13 -6
- package/agents/future-intent-researcher.md +1 -1
- package/agents/intent-curator.md +1 -1
- package/deprecations.yml +6 -5
- package/package.json +1 -1
- package/scripts/dashboard.rb +480 -0
- package/scripts/doctor.rb +37 -30
- package/scripts/hook-auto-arm +3 -3
- package/scripts/hook-continue +1 -1
- package/scripts/hook-gate-check +3 -3
- package/scripts/hook-session-start +1 -1
- package/scripts/install.rb +175 -119
- package/scripts/lib/bridge.rb +1 -1
- package/scripts/migrate-to-global +1 -1
- package/skills/_active-intent-gate.md +1 -1
- package/skills/auto/SKILL.md +18 -6
- package/skills/auto/evals/evals.json +17 -17
- package/skills/auto/references/agent-architecture.md +1 -1
- package/skills/brainstorming/SKILL.md +8 -8
- package/skills/brainstorming-grill-me/SKILL.md +5 -5
- package/skills/continuing/SKILL.md +16 -4
- package/skills/creating-intent/SKILL.md +1 -1
- package/skills/creating-project/SKILL.md +4 -4
- package/skills/dashboard/SKILL.md +92 -0
- package/skills/doctor/SKILL.md +4 -4
- package/skills/evaluating-skills/SKILL.md +1 -1
- package/skills/executing-plan/SKILL.md +1 -1
- package/skills/install/SKILL.md +32 -8
- package/skills/intent-curator/SKILL.md +3 -3
- package/skills/linking-intents/SKILL.md +1 -1
- package/skills/managing-index/SKILL.md +1 -1
- package/skills/releasing/SKILL.md +3 -3
- package/skills/research/SKILL.md +2 -2
- package/skills/savepoint/SKILL.md +1 -1
- package/skills/uninstall/SKILL.md +39 -28
- package/skills/update/SKILL.md +2 -2
- package/skills/writing-instructions/SKILL.md +2 -2
- package/skills/writing-plans/SKILL.md +5 -5
- package/templates/agents.md +7 -7
package/scripts/doctor.rb
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
require "json"
|
|
13
13
|
require "yaml"
|
|
14
14
|
require "time"
|
|
15
|
+
require "date"
|
|
15
16
|
|
|
16
17
|
PLASTIC_HOME = File.join(Dir.home, ".plastic")
|
|
17
18
|
|
|
@@ -150,21 +151,31 @@ def parse_frontmatter(path)
|
|
|
150
151
|
parts = content.split("---", 3)
|
|
151
152
|
return nil if parts.length < 3
|
|
152
153
|
|
|
153
|
-
|
|
154
|
+
# created: dates parse as Date objects, which safe_load rejects by default —
|
|
155
|
+
# permit Date/Time so valid frontmatter isn't misreported as missing.
|
|
156
|
+
YAML.safe_load(parts[1], permitted_classes: [Date, Time]) || {}
|
|
154
157
|
rescue
|
|
155
158
|
nil
|
|
156
159
|
end
|
|
157
160
|
|
|
158
161
|
# --- Collect all intent directories (global + project stores) ---
|
|
159
162
|
|
|
163
|
+
# Child directories of a store, excluding dotfiles/dot-directories
|
|
164
|
+
# (e.g. .obsidian, .git) which are tooling artifacts, not intents.
|
|
165
|
+
def store_intent_dirs(store)
|
|
166
|
+
Dir.children(store).reject { |e| e.start_with?(".") }.select do |e|
|
|
167
|
+
File.directory?(File.join(store, e))
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
160
171
|
def all_intent_dirs
|
|
161
172
|
dirs = []
|
|
162
173
|
|
|
163
174
|
global_store = File.join(PLASTIC_HOME, "store")
|
|
164
175
|
if File.directory?(global_store)
|
|
165
|
-
|
|
176
|
+
store_intent_dirs(global_store).each do |entry|
|
|
166
177
|
full = File.join(global_store, entry)
|
|
167
|
-
dirs << { path: full, name: entry, scope: "global" }
|
|
178
|
+
dirs << { path: full, name: entry, scope: "global" }
|
|
168
179
|
end
|
|
169
180
|
end
|
|
170
181
|
|
|
@@ -174,9 +185,9 @@ def all_intent_dirs
|
|
|
174
185
|
project_store = File.join(projects_root, project, "store")
|
|
175
186
|
next unless File.directory?(project_store)
|
|
176
187
|
|
|
177
|
-
|
|
188
|
+
store_intent_dirs(project_store).each do |entry|
|
|
178
189
|
full = File.join(project_store, entry)
|
|
179
|
-
dirs << { path: full, name: entry, scope: "project:#{project}" }
|
|
190
|
+
dirs << { path: full, name: entry, scope: "project:#{project}" }
|
|
180
191
|
end
|
|
181
192
|
end
|
|
182
193
|
end
|
|
@@ -227,7 +238,7 @@ def check_global_store
|
|
|
227
238
|
# orphaned_intents — directories in store/ not referenced in INDEX.md
|
|
228
239
|
store_dir = File.join(PLASTIC_HOME, "store")
|
|
229
240
|
if File.directory?(store_dir)
|
|
230
|
-
intent_dirs =
|
|
241
|
+
intent_dirs = store_intent_dirs(store_dir)
|
|
231
242
|
orphans = intent_dirs.reject { |d| content.include?("store/#{d}") }
|
|
232
243
|
|
|
233
244
|
if orphans.empty?
|
|
@@ -459,22 +470,30 @@ def check_claude_registration(agent_dir)
|
|
|
459
470
|
end
|
|
460
471
|
end
|
|
461
472
|
|
|
462
|
-
# skills_exist
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
473
|
+
# skills_exist — flat, hyphen-namespaced personal skills (plastic-<name>/)
|
|
474
|
+
checks << flat_skills_check(agent_dir, "--claude")
|
|
475
|
+
|
|
476
|
+
checks
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# Plastic skills install as ~/.claude/skills/plastic-<name>/SKILL.md. Pass if at
|
|
480
|
+
# least one such skill is present.
|
|
481
|
+
def flat_skills_check(agent_dir, installer_flag)
|
|
482
|
+
skills_root = File.join(agent_dir, "skills")
|
|
483
|
+
found = Dir.glob(File.join(skills_root, "plastic-*", "SKILL.md"))
|
|
484
|
+
|
|
485
|
+
if !found.empty?
|
|
486
|
+
check(
|
|
466
487
|
category: "agent_registration", name: "skills_exist", status: "pass",
|
|
467
|
-
message: "
|
|
488
|
+
message: "#{found.size} plastic-* skill(s) installed in #{tilde(skills_root)}"
|
|
468
489
|
)
|
|
469
490
|
else
|
|
470
|
-
|
|
491
|
+
check(
|
|
471
492
|
category: "agent_registration", name: "skills_exist", status: "fail",
|
|
472
|
-
message: "
|
|
473
|
-
fixable: true, fix_hint: "Re-run the Plastic installer: npx @zalom/plastic@latest
|
|
493
|
+
message: "No plastic-* skills found in #{tilde(skills_root)}",
|
|
494
|
+
fixable: true, fix_hint: "Re-run the Plastic installer: npx @zalom/plastic@latest #{installer_flag}"
|
|
474
495
|
)
|
|
475
496
|
end
|
|
476
|
-
|
|
477
|
-
checks
|
|
478
497
|
end
|
|
479
498
|
|
|
480
499
|
def check_generic_agent_registration(agent_key, agent_dir)
|
|
@@ -482,19 +501,7 @@ def check_generic_agent_registration(agent_key, agent_dir)
|
|
|
482
501
|
config = AGENTS[agent_key]
|
|
483
502
|
|
|
484
503
|
# For codex/hermes: just check skills exist (no settings.json hooks)
|
|
485
|
-
|
|
486
|
-
if File.directory?(skills_dir) && !Dir.empty?(skills_dir)
|
|
487
|
-
checks << check(
|
|
488
|
-
category: "agent_registration", name: "skills_exist", status: "pass",
|
|
489
|
-
message: "Skills directory exists at #{tilde(skills_dir)}"
|
|
490
|
-
)
|
|
491
|
-
else
|
|
492
|
-
checks << check(
|
|
493
|
-
category: "agent_registration", name: "skills_exist", status: "fail",
|
|
494
|
-
message: "Skills directory missing or empty at #{tilde(skills_dir)}",
|
|
495
|
-
fixable: true, fix_hint: "Re-run the Plastic installer: npx @zalom/plastic@latest --#{agent_key}"
|
|
496
|
-
)
|
|
497
|
-
end
|
|
504
|
+
checks << flat_skills_check(agent_dir, "--#{agent_key}")
|
|
498
505
|
|
|
499
506
|
checks
|
|
500
507
|
end
|
|
@@ -695,7 +702,7 @@ def check_project_stores
|
|
|
695
702
|
checks << check(
|
|
696
703
|
category: "project_stores", name: "project_yml_exists", status: "warn",
|
|
697
704
|
message: "project.yml missing for project '#{slug}'",
|
|
698
|
-
fixable: true, fix_hint: "Create project.yml from template — see plastic
|
|
705
|
+
fixable: true, fix_hint: "Create project.yml from template — see plastic-creating-project"
|
|
699
706
|
)
|
|
700
707
|
end
|
|
701
708
|
|
package/scripts/hook-auto-arm
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# Usage: hook-auto-arm <message>
|
|
6
6
|
# UserPromptSubmit arming (intent 27). Two jobs, both additive context — never blocks:
|
|
7
|
-
# 1. Detect auto-trigger phrases -> steer the agent to invoke plastic
|
|
7
|
+
# 1. Detect auto-trigger phrases -> steer the agent to invoke plastic-auto.
|
|
8
8
|
# 2. If auto mode is armed and the active intent hasn't reached How -> remind the agent
|
|
9
9
|
# that project-code edits are gate-blocked until plan.md + checklist.md exist.
|
|
10
10
|
# Outputs UserPromptSubmit hookSpecificOutput JSON, or exits silently.
|
|
@@ -16,11 +16,11 @@ message = (ARGV[0] || "").downcase
|
|
|
16
16
|
parts = []
|
|
17
17
|
|
|
18
18
|
# --- 1. Auto-trigger steering ---
|
|
19
|
-
AUTO_TRIGGERS = ["take it from here", "deliver this", "/plastic
|
|
19
|
+
AUTO_TRIGGERS = ["take it from here", "deliver this", "/plastic-auto", "auto mode"].freeze
|
|
20
20
|
# bare "auto" only as a standalone word to avoid matching "automatic", "automation", etc.
|
|
21
21
|
triggered = AUTO_TRIGGERS.any? { |p| message.include?(p) } || message.match?(/\bauto\b/)
|
|
22
22
|
if triggered
|
|
23
|
-
parts << "Auto-mode request detected. Invoke the plastic
|
|
23
|
+
parts << "Auto-mode request detected. Invoke the plastic-auto skill to deliver the " \
|
|
24
24
|
"active intent autonomously — it arms the lifecycle gate so project code " \
|
|
25
25
|
"cannot be edited before plan.md + checklist.md exist."
|
|
26
26
|
end
|
package/scripts/hook-continue
CHANGED
|
@@ -116,7 +116,7 @@ if stale.any?
|
|
|
116
116
|
parts << ""
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
-
parts << "Follow the plastic
|
|
119
|
+
parts << "Follow the plastic-continuing skill workflow.\n"
|
|
120
120
|
parts << "1. If active intents exist: read their state and resume\n"
|
|
121
121
|
parts << "2. If no active intents: present future intents as options\n"
|
|
122
122
|
parts << "3. If stale intents exist: surface them and ask user to decide"
|
package/scripts/hook-gate-check
CHANGED
|
@@ -102,9 +102,9 @@ if is_stage_file || is_action_file
|
|
|
102
102
|
stage_labels = { "what" => "What", "why" => "Why", "how" => "How", "exec" => "Exec", "done" => "Done" }
|
|
103
103
|
next_hints = {
|
|
104
104
|
"why" => "write spec.md",
|
|
105
|
-
"how" => "Why complete. Invoke plastic
|
|
106
|
-
"exec" => "How complete. Invoke plastic
|
|
107
|
-
"done" => "Exec complete. Intent must be completed now — write outcome.md, update INDEX.md, auto-commit. Use plastic
|
|
105
|
+
"how" => "Why complete. Invoke plastic-auto to deliver autonomously, or write plan.md manually.",
|
|
106
|
+
"exec" => "How complete. Invoke plastic-auto or plastic-executing-plan to execute, or work through the checklist manually.",
|
|
107
|
+
"done" => "Exec complete. Intent must be completed now — write outcome.md, update INDEX.md, auto-commit. Use plastic-auto or do it manually."
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
context_parts = ["PLASTIC"]
|
|
@@ -168,7 +168,7 @@ cache_file = "#{store_root}/.cache/update-check.json"
|
|
|
168
168
|
if File.exist?(cache_file)
|
|
169
169
|
cache = JSON.parse(File.read(cache_file)) rescue {}
|
|
170
170
|
if cache["updateAvailable"]
|
|
171
|
-
update_notice = "Plastic update available: #{cache["current"]} -> #{cache["latest"]} — run /plastic
|
|
171
|
+
update_notice = "Plastic update available: #{cache["current"]} -> #{cache["latest"]} — run /plastic-update"
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
174
|
|
package/scripts/install.rb
CHANGED
|
@@ -168,6 +168,7 @@ def distribute(mode)
|
|
|
168
168
|
"scripts/hook-auto-arm" => "scripts/hook-auto-arm",
|
|
169
169
|
"scripts/lib/bridge.rb" => "scripts/lib/bridge.rb",
|
|
170
170
|
"scripts/doctor.rb" => "scripts/doctor.rb",
|
|
171
|
+
"scripts/dashboard.rb" => "scripts/dashboard.rb",
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
core_files.each do |src, dest|
|
|
@@ -258,6 +259,19 @@ end
|
|
|
258
259
|
|
|
259
260
|
# --- Agent adapters ---
|
|
260
261
|
|
|
262
|
+
def manifest_path_for(key, config)
|
|
263
|
+
case key
|
|
264
|
+
when "claude" then File.join(config[:dir], "plastic", "manifest.json")
|
|
265
|
+
else File.join(config[:dir], "plastic-manifest.json")
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def manifest_files(manifest_path)
|
|
270
|
+
return [] unless File.exist?(manifest_path)
|
|
271
|
+
data = JSON.parse(File.read(manifest_path)) rescue {}
|
|
272
|
+
(data["files"] || {}).keys
|
|
273
|
+
end
|
|
274
|
+
|
|
261
275
|
def install_for_agent(key, force)
|
|
262
276
|
config = agent_config(key)
|
|
263
277
|
return { agent: config[:name], success: false, reason: "Unknown agent" } unless config
|
|
@@ -266,20 +280,47 @@ def install_for_agent(key, force)
|
|
|
266
280
|
return { agent: config[:name], success: false, reason: "#{config[:dir]} not found \u{2014} #{config[:name]} not installed?" }
|
|
267
281
|
end
|
|
268
282
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
283
|
+
# Capture the prior manifest so we can prune files that no longer ship
|
|
284
|
+
# (renamed/removed skills) after a re-copy. This gives leftover-free updates.
|
|
285
|
+
old_files = manifest_files(manifest_path_for(key, config))
|
|
286
|
+
|
|
287
|
+
result = case key
|
|
288
|
+
when "claude" then install_claude(config, force)
|
|
289
|
+
when "codex" then install_codex(config, force)
|
|
290
|
+
when "hermes" then install_hermes(config, force)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
new_files = manifest_files(manifest_path_for(key, config))
|
|
294
|
+
pruned = prune_removed_files(old_files - new_files)
|
|
295
|
+
result[:pruned] = pruned if pruned.positive?
|
|
296
|
+
result
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Delete tracked files present in the old manifest but absent from the new one,
|
|
300
|
+
# then remove any now-empty skill directories they lived in.
|
|
301
|
+
def prune_removed_files(stale_files)
|
|
302
|
+
removed = 0
|
|
303
|
+
dirs = []
|
|
304
|
+
stale_files.each do |f|
|
|
305
|
+
if File.exist?(f)
|
|
306
|
+
File.delete(f)
|
|
307
|
+
removed += 1
|
|
308
|
+
end
|
|
309
|
+
dirs << File.dirname(f)
|
|
273
310
|
end
|
|
311
|
+
dirs.uniq.sort_by { |d| -d.length }.each do |d|
|
|
312
|
+
FileUtils.rmdir(d) if File.directory?(d) && Dir.empty?(d)
|
|
313
|
+
end
|
|
314
|
+
removed
|
|
274
315
|
end
|
|
275
316
|
|
|
276
317
|
def install_claude(config, force)
|
|
277
318
|
hooks_dir = File.join(config[:dir], "hooks")
|
|
278
|
-
|
|
319
|
+
skills_root = File.join(config[:dir], "skills")
|
|
279
320
|
plastic_dir = File.join(config[:dir], "plastic")
|
|
280
321
|
|
|
281
322
|
FileUtils.mkdir_p(hooks_dir)
|
|
282
|
-
FileUtils.mkdir_p(
|
|
323
|
+
FileUtils.mkdir_p(skills_root)
|
|
283
324
|
FileUtils.mkdir_p(plastic_dir)
|
|
284
325
|
|
|
285
326
|
installed = []
|
|
@@ -299,19 +340,19 @@ def install_claude(config, force)
|
|
|
299
340
|
installed << dest
|
|
300
341
|
end
|
|
301
342
|
|
|
302
|
-
# Copy skills
|
|
343
|
+
# Copy skills as flat, hyphen-namespaced personal skills (plastic-<name>/)
|
|
303
344
|
skills_source = File.join(PACKAGE_ROOT, "skills")
|
|
304
|
-
installed +=
|
|
345
|
+
installed += install_skills_flat(skills_source, skills_root) if File.directory?(skills_source)
|
|
305
346
|
|
|
306
347
|
# Write VERSION
|
|
307
348
|
version_file = File.join(plastic_dir, "VERSION")
|
|
308
349
|
File.write(version_file, "#{VERSION}\n")
|
|
309
350
|
installed << version_file
|
|
310
351
|
|
|
311
|
-
#
|
|
312
|
-
|
|
352
|
+
# Remove any prior plugin/marketplace install so the layouts don't coexist
|
|
353
|
+
migrate_legacy_plugin(config[:dir])
|
|
313
354
|
|
|
314
|
-
# Merge hooks +
|
|
355
|
+
# Merge hooks + statusline into settings.json (no plugin registration)
|
|
315
356
|
settings_path = File.join(config[:dir], "settings.json")
|
|
316
357
|
merge_claude_hooks(settings_path)
|
|
317
358
|
|
|
@@ -323,99 +364,106 @@ def install_claude(config, force)
|
|
|
323
364
|
end
|
|
324
365
|
|
|
325
366
|
def install_codex(config, force)
|
|
326
|
-
skills_dir = File.join(config[:dir], "skills", "plastic")
|
|
327
|
-
FileUtils.mkdir_p(skills_dir)
|
|
328
|
-
|
|
329
367
|
installed = []
|
|
330
368
|
skills_source = File.join(PACKAGE_ROOT, "skills")
|
|
331
|
-
installed +=
|
|
332
|
-
|
|
333
|
-
manifest_path = File.join(config[:dir], "plastic-manifest.json")
|
|
334
|
-
write_manifest(installed, manifest_path)
|
|
369
|
+
installed += install_skills_flat(skills_source, File.join(config[:dir], "skills")) if File.directory?(skills_source)
|
|
335
370
|
|
|
371
|
+
write_manifest(installed, File.join(config[:dir], "plastic-manifest.json"))
|
|
336
372
|
{ agent: config[:name], success: true, files: installed.size }
|
|
337
373
|
end
|
|
338
374
|
|
|
339
375
|
def install_hermes(config, force)
|
|
340
|
-
skills_dir = File.join(config[:dir], "skills", "plastic")
|
|
341
|
-
FileUtils.mkdir_p(skills_dir)
|
|
342
|
-
|
|
343
376
|
installed = []
|
|
344
377
|
skills_source = File.join(PACKAGE_ROOT, "skills")
|
|
345
|
-
installed +=
|
|
346
|
-
|
|
347
|
-
manifest_path = File.join(config[:dir], "plastic-manifest.json")
|
|
348
|
-
write_manifest(installed, manifest_path)
|
|
378
|
+
installed += install_skills_flat(skills_source, File.join(config[:dir], "skills")) if File.directory?(skills_source)
|
|
349
379
|
|
|
380
|
+
write_manifest(installed, File.join(config[:dir], "plastic-manifest.json"))
|
|
350
381
|
{ agent: config[:name], success: true, files: installed.size }
|
|
351
382
|
end
|
|
352
383
|
|
|
353
|
-
#
|
|
384
|
+
# Copy each skills/<name>/ to <skills_root>/plastic-<name>/ (flat, namespaced by
|
|
385
|
+
# directory name — the only personal-skill namespacing Claude Code supports).
|
|
386
|
+
# The non-skill `_active-intent-gate.md` is relocated to ~/.plastic/ instead.
|
|
387
|
+
def install_skills_flat(skills_source, skills_root)
|
|
388
|
+
installed = []
|
|
389
|
+
FileUtils.mkdir_p(skills_root)
|
|
390
|
+
|
|
391
|
+
Dir.children(skills_source).reject { |e| e.start_with?(".") }.each do |entry|
|
|
392
|
+
src = File.join(skills_source, entry)
|
|
393
|
+
if File.directory?(src)
|
|
394
|
+
installed += copy_dir_recursive(src, File.join(skills_root, "plastic-#{entry}"))
|
|
395
|
+
elsif entry == "_active-intent-gate.md"
|
|
396
|
+
FileUtils.mkdir_p(PLASTIC_HOME)
|
|
397
|
+
dest = File.join(PLASTIC_HOME, entry)
|
|
398
|
+
FileUtils.cp(src, dest)
|
|
399
|
+
installed << dest
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
installed
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# --- Legacy plugin migration ---
|
|
407
|
+
|
|
408
|
+
# Earlier versions registered Plastic as a local marketplace plugin
|
|
409
|
+
# (plastic@plastic) for skill discovery. The current model uses flat,
|
|
410
|
+
# hyphen-namespaced personal skills, so any prior plugin layout is removed here
|
|
411
|
+
# to stop the two coexisting. Returns a list of what was migrated.
|
|
412
|
+
def migrate_legacy_plugin(claude_dir)
|
|
413
|
+
removed = []
|
|
414
|
+
|
|
415
|
+
legacy_dirs = [
|
|
416
|
+
File.join(claude_dir, "plugins", "marketplaces", "plastic"),
|
|
417
|
+
File.join(claude_dir, "plugins", "cache", "plastic"),
|
|
418
|
+
File.join(claude_dir, "skills", "plastic"), # old nested skills layout
|
|
419
|
+
]
|
|
420
|
+
legacy_dirs.each do |d|
|
|
421
|
+
if File.directory?(d)
|
|
422
|
+
FileUtils.rm_rf(d)
|
|
423
|
+
removed << d
|
|
424
|
+
end
|
|
425
|
+
end
|
|
354
426
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
427
|
+
# settings.json: drop enabledPlugins + extraKnownMarketplaces entries
|
|
428
|
+
settings_path = File.join(claude_dir, "settings.json")
|
|
429
|
+
settings = read_json_safe(settings_path)
|
|
430
|
+
if settings
|
|
431
|
+
changed = false
|
|
432
|
+
if settings.dig("enabledPlugins", "plastic@plastic")
|
|
433
|
+
settings["enabledPlugins"].delete("plastic@plastic")
|
|
434
|
+
settings.delete("enabledPlugins") if settings["enabledPlugins"].empty?
|
|
435
|
+
changed = true
|
|
436
|
+
end
|
|
437
|
+
if settings.dig("extraKnownMarketplaces", "plastic")
|
|
438
|
+
settings["extraKnownMarketplaces"].delete("plastic")
|
|
439
|
+
settings.delete("extraKnownMarketplaces") if settings["extraKnownMarketplaces"].empty?
|
|
440
|
+
changed = true
|
|
441
|
+
end
|
|
442
|
+
if changed
|
|
443
|
+
write_json_atomic(settings_path, settings)
|
|
444
|
+
removed << "settings.json: plastic@plastic plugin registration"
|
|
445
|
+
end
|
|
446
|
+
end
|
|
358
447
|
|
|
359
|
-
|
|
448
|
+
# plugins/known_marketplaces.json: drop the plastic entry
|
|
449
|
+
known_path = File.join(claude_dir, "plugins", "known_marketplaces.json")
|
|
450
|
+
known = read_json_safe(known_path)
|
|
451
|
+
if known.is_a?(Hash) && known.key?("plastic")
|
|
452
|
+
known.delete("plastic")
|
|
453
|
+
write_json_atomic(known_path, known)
|
|
454
|
+
removed << "known_marketplaces.json: plastic"
|
|
455
|
+
end
|
|
360
456
|
|
|
361
|
-
|
|
362
|
-
"
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
"author" => { "name" => "Zlatko Alomerovic", "email" => "zlatko.alomerovic@gmail.com" },
|
|
366
|
-
"homepage" => "https://github.com/zalom/plastic",
|
|
367
|
-
"repository" => "https://github.com/zalom/plastic",
|
|
368
|
-
"license" => "MIT",
|
|
369
|
-
"keywords" => %w[intent-driven state-management zettelkasten ai-agent]
|
|
370
|
-
}
|
|
371
|
-
write_json_atomic(File.join(plugin_meta_dir, "plugin.json"), plugin_json)
|
|
372
|
-
|
|
373
|
-
marketplace_json = {
|
|
374
|
-
"name" => "plastic",
|
|
375
|
-
"description" => "Marketplace for Plastic — intent-driven state management",
|
|
376
|
-
"owner" => { "name" => "Zlatko Alomerovic", "email" => "zlatko.alomerovic@gmail.com" },
|
|
377
|
-
"plugins" => [{
|
|
378
|
-
"name" => "plastic",
|
|
379
|
-
"description" => "Intent-driven state management for AI coding sessions",
|
|
380
|
-
"version" => VERSION,
|
|
381
|
-
"source" => "./",
|
|
382
|
-
"author" => { "name" => "Zlatko Alomerovic", "email" => "zlatko.alomerovic@gmail.com" }
|
|
383
|
-
}]
|
|
384
|
-
}
|
|
385
|
-
write_json_atomic(File.join(plugin_meta_dir, "marketplace.json"), marketplace_json)
|
|
457
|
+
unless removed.empty?
|
|
458
|
+
puts " \u{1f9f9} Migrated legacy plugin install:"
|
|
459
|
+
removed.each { |r| puts " - #{tilde(r)}" }
|
|
460
|
+
end
|
|
386
461
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
copy_dir_recursive(skills_source, skills_dest)
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
# Sync agents
|
|
396
|
-
agents_source = File.join(PACKAGE_ROOT, "agents")
|
|
397
|
-
agents_dest = File.join(marketplace_dir, "agents")
|
|
398
|
-
if File.directory?(agents_source)
|
|
399
|
-
FileUtils.rm_rf(agents_dest)
|
|
400
|
-
copy_dir_recursive(agents_source, agents_dest)
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
# Write .claude/settings.json (permissions)
|
|
404
|
-
claude_settings_dir = File.join(marketplace_dir, ".claude")
|
|
405
|
-
FileUtils.mkdir_p(claude_settings_dir)
|
|
406
|
-
write_json_atomic(File.join(claude_settings_dir, "settings.json"), {
|
|
407
|
-
"permissions" => {
|
|
408
|
-
"allow" => [
|
|
409
|
-
"Bash(ruby *)",
|
|
410
|
-
"Bash(ls *)",
|
|
411
|
-
"Bash(find *)",
|
|
412
|
-
"Bash(git *)",
|
|
413
|
-
"Bash(mkdir *)",
|
|
414
|
-
"Bash(chmod *)",
|
|
415
|
-
"mcp__serena__*"
|
|
416
|
-
]
|
|
417
|
-
}
|
|
418
|
-
})
|
|
462
|
+
removed
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def tilde(path)
|
|
466
|
+
path.sub(Dir.home, "~")
|
|
419
467
|
end
|
|
420
468
|
|
|
421
469
|
# --- settings.json merge (read-modify-write, never clobber) ---
|
|
@@ -486,13 +534,8 @@ def merge_claude_hooks(settings_path)
|
|
|
486
534
|
|
|
487
535
|
settings["statusLine"] = { "type" => "command", "command" => "#{hook_dir}/plastic-statusline" }
|
|
488
536
|
|
|
489
|
-
#
|
|
490
|
-
|
|
491
|
-
plugins["plastic@plastic"] = true
|
|
492
|
-
|
|
493
|
-
# Register the marketplace source
|
|
494
|
-
marketplaces = settings["extraKnownMarketplaces"] ||= {}
|
|
495
|
-
marketplaces["plastic"] ||= { "source" => { "source" => "github", "repo" => "zalom/plastic" } }
|
|
537
|
+
# No plugin/marketplace registration: skills are flat personal skills
|
|
538
|
+
# (plastic-<name>/) discovered directly from ~/.claude/skills.
|
|
496
539
|
|
|
497
540
|
write_json_atomic(settings_path, settings)
|
|
498
541
|
end
|
|
@@ -526,14 +569,25 @@ def handle_uninstall(agents)
|
|
|
526
569
|
next unless config
|
|
527
570
|
|
|
528
571
|
result = uninstall_agent(key, config)
|
|
529
|
-
|
|
530
|
-
puts " \u{2705} #{config[:name]}: uninstalled (#{result[:files]} files removed)"
|
|
531
|
-
else
|
|
572
|
+
unless result[:success]
|
|
532
573
|
puts " \u{26a0}\u{fe0f} #{config[:name]}: #{result[:reason]}"
|
|
574
|
+
next
|
|
533
575
|
end
|
|
576
|
+
|
|
577
|
+
puts " \u{2705} #{config[:name]}: uninstalled (#{result[:files]} files removed)"
|
|
578
|
+
result[:removed].each { |r| puts " removed: #{tilde(r)}" }
|
|
534
579
|
end
|
|
535
580
|
|
|
536
|
-
|
|
581
|
+
# What was deliberately left behind
|
|
582
|
+
puts "\n Left in place (not removed):"
|
|
583
|
+
puts " - #{tilde(PLASTIC_HOME)} (your intent store, history, and projects)"
|
|
584
|
+
puts " - any non-Plastic entries in settings.json"
|
|
585
|
+
|
|
586
|
+
puts "\n Verify removal:"
|
|
587
|
+
puts " ls ~/.claude/skills | grep '^plastic-' # → no output"
|
|
588
|
+
puts " ls ~/.claude/hooks | grep '^plastic-' # → no output"
|
|
589
|
+
puts " grep -c plastic ~/.claude/settings.json # → only hook refs gone"
|
|
590
|
+
puts "\n To also delete your intent store: rm -rf #{tilde(PLASTIC_HOME)}\n\n"
|
|
537
591
|
end
|
|
538
592
|
|
|
539
593
|
def uninstall_agent(key, config)
|
|
@@ -541,45 +595,47 @@ def uninstall_agent(key, config)
|
|
|
541
595
|
return { success: false, reason: "#{config[:dir]} not found" }
|
|
542
596
|
end
|
|
543
597
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
else File.join(config[:dir], "plastic-manifest.json")
|
|
547
|
-
end
|
|
548
|
-
|
|
549
|
-
files_removed = 0
|
|
598
|
+
removed = []
|
|
599
|
+
manifest_path = manifest_path_for(key, config)
|
|
550
600
|
|
|
551
601
|
if File.exist?(manifest_path)
|
|
552
602
|
manifest = JSON.parse(File.read(manifest_path)) rescue {}
|
|
553
603
|
(manifest["files"] || {}).each_key do |f|
|
|
554
604
|
if File.exist?(f)
|
|
555
605
|
File.delete(f)
|
|
556
|
-
|
|
606
|
+
removed << f
|
|
557
607
|
end
|
|
558
608
|
end
|
|
559
609
|
File.delete(manifest_path)
|
|
560
|
-
|
|
610
|
+
removed << manifest_path
|
|
561
611
|
end
|
|
562
612
|
|
|
563
|
-
#
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
613
|
+
# Remove flat plastic-<name>/ skill dirs (now-empty after manifest deletion,
|
|
614
|
+
# plus any the manifest missed) and the plastic state dir.
|
|
615
|
+
skills_root = File.join(config[:dir], "skills")
|
|
616
|
+
if File.directory?(skills_root)
|
|
617
|
+
Dir.children(skills_root).select { |e| e.start_with?("plastic-") }.each do |d|
|
|
618
|
+
full = File.join(skills_root, d)
|
|
619
|
+
FileUtils.rm_rf(full)
|
|
620
|
+
removed << full
|
|
621
|
+
end
|
|
622
|
+
end
|
|
573
623
|
|
|
574
|
-
|
|
624
|
+
[File.join(config[:dir], "plastic")].each do |d|
|
|
625
|
+
if File.directory?(d)
|
|
626
|
+
FileUtils.rm_rf(d)
|
|
627
|
+
removed << d
|
|
628
|
+
end
|
|
629
|
+
end
|
|
575
630
|
|
|
576
|
-
#
|
|
631
|
+
# Claude Code: clean hooks/statusline and any legacy plugin registration
|
|
577
632
|
if key == "claude"
|
|
578
633
|
settings_path = File.join(config[:dir], "settings.json")
|
|
579
634
|
remove_claude_hooks(settings_path) if File.exist?(settings_path)
|
|
635
|
+
removed.concat(migrate_legacy_plugin(config[:dir]))
|
|
580
636
|
end
|
|
581
637
|
|
|
582
|
-
{ success: true, files:
|
|
638
|
+
{ success: true, files: removed.size, removed: removed }
|
|
583
639
|
end
|
|
584
640
|
|
|
585
641
|
def remove_claude_hooks(settings_path)
|
package/scripts/lib/bridge.rb
CHANGED
|
@@ -211,7 +211,7 @@ module Bridge
|
|
|
211
211
|
|
|
212
212
|
id = intent_info["id"]
|
|
213
213
|
"intent #{id} has not reached How — write plan.md + checklist.md before " \
|
|
214
|
-
"editing project code. Run plastic
|
|
214
|
+
"editing project code. Run plastic-auto or plastic-writing-plans first. " \
|
|
215
215
|
"(blocked edit: #{file_abs})"
|
|
216
216
|
end
|
|
217
217
|
|
|
@@ -17,7 +17,7 @@ GLOBAL_ROOT="$HOME/.plastic"
|
|
|
17
17
|
LOCAL_PLASTIC="$PROJECT_PATH/.plastic"
|
|
18
18
|
|
|
19
19
|
if [ ! -d "$GLOBAL_ROOT" ]; then
|
|
20
|
-
echo "Error: Global Plastic not installed. Run /plastic
|
|
20
|
+
echo "Error: Global Plastic not installed. Run /plastic-install first." >&2
|
|
21
21
|
exit 1
|
|
22
22
|
fi
|
|
23
23
|
|
|
@@ -13,7 +13,7 @@ Before proceeding, resolve the active intent:
|
|
|
13
13
|
- Look under `## Active` for intent entries
|
|
14
14
|
- If exactly one active intent → use it
|
|
15
15
|
- If multiple active intents → ask user which one
|
|
16
|
-
- If no active intent → refuse: "No active intent. Create one first with /plastic
|
|
16
|
+
- If no active intent → refuse: "No active intent. Create one first with /plastic-creating-intent"
|
|
17
17
|
|
|
18
18
|
3. **Resolve paths:**
|
|
19
19
|
- Intent directory: `{store}/store/{id}--{slug}/`
|