@zalom/plastic 1.0.0-alpha.9 → 1.0.0-beta.2

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.
Files changed (99) hide show
  1. package/PLASTIC.md +163 -469
  2. package/README.md +95 -58
  3. package/agents/plastic-brainstorming.md +37 -0
  4. package/agents/plastic-enforcer.md +36 -0
  5. package/agents/plastic-executor.md +37 -0
  6. package/agents/{future-intent-researcher.md → plastic-future-intent-researcher.md} +1 -1
  7. package/agents/{intent-curator.md → plastic-intent-curator.md} +1 -1
  8. package/agents/plastic-planner.md +37 -0
  9. package/agents/plastic-spec-specialist.md +37 -0
  10. package/bin/plastic.js +57 -0
  11. package/bin/test +28 -0
  12. package/deprecations.yml +1 -10
  13. package/hooks/auto-arm +5 -0
  14. package/hooks/bash-gate +3 -0
  15. package/hooks/check-update +12 -8
  16. package/hooks/code-gate +12 -0
  17. package/hooks/create-gate +3 -0
  18. package/hooks/gate-check +3 -1
  19. package/hooks/hooks.json +52 -0
  20. package/hooks/qmd-search +8 -0
  21. package/package.json +2 -2
  22. package/scripts/dashboard.rb +687 -0
  23. package/scripts/doctor.rb +1054 -628
  24. package/scripts/hook-auto-arm +51 -0
  25. package/scripts/hook-bash-gate +41 -0
  26. package/scripts/hook-code-gate +27 -0
  27. package/scripts/hook-continue +15 -114
  28. package/scripts/hook-create-gate +59 -0
  29. package/scripts/hook-gate-check +47 -32
  30. package/scripts/hook-qmd-search +44 -0
  31. package/scripts/hook-session-start +106 -38
  32. package/scripts/install.rb +91 -529
  33. package/scripts/lib/boot_banner.rb +28 -0
  34. package/scripts/lib/bridge.rb +404 -19
  35. package/scripts/lib/installer_core.rb +807 -0
  36. package/scripts/lib/intent_validator.rb +235 -0
  37. package/scripts/lib/qmd_hook.rb +44 -0
  38. package/scripts/lib/qmd_sync.rb +209 -0
  39. package/scripts/lib/store_provisioning.rb +100 -0
  40. package/scripts/migrate-to-global +1 -1
  41. package/scripts/new-intent +226 -0
  42. package/scripts/provision-project-store +53 -0
  43. package/scripts/qmd-sync +92 -0
  44. package/scripts/select-update-target +93 -0
  45. package/scripts/spawn-preamble +121 -0
  46. package/scripts/uninstall.rb +53 -0
  47. package/scripts/update.rb +164 -0
  48. package/scripts/validate-intent +54 -0
  49. package/scripts/versions.rb +141 -0
  50. package/skills/_active-intent-gate.md +1 -1
  51. package/skills/add-project-store/SKILL.md +54 -0
  52. package/skills/auto/SKILL.md +77 -7
  53. package/skills/auto/evals/evals.json +207 -0
  54. package/skills/auto/references/agent-architecture.md +135 -0
  55. package/skills/brainstorming/SKILL.md +9 -9
  56. package/skills/brainstorming-grill-me/SKILL.md +6 -6
  57. package/skills/continuing/SKILL.md +92 -82
  58. package/skills/continuing/evals/evals.json +136 -0
  59. package/skills/continuing/references/context-management.md +32 -0
  60. package/skills/creating-intent/SKILL.md +74 -36
  61. package/skills/creating-intent/evals/evals.json +56 -0
  62. package/skills/creating-intent/references/lifecycle.md +76 -0
  63. package/skills/creating-intent/references/wikilinks.md +8 -0
  64. package/skills/creating-project/SKILL.md +40 -8
  65. package/skills/creating-project/references/hubs-projects.md +55 -0
  66. package/skills/dashboard/SKILL.md +121 -0
  67. package/skills/dashboard/templates/dashboard-global.md +31 -0
  68. package/skills/dashboard/templates/dashboard-project.md +40 -0
  69. package/skills/doctor/SKILL.md +51 -4
  70. package/skills/doctor/references/gates-stuck-detection.md +38 -0
  71. package/skills/doctor/report.md +4 -0
  72. package/skills/evaluating-skills/SKILL.md +140 -0
  73. package/skills/evaluating-skills/assets/eval-template.json +12 -0
  74. package/skills/evaluating-skills/evals/evals.json +75 -0
  75. package/skills/evaluating-skills/references/convention-checks.md +76 -0
  76. package/skills/evaluating-skills/references/eval-methodology.md +154 -0
  77. package/skills/executing-plan/SKILL.md +5 -3
  78. package/skills/install/SKILL.md +69 -8
  79. package/skills/intent-curator/SKILL.md +3 -3
  80. package/skills/linking-intents/SKILL.md +16 -7
  81. package/skills/linking-intents/references/zettelkasten.md +38 -0
  82. package/skills/managing-index/SKILL.md +5 -1
  83. package/skills/managing-index/references/zettelkasten-linking.md +1 -1
  84. package/skills/releasing/SKILL.md +80 -23
  85. package/skills/releasing/references/deprecations.md +60 -0
  86. package/skills/research/SKILL.md +2 -2
  87. package/skills/savepoint/SKILL.md +46 -37
  88. package/skills/savepoint/references/context-management.md +32 -0
  89. package/skills/uninstall/SKILL.md +39 -28
  90. package/skills/update/SKILL.md +41 -44
  91. package/skills/versions/SKILL.md +65 -0
  92. package/skills/writing-instructions/SKILL.md +159 -0
  93. package/skills/writing-instructions/references/agentskills-spec.md +135 -0
  94. package/skills/writing-plans/SKILL.md +5 -5
  95. package/templates/agents.md +7 -7
  96. package/templates/outcome.md +13 -0
  97. package/templates/savepoint.md +14 -13
  98. package/templates/spec.md +25 -0
  99. package/bin/install.js +0 -29
