@zalom/plastic 1.0.0-alpha.1 → 1.0.0-alpha.10

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.
@@ -117,15 +117,23 @@ if File.exist?(projects_path)
117
117
  end
118
118
  end
119
119
 
120
+ # --- Load PLASTIC.md conventions ---
121
+
122
+ plastic_md_path = "#{store_root}/PLASTIC.md"
123
+ plastic_md = File.exist?(plastic_md_path) ? File.read(plastic_md_path).strip : nil
124
+
120
125
  # --- Load deprecations ---
121
126
 
127
+ dep_file = if plugin_root && !plugin_root.empty?
128
+ "#{plugin_root}/deprecations.yml"
129
+ else
130
+ "#{store_root}/deprecations.yml"
131
+ end
132
+
122
133
  deprecations = []
123
- if plugin_root && !plugin_root.empty?
124
- dep_file = "#{plugin_root}/deprecations.yml"
125
- if File.exist?(dep_file)
126
- dep_data = YAML.safe_load(File.read(dep_file)) rescue {}
127
- deprecations = dep_data["deprecations"] || []
128
- end
134
+ if File.exist?(dep_file)
135
+ dep_data = YAML.safe_load(File.read(dep_file)) rescue {}
136
+ deprecations = dep_data["deprecations"] || []
129
137
  end
130
138
 
131
139
  dismissed_json = `"#{read_config}" deprecations_dismissed`.strip
@@ -136,7 +144,10 @@ rescue
136
144
  end
137
145
 
138
146
  current_version = nil
139
- if plugin_root && !plugin_root.empty?
147
+ version_file = "#{store_root}/VERSION"
148
+ if File.exist?(version_file)
149
+ current_version = File.read(version_file).strip
150
+ elsif plugin_root && !plugin_root.empty?
140
151
  plugin_json_path = "#{plugin_root}/.claude-plugin/plugin.json"
141
152
  if File.exist?(plugin_json_path)
142
153
  pj = JSON.parse(File.read(plugin_json_path)) rescue {}
@@ -150,18 +161,40 @@ active_deprecations = deprecations.select do |dep|
150
161
  !dismissed.include?(dep["id"])
151
162
  end
152
163
 
153
- # --- Early exit if nothing to show ---
164
+ # --- Check for available updates (from previous session's check) ---
165
+
166
+ update_notice = nil
167
+ cache_file = "#{store_root}/.cache/update-check.json"
168
+ if File.exist?(cache_file)
169
+ cache = JSON.parse(File.read(cache_file)) rescue {}
170
+ if cache["updateAvailable"]
171
+ update_notice = "Plastic update available: #{cache["current"]} -> #{cache["latest"]} — run /plastic:update"
172
+ end
173
+ end
174
+
175
+ # --- Early exit only if no PLASTIC.md ---
154
176
 
155
177
  all_active = active + project_active
156
178
  all_future = future + project_future
157
- exit 0 if all_active.empty? && stale.empty? && all_future.empty? && active_deprecations.empty?
179
+ exit 0 unless plastic_md
158
180
 
159
181
  # --- Build context ---
160
182
 
161
183
  parts = []
162
184
 
185
+ # Conventions always loaded first
186
+ parts << plastic_md
187
+ parts << "\n---\n"
188
+
163
189
  if current_project
164
- parts << "PLASTIC — Global store | Project: #{current_project["slug"]} (#{current_project["path"]})\n"
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}/"
196
+ end
197
+ parts << banner + "\n"
165
198
  else
166
199
  parts << "PLASTIC — Global store loaded from ~/.plastic/\n"
167
200
  end
@@ -215,6 +248,10 @@ if active_deprecations.any?
215
248
  end
216
249
  end
217
250
 
251
+ if update_notice
252
+ parts.unshift("! #{update_notice}\n")
253
+ end
254
+
218
255
  payload = {
219
256
  "hookSpecificOutput" => {
220
257
  "hookEventName" => "SessionStart",
@@ -141,13 +141,19 @@ def distribute(mode)
141
141
  puts " \u{1f4e6} #{mode == :update ? "Updating" : "Installing"} core files to #{PLASTIC_HOME}"
142
142
 
143
143
  FileUtils.mkdir_p(PLASTIC_HOME)
144
- FileUtils.mkdir_p(File.join(PLASTIC_HOME, "scripts"))
144
+ FileUtils.mkdir_p(File.join(PLASTIC_HOME, "scripts", "lib"))
145
145
 
146
146
  core_files = {
147
147
  "PLASTIC.md" => "PLASTIC.md",
148
148
  "deprecations.yml" => "deprecations.yml",
149
149
  "scripts/folgezettel-id" => "scripts/folgezettel-id",
150
150
  "scripts/read-config" => "scripts/read-config",
151
+ "scripts/hook-session-start" => "scripts/hook-session-start",
152
+ "scripts/hook-continue" => "scripts/hook-continue",
153
+ "scripts/hook-future-intent-check" => "scripts/hook-future-intent-check",
154
+ "scripts/hook-gate-check" => "scripts/hook-gate-check",
155
+ "scripts/lib/bridge.rb" => "scripts/lib/bridge.rb",
156
+ "scripts/doctor.rb" => "scripts/doctor.rb",
151
157
  }
152
158
 
153
159
  core_files.each do |src, dest|
@@ -211,6 +217,31 @@ def bootstrap
211
217
  puts " \u{2705} Store bootstrapped"
212
218
  end
213
219
 
220
+ def bootstrap_project_store(slug)
221
+ project_dir = File.join(PLASTIC_HOME, "projects", slug)
222
+ store_dir = File.join(project_dir, "store")
223
+
224
+ FileUtils.mkdir_p(store_dir)
225
+
226
+ template = File.join(PACKAGE_ROOT, "templates", "project.yml")
227
+ dest = File.join(project_dir, "project.yml")
228
+ write_if_missing(dest, File.read(template)) if File.exist?(template)
229
+
230
+ write_if_missing(File.join(project_dir, "INDEX.md"), <<~MD)
231
+ # Index
232
+
233
+ ## Active
234
+
235
+ ## Future
236
+
237
+ ## Clusters
238
+
239
+ ## Abandoned
240
+
241
+ ## Completed
242
+ MD
243
+ end
244
+
214
245
  # --- Agent adapters ---
215
246
 
216
247
  def install_for_agent(key, force)
@@ -239,7 +270,7 @@ def install_claude(config, force)
239
270
 
240
271
  installed = []
241
272
 
242
- # Copy hooks
273
+ # Copy hooks, rewriting script paths for the installed location
243
274
  hook_source = File.join(PACKAGE_ROOT, "hooks")
244
275
  Dir.glob(File.join(hook_source, "*")).each do |f|
245
276
  next unless File.file?(f)
@@ -247,7 +278,9 @@ def install_claude(config, force)
247
278
  next if %w[hooks.json run-hook].include?(basename)
248
279
  dest_name = basename.start_with?("plastic-") ? basename : "plastic-#{basename}"
249
280
  dest = File.join(hooks_dir, dest_name)
250
- FileUtils.cp(f, dest)
281
+ content = File.read(f)
282
+ content = content.gsub('$SCRIPT_DIR/../scripts/', '$HOME/.plastic/scripts/')
283
+ File.write(dest, content)
251
284
  FileUtils.chmod(0o755, dest)
252
285
  installed << dest
253
286
  end
@@ -261,7 +294,10 @@ def install_claude(config, force)
261
294
  File.write(version_file, "#{VERSION}\n")
262
295
  installed << version_file
263
296
 
264
- # Merge hooks into settings.json
297
+ # Sync marketplace plugin so Claude Code discovers skills
298
+ sync_marketplace_plugin(config[:dir])
299
+
300
+ # Merge hooks + enabledPlugins into settings.json
265
301
  settings_path = File.join(config[:dir], "settings.json")
266
302
  merge_claude_hooks(settings_path)
267
303
 
@@ -300,46 +336,167 @@ def install_hermes(config, force)
300
336
  { agent: config[:name], success: true, files: installed.size }
301
337
  end
302
338
 
339
+ # --- Marketplace plugin sync ---
340
+
341
+ def sync_marketplace_plugin(claude_dir)
342
+ marketplace_dir = File.join(claude_dir, "plugins", "marketplaces", "plastic")
343
+ plugin_meta_dir = File.join(marketplace_dir, ".claude-plugin")
344
+
345
+ FileUtils.mkdir_p(plugin_meta_dir)
346
+
347
+ plugin_json = {
348
+ "name" => "plastic",
349
+ "description" => "Intent-driven state management for AI coding sessions. Neuroplasticity for your codebase.",
350
+ "version" => VERSION,
351
+ "author" => { "name" => "Zlatko Alomerovic", "email" => "zlatko.alomerovic@gmail.com" },
352
+ "homepage" => "https://github.com/zalom/plastic",
353
+ "repository" => "https://github.com/zalom/plastic",
354
+ "license" => "MIT",
355
+ "keywords" => %w[intent-driven state-management zettelkasten ai-agent]
356
+ }
357
+ write_json_atomic(File.join(plugin_meta_dir, "plugin.json"), plugin_json)
358
+
359
+ marketplace_json = {
360
+ "name" => "plastic",
361
+ "description" => "Marketplace for Plastic — intent-driven state management",
362
+ "owner" => { "name" => "Zlatko Alomerovic", "email" => "zlatko.alomerovic@gmail.com" },
363
+ "plugins" => [{
364
+ "name" => "plastic",
365
+ "description" => "Intent-driven state management for AI coding sessions",
366
+ "version" => VERSION,
367
+ "source" => "./",
368
+ "author" => { "name" => "Zlatko Alomerovic", "email" => "zlatko.alomerovic@gmail.com" }
369
+ }]
370
+ }
371
+ write_json_atomic(File.join(plugin_meta_dir, "marketplace.json"), marketplace_json)
372
+
373
+ # Sync skills
374
+ skills_source = File.join(PACKAGE_ROOT, "skills")
375
+ skills_dest = File.join(marketplace_dir, "skills")
376
+ if File.directory?(skills_source)
377
+ FileUtils.rm_rf(skills_dest)
378
+ copy_dir_recursive(skills_source, skills_dest)
379
+ end
380
+
381
+ # Sync agents
382
+ agents_source = File.join(PACKAGE_ROOT, "agents")
383
+ agents_dest = File.join(marketplace_dir, "agents")
384
+ if File.directory?(agents_source)
385
+ FileUtils.rm_rf(agents_dest)
386
+ copy_dir_recursive(agents_source, agents_dest)
387
+ end
388
+
389
+ # Write .claude/settings.json (permissions)
390
+ claude_settings_dir = File.join(marketplace_dir, ".claude")
391
+ FileUtils.mkdir_p(claude_settings_dir)
392
+ write_json_atomic(File.join(claude_settings_dir, "settings.json"), {
393
+ "permissions" => {
394
+ "allow" => [
395
+ "Bash(ruby *)",
396
+ "Bash(ls *)",
397
+ "Bash(find *)",
398
+ "Bash(git *)",
399
+ "Bash(mkdir *)",
400
+ "Bash(chmod *)",
401
+ "mcp__serena__*"
402
+ ]
403
+ }
404
+ })
405
+ end
406
+
303
407
  # --- settings.json merge (read-modify-write, never clobber) ---
304
408
 
305
409
  def merge_claude_hooks(settings_path)
306
410
  settings = read_json_safe(settings_path) || {}
307
- return if settings.nil? # unparseable — refuse to modify
411
+ return if settings.nil?
308
412
 
309
413
  hooks = settings["hooks"] ||= {}
310
414
  hook_dir = File.join(Dir.home, ".claude", "hooks")
311
415
 
416
+ purge_stale_plastic_hooks(hooks)
417
+
312
418
  plastic_hooks = {
313
- "SessionStart" => [
314
- { "type" => "command", "command" => "ruby #{hook_dir}/plastic-session-start", "statusMessage" => "Loading Plastic context..." },
315
- { "type" => "command", "command" => "#{hook_dir}/plastic-check-update", "statusMessage" => "" },
316
- ],
317
- "PreCompact" => [
318
- { "type" => "command", "command" => "ruby #{hook_dir}/plastic-savepoint", "statusMessage" => "Saving Plastic intent state..." },
319
- ],
320
- "PostToolUse" => [
321
- { "matcher" => "Write|Edit", "type" => "command", "command" => "#{hook_dir}/plastic-gate-check", "statusMessage" => "Checking lifecycle gates..." },
322
- ],
323
- "UserPromptSubmit" => [
324
- { "type" => "command", "command" => "#{hook_dir}/plastic-continue", "statusMessage" => "Checking for continue..." },
325
- { "type" => "command", "command" => "#{hook_dir}/plastic-future-intent-check", "statusMessage" => "Checking future intents..." },
326
- ],
327
- "statusLine" => [
328
- { "type" => "command", "command" => "#{hook_dir}/plastic-statusline" },
329
- ],
419
+ "SessionStart" => {
420
+ "matcher" => "",
421
+ "hooks" => [
422
+ { "type" => "command", "command" => "#{hook_dir}/plastic-session-start", "statusMessage" => "Loading Plastic context..." },
423
+ { "type" => "command", "command" => "#{hook_dir}/plastic-check-update", "statusMessage" => "" },
424
+ ],
425
+ },
426
+ "PreCompact" => {
427
+ "matcher" => "",
428
+ "hooks" => [
429
+ { "type" => "command", "command" => "#{hook_dir}/plastic-savepoint", "statusMessage" => "Saving Plastic intent state..." },
430
+ ],
431
+ },
432
+ "PostToolUse" => {
433
+ "matcher" => "Write|Edit",
434
+ "hooks" => [
435
+ { "type" => "command", "command" => "#{hook_dir}/plastic-gate-check", "statusMessage" => "Checking lifecycle gates..." },
436
+ ],
437
+ },
438
+ "UserPromptSubmit" => {
439
+ "matcher" => "",
440
+ "hooks" => [
441
+ { "type" => "command", "command" => "#{hook_dir}/plastic-continue", "statusMessage" => "Checking for continue..." },
442
+ { "type" => "command", "command" => "#{hook_dir}/plastic-future-intent-check", "statusMessage" => "Checking future intents..." },
443
+ ],
444
+ },
330
445
  }
331
446
 
332
- plastic_hooks.each do |event, entries|
447
+ plastic_hooks.each do |event, group|
333
448
  hooks[event] ||= []
334
- entries.each do |entry|
335
- already = hooks[event].any? { |h| h["command"] == entry["command"] }
336
- hooks[event] << entry unless already
449
+ existing = hooks[event].find { |g| g.is_a?(Hash) && g["hooks"].is_a?(Array) && g["hooks"].any? { |h| h["command"].to_s.include?("plastic-") } }
450
+
451
+ if existing
452
+ existing["matcher"] = group["matcher"]
453
+ existing["hooks"] = group["hooks"]
454
+ else
455
+ hooks[event] << group
337
456
  end
338
457
  end
339
458
 
459
+ existing_status = settings["statusLine"]
460
+ if existing_status && !existing_status.dig("command").to_s.include?("plastic-")
461
+ cache_dir = File.join(PLASTIC_HOME, ".cache")
462
+ FileUtils.mkdir_p(cache_dir)
463
+ File.write(File.join(cache_dir, "original-statusline.json"), JSON.pretty_generate(existing_status))
464
+ end
465
+
466
+ settings["statusLine"] = { "type" => "command", "command" => "#{hook_dir}/plastic-statusline" }
467
+
468
+ # Register Plastic as an enabled plugin for skill discovery
469
+ plugins = settings["enabledPlugins"] ||= {}
470
+ plugins["plastic@plastic"] = true
471
+
472
+ # Register the marketplace source
473
+ marketplaces = settings["extraKnownMarketplaces"] ||= {}
474
+ marketplaces["plastic"] ||= { "source" => { "source" => "github", "repo" => "zalom/plastic" } }
475
+
340
476
  write_json_atomic(settings_path, settings)
341
477
  end
342
478
 
479
+ def purge_stale_plastic_hooks(hooks)
480
+ plastic_cmd = ->(cmd) { cmd.to_s.include?("plastic-") }
481
+
482
+ hooks.delete("statusLine")
483
+
484
+ hooks.each do |event, groups|
485
+ next unless groups.is_a?(Array)
486
+
487
+ hooks[event] = groups.map do |group|
488
+ if group.is_a?(Hash) && group["hooks"].is_a?(Array)
489
+ group["hooks"].reject! { |h| plastic_cmd.call(h["command"]) }
490
+ group unless group["hooks"].empty?
491
+ elsif group.is_a?(Hash) && group["command"]
492
+ plastic_cmd.call(group["command"]) ? nil : group
493
+ else
494
+ group
495
+ end
496
+ end.compact
497
+ end
498
+ end
499
+
343
500
  # --- Uninstall ---
344
501
 
345
502
  def handle_uninstall(agents)
@@ -384,7 +541,12 @@ def uninstall_agent(key, config)
384
541
 
385
542
  # Clean known directories
386
543
  dirs_to_clean = case key
387
- when "claude" then [File.join(config[:dir], "plastic"), File.join(config[:dir], "skills", "plastic")]
544
+ when "claude"
545
+ [
546
+ File.join(config[:dir], "plastic"),
547
+ File.join(config[:dir], "skills", "plastic"),
548
+ File.join(config[:dir], "plugins", "marketplaces", "plastic"),
549
+ ]
388
550
  else [File.join(config[:dir], "skills", "plastic")]
389
551
  end
390
552
 
@@ -403,11 +565,37 @@ def remove_claude_hooks(settings_path)
403
565
  settings = read_json_safe(settings_path)
404
566
  return unless settings && settings["hooks"]
405
567
 
406
- settings["hooks"].each do |event, entries|
407
- settings["hooks"][event] = entries.reject { |h| (h["command"] || "").include?("plastic-") }
568
+ settings["hooks"].each do |event, groups|
569
+ next unless groups.is_a?(Array)
570
+
571
+ settings["hooks"][event] = groups.map do |group|
572
+ if group.is_a?(Hash) && group["hooks"].is_a?(Array)
573
+ group["hooks"].reject! { |h| h["command"].to_s.include?("plastic-") }
574
+ group unless group["hooks"].empty?
575
+ elsif group.is_a?(Hash) && group["command"]
576
+ group["command"].to_s.include?("plastic-") ? nil : group
577
+ else
578
+ group
579
+ end
580
+ end.compact
408
581
  end
409
- settings["hooks"].delete_if { |_, v| v.empty? }
582
+
583
+ settings["hooks"].delete_if { |_, v| v.is_a?(Array) && v.empty? }
410
584
  settings.delete("hooks") if settings["hooks"]&.empty?
585
+ if settings.dig("statusLine", "command").to_s.include?("plastic-")
586
+ settings.delete("statusLine")
587
+ original_path = File.join(PLASTIC_HOME, ".cache", "original-statusline.json")
588
+ if File.exist?(original_path)
589
+ original = JSON.parse(File.read(original_path)) rescue nil
590
+ settings["statusLine"] = original if original
591
+ end
592
+ end
593
+
594
+ # Remove from enabledPlugins
595
+ if settings["enabledPlugins"]
596
+ settings["enabledPlugins"].delete("plastic@plastic")
597
+ settings.delete("enabledPlugins") if settings["enabledPlugins"].empty?
598
+ end
411
599
 
412
600
  write_json_atomic(settings_path, settings)
413
601
  end
@@ -2,6 +2,7 @@
2
2
  # encoding: UTF-8
3
3
 
4
4
  require "json"
5
+ require "yaml"
5
6
  require "fileutils"
6
7
  require "tempfile"
7
8
 
@@ -136,4 +137,37 @@ module Bridge
136
137
 
137
138
  nil # no gate violation
138
139
  end
140
+
141
+ PROJECT_CONFIG_DEFAULTS = {
142
+ "governing_docs" => ["AGENTS.md"],
143
+ "release" => {
144
+ "on_complete" => "commit",
145
+ },
146
+ }.freeze
147
+
148
+ def self.read_project_config(slug)
149
+ path = File.join(Dir.home, ".plastic", "projects", slug, "project.yml")
150
+ config = if File.exist?(path)
151
+ YAML.safe_load(File.read(path)) || {}
152
+ else
153
+ {}
154
+ end
155
+
156
+ deep_merge(PROJECT_CONFIG_DEFAULTS, config)
157
+ rescue => e
158
+ $stderr.puts "Warning: failed to read project config for #{slug}: #{e.message}"
159
+ PROJECT_CONFIG_DEFAULTS.dup
160
+ end
161
+
162
+ def self.deep_merge(base, overlay)
163
+ result = base.dup
164
+ overlay.each do |key, value|
165
+ if value.is_a?(Hash) && result[key].is_a?(Hash)
166
+ result[key] = deep_merge(result[key], value)
167
+ else
168
+ result[key] = value
169
+ end
170
+ end
171
+ result
172
+ end
139
173
  end
@@ -0,0 +1,26 @@
1
+ ## Active Intent Gate
2
+
3
+ Before proceeding, resolve the active intent:
4
+
5
+ 1. **Detect store:**
6
+ - Read `~/.plastic/projects.yml`
7
+ - Match CWD against registered project paths
8
+ - If match → project store at `~/.plastic/projects/{slug}/store/`
9
+ - If no match → global store at `~/.plastic/store/`
10
+
11
+ 2. **Find active intent:**
12
+ - Read `INDEX.md` from the detected store
13
+ - Look under `## Active` for intent entries
14
+ - If exactly one active intent → use it
15
+ - If multiple active intents → ask user which one
16
+ - If no active intent → refuse: "No active intent. Create one first with /plastic:creating-intent"
17
+
18
+ 3. **Resolve paths:**
19
+ - Intent directory: `{store}/store/{id}--{slug}/`
20
+ - Spec: `{intent_dir}/spec.md`
21
+ - Plan: `{intent_dir}/plan.md`
22
+ - Checklist: `{intent_dir}/checklist.md`
23
+ - Resources: `{intent_dir}/resources/`
24
+ - Outcome: `{intent_dir}/outcome.md`
25
+
26
+ All lifecycle artifacts MUST be written to the intent directory. Never write to `docs/superpowers/specs/`, `docs/superpowers/plans/`, or any other external path.
@@ -111,12 +111,26 @@ During initial project creation, all decisions are non-destructive by definition
111
111
  1. Verify all checklist items are checked
112
112
  2. Write `outcome.md` with detailed results
113
113
  3. Write `## Outcome` summary in the intent file (1-2 sentences)
114
- 4. Review `## Insights` for observations that should spawn future intents. If any:
114
+ 4. **Release (if configured)**
115
+ 1. Detect project — match CWD against paths in `~/.plastic/projects.yml` to find the project slug. If no match, skip to step 5 (default commit-only behavior).
116
+ 2. Read `~/.plastic/projects/{slug}/project.yml`. If the file doesn't exist or has no `release` key, skip to step 5.
117
+ 3. Based on `release.on_complete`:
118
+ - `commit` — git add + commit (same as default, proceed to step 5)
119
+ - `commit_and_push` — git add + commit + push
120
+ - `manual` — skip auto-commit, notify user: "Release configured as manual — commit when ready."
121
+ 4. If `release.verify` is set, run the verify command (e.g. `bundle exec rake test`):
122
+ - **Exit 0 (green):** proceed to sub-step 5
123
+ - **Non-zero (red):** check `release.on_red`:
124
+ - `fix_and_retry` — attempt to fix the failure, re-run verify (max 2 retries)
125
+ - `stop` — write `savepoint.md` with current state, notify user: "Verify failed — savepoint written.", **STOP**
126
+ - `manual` — notify user: "Verify failed: [summary]. Resolve manually."
127
+ 5. If `release.on_green` has items, invoke `plastic:releasing` to handle them (tag, changelog, publish, etc.). Do NOT duplicate release logic — delegate entirely.
128
+ 5. Review `## Insights` for observations that should spawn future intents. If any:
115
129
  - Create them (using `plastic:creating-intent` conventions)
116
130
  - Update `chain` in the current intent's frontmatter
117
- 5. Move intent from `## Active` to `## Completed` in INDEX.md (with today's date)
118
- 6. Auto-commit: `cd <store-root> && git add . && git commit -m "feat: deliver intent <ID> — <name>"`
119
- 7. Notify user: "Intent [ID] — [name] delivered. [1-2 sentence summary]. See outcome.md for details."
131
+ 6. Move intent from `## Active` to `## Completed` in INDEX.md (with today's date)
132
+ 7. Auto-commit: `cd <store-root> && git add . && git commit -m "feat: deliver intent <ID> — <name>"`
133
+ 8. Notify user: "Intent [ID] — [name] delivered. [1-2 sentence summary]. See outcome.md for details."
120
134
 
121
135
  ## Error Handling
122
136
 
@@ -0,0 +1,143 @@
1
+ ---
2
+ name: plastic:brainstorming
3
+ description: "Explore intent requirements and design before implementation. Produces spec.md in the active intent directory."
4
+ ---
5
+
6
+ # Brainstorming Ideas Into Designs
7
+
8
+ Help turn ideas into fully formed designs and specs through natural collaborative dialogue.
9
+
10
+ Announce: "I'm using the brainstorming skill to explore the design for intent {id} — {name}."
11
+
12
+ Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design and get user approval.
13
+
14
+ <HARD-GATE>
15
+ Do NOT invoke any implementation skill, write any code, scaffold any project, or take any implementation action until you have presented a design and the user has approved it. This applies to EVERY project regardless of perceived simplicity.
16
+ </HARD-GATE>
17
+
18
+ ## Active Intent Gate
19
+
20
+ Before proceeding, resolve the active intent:
21
+
22
+ 1. **Detect store:** Read `~/.plastic/projects.yml`, match CWD against registered project paths. If match → project store at `~/.plastic/projects/{slug}/store/`. If no match → global store at `~/.plastic/store/`.
23
+ 2. **Find active intent:** Read `INDEX.md` from the detected store. Look under `## Active`. If exactly one → use it. If multiple → ask which. If none → refuse: "No active intent. Create one first with /plastic:creating-intent"
24
+ 3. **Resolve intent directory:** `{store}/store/{id}--{slug}/`
25
+
26
+ All artifacts go to the intent directory. Never write to external paths.
27
+
28
+ ## Anti-Pattern: "This Is Too Simple To Need A Design"
29
+
30
+ Every project goes through this process. A todo list, a single-function utility, a config change — all of them. "Simple" projects are where unexamined assumptions cause the most wasted work. The design can be short (a few sentences for truly simple projects), but you MUST present it and get approval.
31
+
32
+ ## Checklist
33
+
34
+ You MUST create a task for each of these items and complete them in order:
35
+
36
+ 1. **Explore project context** — check files, docs, recent commits, read active intent
37
+ 2. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria
38
+ 3. **Propose 2-3 approaches** — with trade-offs and your recommendation
39
+ 4. **Present design** — in sections scaled to their complexity, get user approval after each section
40
+ 5. **Write spec** — save to `{intent_dir}/spec.md` and commit to store repo
41
+ 6. **Spec self-review** — placeholder scan, consistency, scope, ambiguity
42
+ 7. **User reviews written spec** — ask user to review before proceeding
43
+ 8. **Transition to planning** — invoke `plastic:writing-plans`
44
+
45
+ ## Process Flow
46
+
47
+ ```dot
48
+ digraph brainstorming {
49
+ "Explore project context" [shape=box];
50
+ "Ask clarifying questions" [shape=box];
51
+ "Propose 2-3 approaches" [shape=box];
52
+ "Present design sections" [shape=box];
53
+ "User approves design?" [shape=diamond];
54
+ "Write spec" [shape=box];
55
+ "Spec self-review\n(fix inline)" [shape=box];
56
+ "User reviews spec?" [shape=diamond];
57
+ "Invoke plastic:writing-plans" [shape=doublecircle];
58
+
59
+ "Explore project context" -> "Ask clarifying questions";
60
+ "Ask clarifying questions" -> "Propose 2-3 approaches";
61
+ "Propose 2-3 approaches" -> "Present design sections";
62
+ "Present design sections" -> "User approves design?";
63
+ "User approves design?" -> "Present design sections" [label="no, revise"];
64
+ "User approves design?" -> "Write spec" [label="yes"];
65
+ "Write spec" -> "Spec self-review\n(fix inline)";
66
+ "Spec self-review\n(fix inline)" -> "User reviews spec?";
67
+ "User reviews spec?" -> "Write spec" [label="changes requested"];
68
+ "User reviews spec?" -> "Invoke plastic:writing-plans" [label="approved"];
69
+ }
70
+ ```
71
+
72
+ **The terminal state is invoking `plastic:writing-plans`.** Do NOT invoke any other implementation skill. The ONLY skill you invoke after brainstorming is `plastic:writing-plans`.
73
+
74
+ ## The Process
75
+
76
+ **Understanding the idea:**
77
+ - Check out the current project state first (files, docs, recent commits)
78
+ - Before asking detailed questions, assess scope: if the request describes multiple independent subsystems (e.g., "build a platform with chat, file storage, billing, and analytics"), flag this immediately. Don't spend questions refining details of a project that needs to be decomposed first.
79
+ - If the project is too large for a single spec, help the user decompose into sub-projects: what are the independent pieces, how do they relate, what order should they be built? Then brainstorm the first sub-project through the normal design flow. Each sub-project gets its own spec → plan → implementation cycle.
80
+ - For appropriately-scoped projects, ask questions one at a time to refine the idea
81
+ - Prefer multiple choice questions when possible, but open-ended is fine too
82
+ - Only one question per message - if a topic needs more exploration, break it into multiple questions
83
+ - Focus on understanding: purpose, constraints, success criteria
84
+
85
+ **Exploring approaches:**
86
+ - Propose 2-3 different approaches with trade-offs
87
+ - Present options conversationally with your recommendation and reasoning
88
+ - Lead with your recommended option and explain why
89
+
90
+ **Presenting the design:**
91
+ - Once you believe you understand what you're building, present the design
92
+ - Scale each section to its complexity: a few sentences if straightforward, up to 200-300 words if nuanced
93
+ - Ask after each section whether it looks right so far
94
+ - Cover: architecture, components, data flow, error handling, testing
95
+ - Be ready to go back and clarify if something doesn't make sense
96
+
97
+ **Design for isolation and clarity:**
98
+ - Break the system into smaller units that each have one clear purpose, communicate through well-defined interfaces, and can be understood and tested independently
99
+ - For each unit, you should be able to answer: what does it do, how do you use it, and what does it depend on?
100
+ - Can someone understand what a unit does without reading its internals? Can you change the internals without breaking consumers? If not, the boundaries need work.
101
+ - Smaller, well-bounded units are also easier for you to work with - you reason better about code you can hold in context at once, and your edits are more reliable when files are focused. When a file grows large, that's often a signal that it's doing too much.
102
+
103
+ **Working in existing codebases:**
104
+ - Explore the current structure before proposing changes. Follow existing patterns.
105
+ - Where existing code has problems that affect the work (e.g., a file that's grown too large, unclear boundaries, tangled responsibilities), include targeted improvements as part of the design - the way a good developer improves code they're working in.
106
+ - Don't propose unrelated refactoring. Stay focused on what serves the current goal.
107
+
108
+ ## After the Design
109
+ **Documentation:**
110
+ - Write the validated design (spec) to `{intent_dir}/spec.md`
111
+ - Use elements-of-style:writing-clearly-and-concisely skill if available
112
+ - Commit to the store repo:
113
+ ```
114
+ cd {store_root} && git add . && git commit -m "docs: spec for intent {id} — {name}"
115
+ ```
116
+
117
+ **Spec Self-Review:**
118
+ After writing the spec document, look at it with fresh eyes:
119
+ 1. **Placeholder scan:** Any "TBD", "TODO", incomplete sections, or vague requirements? Fix them.
120
+ 2. **Internal consistency:** Do any sections contradict each other? Does the architecture match the feature descriptions?
121
+ 3. **Scope check:** Is this focused enough for a single implementation plan, or does it need decomposition?
122
+ 4. **Ambiguity check:** Could any requirement be interpreted two different ways? If so, pick one and make it explicit.
123
+
124
+ Fix any issues inline. No need to re-review — just fix and move on.
125
+
126
+ **User Review Gate:**
127
+ After the spec review loop passes, ask the user to review the written spec before proceeding:
128
+ > "Spec written and committed to `{intent_dir}/spec.md`. Please review it and let me know if you want to make any changes before we start writing out the implementation plan."
129
+
130
+ Wait for the user's response. If they request changes, make them and re-run the spec review loop. Only proceed once the user approves.
131
+
132
+ **Implementation:**
133
+ - Invoke `plastic:writing-plans` to create the implementation plan
134
+ - Do NOT invoke any other skill. `plastic:writing-plans` is the next step.
135
+
136
+ ## Key Principles
137
+
138
+ - **One question at a time** - Don't overwhelm with multiple questions
139
+ - **Multiple choice preferred** - Easier to answer than open-ended when possible
140
+ - **YAGNI ruthlessly** - Remove unnecessary features from all designs
141
+ - **Explore alternatives** - Always propose 2-3 approaches before settling
142
+ - **Incremental validation** - Present design, get approval before moving on
143
+ - **Be flexible** - Go back and clarify when something doesn't make sense