@@ -7,6 +7,9 @@ require "json"
7
7
  require "date"
8
8
  require "yaml"
9
9
  require_relative "lib/bridge"
10
+ require_relative "lib/boot_banner"
11
+ require_relative "lib/qmd_sync"
12
+ require_relative "doctor"
10
13
 
11
14
  index_path, store_root, mode, plugin_root = ARGV
12
15
  exit 0 unless index_path && store_root && mode
@@ -117,15 +120,23 @@ if File.exist?(projects_path)
117
120
  end
118
121
  end
119
122
 
123
+ # --- Load PLASTIC.md conventions ---
124
+
125
+ plastic_md_path = "#{store_root}/PLASTIC.md"
126
+ plastic_md = File.exist?(plastic_md_path) ? File.read(plastic_md_path).strip : nil
127
+
120
128
  # --- Load deprecations ---
121
129
 
130
+ dep_file = if plugin_root && !plugin_root.empty?
131
+ "#{plugin_root}/deprecations.yml"
132
+ else
133
+ "#{store_root}/deprecations.yml"
134
+ end
135
+
122
136
  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
137
+ if File.exist?(dep_file)
138
+ dep_data = YAML.safe_load(File.read(dep_file)) rescue {}
139
+ deprecations = dep_data["deprecations"] || []
129
140
  end
130
141
 
131
142
  dismissed_json = `"#{read_config}" deprecations_dismissed`.strip
@@ -136,7 +147,10 @@ rescue
136
147
  end
137
148
 
138
149
  current_version = nil
139
- if plugin_root && !plugin_root.empty?
150
+ version_file = "#{store_root}/VERSION"
151
+ if File.exist?(version_file)
152
+ current_version = File.read(version_file).strip
153
+ elsif plugin_root && !plugin_root.empty?
140
154
  plugin_json_path = "#{plugin_root}/.claude-plugin/plugin.json"
141
155
  if File.exist?(plugin_json_path)
142
156
  pj = JSON.parse(File.read(plugin_json_path)) rescue {}
@@ -157,53 +171,98 @@ cache_file = "#{store_root}/.cache/update-check.json"
157
171
  if File.exist?(cache_file)
158
172
  cache = JSON.parse(File.read(cache_file)) rescue {}
159
173
  if cache["updateAvailable"]
160
- update_notice = "Plastic update available: #{cache["current"]} -> #{cache["latest"]} — run /plastic:update"
174
+ update_notice = "Plastic update available: #{cache["current"]} -> #{cache["latest"]} — run /plastic-update"
161
175
  end
162
176
  end
163
177
 
164
- # --- Early exit if nothing to show ---
178
+ # --- Run core health in-process (intent 36a) ---
179
+ # Reuse Doctor's own --core checks so there is one source of truth for "core
180
+ # health" (no second process spawn, no duplicated check list). Never blocks the
181
+ # session: any failure or exception degrades to a banner and we continue.
182
+
183
+ core_health = begin
184
+ Doctor.new(plastic_home: store_root).run_core_checks("claude")
185
+ rescue
186
+ nil
187
+ end
188
+ core_banner = BootBanner.render(health: core_health, version: current_version)
189
+
190
+ # --- Assemble session context ---
165
191
 
166
192
  all_active = active + project_active
167
193
  all_future = future + project_future
168
- exit 0 if all_active.empty? && stale.empty? && all_future.empty? && active_deprecations.empty? && update_notice.nil?
169
194
 
170
195
  # --- Build context ---
196
+ # PLASTIC.md conventions and intent context render only when conventions are
197
+ # installed. Deprecation warnings and update notices render regardless, so a
198
+ # partial install (or a missing PLASTIC.md) still surfaces critical warnings.
171
199
 
172
200
  parts = []
173
201
 
174
- if current_project
175
- slug = current_project["slug"]
176
- banner = "Project: #{slug} | Store: ~/.plastic/projects/#{slug}/store/"
177
- if project_active.any? && project_active.first =~ /\[([^\]]+)\].*store\/([\w-]+)\//
178
- intent_name, dir_name = $1, $2
179
- intent_id = dir_name.split("--").first
180
- banner += "\nActive: [#{intent_id} #{intent_name}] | Artifacts → store/#{dir_name}/"
202
+ # Boot banner first: every session start surfaces that Plastic loaded, with the
203
+ # version (clean) or the first failing core check (degraded). Owned by the hook
204
+ # so it runs by construction — the plastic-continuing skill no longer does this.
205
+ parts << core_banner
206
+ parts << ""
207
+
208
+ # --- QMD search status (intent 45a, READ-ONLY) ---
209
+ # Report-only line for the model (additionalContext), never systemMessage and
210
+ # never the exit code. QmdSync.status shells out to `qmd collection list`, so the
211
+ # whole block is guarded: a 2s timeout caps any hang, and rescue-all guarantees a
212
+ # slow/broken/missing qmd appends nothing and the hook continues cleanly.
213
+ begin
214
+ require "timeout"
215
+ qmd_status = Timeout.timeout(2) { QmdSync.status(plastic_home: store_root) }
216
+ if qmd_status[:present]
217
+ if qmd_status[:all_registered]
218
+ parts << "QMD: #{qmd_status[:registered].size} Plastic collections indexed (search with the qmd skill)."
219
+ else
220
+ parts << "QMD detected — run `qmd-sync register --all` to index your Plastic stores for search."
221
+ end
181
222
  end
182
- parts << banner + "\n"
183
- else
184
- parts << "PLASTIC — Global store loaded from ~/.plastic/\n"
223
+ rescue Exception
224
+ # Any failure (timeout, missing binary, parse error) — stay silent, never crash.
185
225
  end
186
226
 
187
- if all_active.any?
188
- parts << "Active intents:\n"
189
- all_active.each { |a| parts << a }
190
- if bridge_data
191
- stage = bridge_data["build"]["stage"]
192
- missing = bridge_data["build"]["missing"]
193
- missing_str = missing.empty? ? "none" : missing.join(", ")
194
- parts << "Stage: #{stage} | Next: #{missing_str}"
227
+ if plastic_md
228
+ # Conventions always loaded first
229
+ parts << plastic_md
230
+ parts << "\n---\n"
231
+
232
+ if current_project
233
+ slug = current_project["slug"]
234
+ banner = "Project: #{slug} | Store: ~/.plastic/projects/#{slug}/store/"
235
+ if project_active.any? && project_active.first =~ /\[([^\]]+)\].*store\/([\w-]+)\//
236
+ intent_name, dir_name = $1, $2
237
+ intent_id = dir_name.split("--").first
238
+ banner += "\nActive: [#{intent_id} — #{intent_name}] | Artifacts → store/#{dir_name}/"
239
+ end
240
+ parts << banner + "\n"
241
+ else
242
+ parts << "PLASTIC — Global store loaded from ~/.plastic/\n"
195
243
  end
196
- parts << ""
197
- else
198
- parts << "No active intents. #{future.length} future intents available.\n"
199
- end
200
244
 
201
- if stale.any?
202
- parts << "\nStale future intents (untouched for days):\n"
203
- stale.each do |s|
204
- parts << "- #{s[:name]} (#{s[:age]} days) — consider: activate, abandon, or defer to agent"
245
+ if all_active.any?
246
+ parts << "Active intents:\n"
247
+ all_active.each { |a| parts << a }
248
+ if bridge_data
249
+ stage = bridge_data["build"]["stage"]
250
+ missing = bridge_data["build"]["missing"]
251
+ missing_str = missing.empty? ? "none" : missing.join(", ")
252
+ parts << "Stage: #{stage} | Next: #{missing_str}"
253
+ end
254
+ parts << ""
255
+ else
256
+ parts << "No active intents. #{future.length} future intents available.\n"
257
+ end
258
+
259
+ if stale.any?
260
+ parts << "\nStale future intents (untouched for days):\n"
261
+ stale.each do |s|
262
+ parts << "- #{s[:name]} (#{s[:age]} days) — consider: activate, abandon, or defer to agent"
263
+ end
264
+ parts << "\nWhen appropriate, ask the user what to do with stale intents."
205
265
  end
206
- parts << "\nWhen appropriate, ask the user what to do with stale intents."
207
266
  end
208
267
 
209
268
  if active_deprecations.any?
@@ -237,10 +296,19 @@ if update_notice
237
296
  parts.unshift("! #{update_notice}\n")
238
297
  end
239
298
 
299
+ # Emit nothing when there is genuinely nothing to surface (no conventions,
300
+ # no deprecations, no update notice).
301
+ exit 0 if parts.join.strip.empty?
302
+
240
303
  payload = {
241
304
  "hookSpecificOutput" => {
242
305
  "hookEventName" => "SessionStart",
243
306
  "additionalContext" => parts.join("\n")
244
- }
307
+ },
308
+ # Intent 54: additionalContext is model-only, so the banner stays invisible to
309
+ # the human. The top-level systemMessage channel is rendered in the user's
310
+ # terminal (and re-fires on /clear). Reuse the same BootBanner line so the
311
+ # visible banner and the model-facing banner cannot drift.
312
+ "systemMessage" => core_banner
245
313
  }
246
314
  puts JSON.generate(payload)