@zalom/plastic 1.0.0-alpha.2 → 1.0.0-alpha.21
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 +128 -473
- package/README.md +90 -58
- package/agents/future-intent-researcher.md +1 -1
- package/agents/intent-curator.md +1 -1
- package/bin/plastic.js +57 -0
- package/bin/test +28 -0
- package/deprecations.yml +7 -6
- package/hooks/auto-arm +5 -0
- package/hooks/bash-gate +3 -0
- package/hooks/check-update +12 -8
- package/hooks/code-gate +10 -0
- package/hooks/hooks.json +25 -4
- package/hooks/statusline +50 -10
- package/package.json +2 -2
- package/scripts/dashboard.rb +480 -0
- package/scripts/doctor.rb +973 -0
- package/scripts/hook-auto-arm +52 -0
- package/scripts/hook-bash-gate +53 -0
- package/scripts/hook-code-gate +39 -0
- package/scripts/hook-continue +15 -114
- package/scripts/hook-gate-check +19 -4
- package/scripts/hook-session-start +76 -31
- package/scripts/install.rb +91 -480
- package/scripts/lib/bridge.rb +255 -0
- package/scripts/lib/installer_core.rb +760 -0
- package/scripts/migrate-to-global +1 -1
- package/scripts/select-update-target +93 -0
- package/scripts/uninstall.rb +53 -0
- package/scripts/update.rb +142 -0
- package/scripts/versions.rb +141 -0
- package/skills/_active-intent-gate.md +26 -0
- package/skills/auto/SKILL.md +62 -9
- package/skills/auto/evals/evals.json +92 -0
- package/skills/auto/references/agent-architecture.md +60 -0
- package/skills/brainstorming/SKILL.md +143 -0
- package/skills/brainstorming-grill-me/SKILL.md +5 -5
- package/skills/continuing/SKILL.md +102 -77
- package/skills/continuing/evals/evals.json +136 -0
- package/skills/continuing/references/context-management.md +32 -0
- package/skills/creating-intent/SKILL.md +16 -1
- package/skills/creating-intent/references/lifecycle.md +74 -0
- package/skills/creating-intent/references/wikilinks.md +8 -0
- package/skills/creating-project/SKILL.md +8 -4
- package/skills/creating-project/references/hubs-projects.md +55 -0
- package/skills/dashboard/SKILL.md +92 -0
- package/skills/doctor/SKILL.md +116 -0
- package/skills/doctor/references/gates-stuck-detection.md +38 -0
- package/skills/doctor/report.md +96 -0
- package/skills/evaluating-skills/SKILL.md +140 -0
- package/skills/evaluating-skills/assets/eval-template.json +12 -0
- package/skills/evaluating-skills/evals/evals.json +75 -0
- package/skills/evaluating-skills/references/convention-checks.md +76 -0
- package/skills/evaluating-skills/references/eval-methodology.md +154 -0
- package/skills/executing-plan/SKILL.md +3 -3
- package/skills/install/SKILL.md +56 -8
- package/skills/intent-curator/SKILL.md +3 -3
- package/skills/linking-intents/SKILL.md +5 -1
- package/skills/linking-intents/references/zettelkasten.md +33 -0
- package/skills/managing-index/SKILL.md +5 -1
- package/skills/releasing/SKILL.md +119 -18
- package/skills/releasing/references/deprecations.md +44 -0
- package/skills/research/SKILL.md +114 -0
- package/skills/savepoint/SKILL.md +46 -37
- package/skills/savepoint/references/context-management.md +32 -0
- package/skills/uninstall/SKILL.md +39 -28
- package/skills/update/SKILL.md +41 -36
- package/skills/versions/SKILL.md +65 -0
- package/skills/writing-instructions/SKILL.md +159 -0
- package/skills/writing-instructions/references/agentskills-spec.md +135 -0
- package/skills/writing-plans/SKILL.md +183 -0
- package/templates/agents.md +16 -0
- package/templates/outcome.md +13 -0
- package/templates/project.yml +5 -0
- package/templates/savepoint.md +14 -13
- package/templates/spec.md +25 -0
- package/bin/install.js +0 -29
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# Usage: hook-auto-arm <message>
|
|
6
|
+
# UserPromptSubmit arming (intent 27). Two jobs, both additive context — never blocks:
|
|
7
|
+
# 1. Detect auto-trigger phrases -> steer the agent to invoke plastic-auto.
|
|
8
|
+
# 2. If auto mode is armed and the active intent hasn't reached How -> remind the agent
|
|
9
|
+
# that project-code edits are gate-blocked until plan.md + checklist.md exist.
|
|
10
|
+
# Outputs UserPromptSubmit hookSpecificOutput JSON, or exits silently.
|
|
11
|
+
|
|
12
|
+
require "json"
|
|
13
|
+
require_relative "lib/bridge"
|
|
14
|
+
|
|
15
|
+
message = (ARGV[0] || "").downcase
|
|
16
|
+
parts = []
|
|
17
|
+
|
|
18
|
+
# --- 1. Auto-trigger steering ---
|
|
19
|
+
AUTO_TRIGGERS = ["take it from here", "deliver this", "/plastic-auto", "auto mode"].freeze
|
|
20
|
+
# bare "auto" only as a standalone word to avoid matching "automatic", "automation", etc.
|
|
21
|
+
triggered = AUTO_TRIGGERS.any? { |p| message.include?(p) } || message.match?(/\bauto\b/)
|
|
22
|
+
if triggered
|
|
23
|
+
parts << "Auto-mode request detected. Invoke the plastic-auto skill to deliver the " \
|
|
24
|
+
"active intent autonomously — it arms the lifecycle gate so project code " \
|
|
25
|
+
"cannot be edited before plan.md + checklist.md exist."
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# --- 2. Standing gate reminder when armed and pre-How ---
|
|
29
|
+
session = ENV["CLAUDE_SESSION_ID"]
|
|
30
|
+
bridge_data = (session && !session.empty? && File.exist?(Bridge.path(session))) ? Bridge.read(session) : nil
|
|
31
|
+
if bridge_data && bridge_data.dig("build", "auto") == true
|
|
32
|
+
intent = bridge_data["intent"] || {}
|
|
33
|
+
store = intent["store"]; dir = intent["dir"]
|
|
34
|
+
if store && dir
|
|
35
|
+
intent_dir = File.expand_path("#{store}/#{dir}")
|
|
36
|
+
reached_how = File.exist?("#{intent_dir}/plan.md") && File.exist?("#{intent_dir}/checklist.md")
|
|
37
|
+
unless reached_how
|
|
38
|
+
parts << "Auto mode armed on intent #{intent["id"]} — produce spec -> plan -> " \
|
|
39
|
+
"checklist before editing project code. Code edits are gate-blocked until then."
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
exit 0 if parts.empty?
|
|
45
|
+
|
|
46
|
+
payload = {
|
|
47
|
+
"hookSpecificOutput" => {
|
|
48
|
+
"hookEventName" => "UserPromptSubmit",
|
|
49
|
+
"additionalContext" => parts.join("\n")
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
puts JSON.generate(payload)
|
|
@@ -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
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# Usage: hook-code-gate <file_path>
|
|
6
|
+
# PreToolUse gate (intent 27): when auto mode is armed and the active intent has not
|
|
7
|
+
# reached How (plan.md + checklist.md), block edits to project code outside the store.
|
|
8
|
+
#
|
|
9
|
+
# Exit 0 = allow. Exit 2 = block (reason on stderr, shown to the agent).
|
|
10
|
+
# No bridge / auto not armed / How reached / path under ~/.plastic or the intent dir = allow.
|
|
11
|
+
|
|
12
|
+
require_relative "lib/bridge"
|
|
13
|
+
|
|
14
|
+
file_path = ARGV[0]
|
|
15
|
+
exit 0 unless file_path && !file_path.empty?
|
|
16
|
+
|
|
17
|
+
# --- Load bridge (mirror hook-gate-check resolution) ---
|
|
18
|
+
session = ENV["CLAUDE_SESSION_ID"]
|
|
19
|
+
bridge_data = nil
|
|
20
|
+
|
|
21
|
+
if session && !session.empty?
|
|
22
|
+
bridge_data = Bridge.read(session) if File.exist?(Bridge.path(session))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
unless bridge_data
|
|
26
|
+
candidates = Dir.glob("/tmp/plastic-*.json").reject { |f| f.end_with?(".tmp") }
|
|
27
|
+
if candidates.any?
|
|
28
|
+
newest = candidates.max_by { |f| File.mtime(f) }
|
|
29
|
+
bridge_data = JSON.parse(File.read(newest)) rescue nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
exit 0 unless bridge_data
|
|
34
|
+
|
|
35
|
+
reason = Bridge.code_gate_decision(bridge_data, file_path)
|
|
36
|
+
exit 0 unless reason
|
|
37
|
+
|
|
38
|
+
$stderr.puts "PLASTIC GATE — #{reason}"
|
|
39
|
+
exit 2
|
package/scripts/hook-continue
CHANGED
|
@@ -1,130 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# encoding: UTF-8
|
|
3
3
|
# Usage: hook-continue <index_path> <store_root> <mode>
|
|
4
|
-
#
|
|
4
|
+
# Renders the dashboard `continue` cockpit as UserPromptSubmit additionalContext.
|
|
5
|
+
# Exits silently (no output) if the dashboard produces nothing — the bash caller
|
|
6
|
+
# then falls back to a minimal notice.
|
|
5
7
|
|
|
6
8
|
require "json"
|
|
7
|
-
require "
|
|
8
|
-
require "yaml"
|
|
9
|
+
require "open3"
|
|
9
10
|
|
|
10
|
-
index_path,
|
|
11
|
-
exit 0 unless index_path &&
|
|
11
|
+
index_path, _store_root, _mode = ARGV
|
|
12
|
+
exit 0 unless index_path && File.exist?(index_path)
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
dashboard = File.expand_path("dashboard.rb", __dir__)
|
|
15
|
+
exit 0 unless File.exist?(dashboard)
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
future = []
|
|
18
|
-
section = nil
|
|
17
|
+
cockpit, _err, status = Open3.capture3("ruby", dashboard, "continue")
|
|
18
|
+
exit 0 unless status.success? && !cockpit.strip.empty?
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
elsif line.start_with?("## Future")
|
|
25
|
-
section = :future
|
|
26
|
-
next
|
|
27
|
-
elsif line.start_with?("## ")
|
|
28
|
-
section = nil
|
|
29
|
-
next
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
next unless section && line.strip.start_with?("- [")
|
|
33
|
-
|
|
34
|
-
if section == :active
|
|
35
|
-
active << line.strip
|
|
36
|
-
elsif section == :future
|
|
37
|
-
future << line.strip
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# --- Detect stale future intents ---
|
|
42
|
-
|
|
43
|
-
stale = []
|
|
44
|
-
stale_days = 3
|
|
45
|
-
config_path = "#{store_root}/config.yml"
|
|
46
|
-
if File.exist?(config_path)
|
|
47
|
-
config = YAML.safe_load(File.read(config_path)) rescue {}
|
|
48
|
-
stale_days = config["stale_threshold_days"] || 3
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
future.each do |f|
|
|
52
|
-
if f =~ /store\/([\w-]+)\//
|
|
53
|
-
dir_name = $1
|
|
54
|
-
intent_file = "#{store_root}/store/#{dir_name}/#{dir_name}.md"
|
|
55
|
-
next unless File.exist?(intent_file)
|
|
56
|
-
|
|
57
|
-
content = File.read(intent_file)
|
|
58
|
-
if content =~ /^created:\s*['"]?(\d{4}-\d{2}-\d{2})/
|
|
59
|
-
age = (Date.today - Date.parse($1)).to_i
|
|
60
|
-
if age >= stale_days
|
|
61
|
-
name = f[/\[([^\]]+)\]/, 1] || "unknown"
|
|
62
|
-
stale << { name: name, age: age, entry: f }
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# --- Detect current project ---
|
|
69
|
-
|
|
70
|
-
current_project = nil
|
|
71
|
-
if mode == "global"
|
|
72
|
-
projects_path = "#{store_root}/projects.yml"
|
|
73
|
-
if File.exist?(projects_path)
|
|
74
|
-
projects = YAML.safe_load(File.read(projects_path)) rescue {}
|
|
75
|
-
cwd = Dir.pwd
|
|
76
|
-
(projects["projects"] || {}).each do |slug, info|
|
|
77
|
-
project_path = File.expand_path(info["path"])
|
|
78
|
-
if cwd.start_with?(project_path)
|
|
79
|
-
current_project = { "slug" => slug, "parent" => info["parent"], "path" => project_path }
|
|
80
|
-
break
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# --- Build context ---
|
|
87
|
-
|
|
88
|
-
parts = []
|
|
89
|
-
parts << "PLASTIC CONTINUE — The user wants to resume work.\n"
|
|
90
|
-
|
|
91
|
-
if mode == "global" && current_project
|
|
92
|
-
parts << "Project: #{current_project["slug"]} (#{current_project["path"]})\n"
|
|
93
|
-
parts << "Governing intent: #{current_project["parent"]}\n" if current_project["parent"]
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
if active.any?
|
|
97
|
-
parts << "## Active Intents (work on these first)\n"
|
|
98
|
-
active.each { |a| parts << a }
|
|
99
|
-
parts << ""
|
|
100
|
-
else
|
|
101
|
-
parts << "## No Active Intents\n"
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
if future.any?
|
|
105
|
-
parts << "## Future Intents (offer as next work)\n"
|
|
106
|
-
future.each { |f| parts << f }
|
|
107
|
-
parts << ""
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
if stale.any?
|
|
111
|
-
parts << "## Stale Future Intents\n"
|
|
112
|
-
parts << "These future intents have not been actioned. Ask the user what to do with each:\n"
|
|
113
|
-
stale.each do |s|
|
|
114
|
-
parts << "- #{s[:name]} (#{s[:age]} days old) — options: activate, abandon, or defer to agent (implement / research / ideate)"
|
|
115
|
-
end
|
|
116
|
-
parts << ""
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
parts << "Follow the plastic:continuing skill workflow.\n"
|
|
120
|
-
parts << "1. If active intents exist: read their state and resume\n"
|
|
121
|
-
parts << "2. If no active intents: present future intents as options\n"
|
|
122
|
-
parts << "3. If stale intents exist: surface them and ask user to decide"
|
|
20
|
+
context = cockpit.rstrip +
|
|
21
|
+
"\n\nFollow the plastic-continuing skill workflow to resume: " \
|
|
22
|
+
"if active intents exist, read their state and resume; otherwise offer the " \
|
|
23
|
+
"Value×Effort matrix items above, surfacing any stale (Nd) / ⚑ triage intents."
|
|
123
24
|
|
|
124
25
|
payload = {
|
|
125
26
|
"hookSpecificOutput" => {
|
|
126
27
|
"hookEventName" => "UserPromptSubmit",
|
|
127
|
-
"additionalContext" =>
|
|
28
|
+
"additionalContext" => context
|
|
128
29
|
}
|
|
129
30
|
}
|
|
130
31
|
puts JSON.generate(payload)
|
package/scripts/hook-gate-check
CHANGED
|
@@ -98,13 +98,21 @@ if is_stage_file || is_action_file
|
|
|
98
98
|
|
|
99
99
|
Bridge.write(session, bridge_data)
|
|
100
100
|
|
|
101
|
+
# Cycle-step savepoint ledger (intent 34) — record this stage-file milestone.
|
|
102
|
+
# Best-effort: the ledger is derived sugar, so a failure must never block.
|
|
103
|
+
begin
|
|
104
|
+
Bridge.append_savepoint(intent_dir_abs, file_path_abs)
|
|
105
|
+
rescue StandardError
|
|
106
|
+
# ignore — rebuildable from disk
|
|
107
|
+
end
|
|
108
|
+
|
|
101
109
|
# Build transition context
|
|
102
110
|
stage_labels = { "what" => "What", "why" => "Why", "how" => "How", "exec" => "Exec", "done" => "Done" }
|
|
103
111
|
next_hints = {
|
|
104
112
|
"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
|
|
113
|
+
"how" => "Why complete. Invoke plastic-auto to deliver autonomously, or write plan.md manually.",
|
|
114
|
+
"exec" => "How complete. Invoke plastic-auto or plastic-executing-plan to execute, or work through the checklist manually.",
|
|
115
|
+
"done" => "Exec complete. Intent must be completed now — write outcome.md, update INDEX.md, auto-commit. Use plastic-auto or do it manually."
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
context_parts = ["PLASTIC"]
|
|
@@ -129,7 +137,14 @@ if is_stage_file || is_action_file
|
|
|
129
137
|
puts JSON.generate(payload)
|
|
130
138
|
exit 0
|
|
131
139
|
else
|
|
132
|
-
# Regular file in intent dir — just update last_activity
|
|
140
|
+
# Regular file in intent dir — just update last_activity. The intent file
|
|
141
|
+
# (What milestone) lands here, so still offer it to the savepoint ledger;
|
|
142
|
+
# non-milestone files self-filter to a no-op (intent 34).
|
|
143
|
+
begin
|
|
144
|
+
Bridge.append_savepoint(intent_dir_abs, file_path_abs)
|
|
145
|
+
rescue StandardError
|
|
146
|
+
# ignore — rebuildable from disk
|
|
147
|
+
end
|
|
133
148
|
bridge_data["build"]["last_activity"] = Time.now.utc.iso8601
|
|
134
149
|
Bridge.write(session, bridge_data)
|
|
135
150
|
exit 0
|
|
@@ -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
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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,42 +161,68 @@ active_deprecations = deprecations.select do |dep|
|
|
|
150
161
|
!dismissed.include?(dep["id"])
|
|
151
162
|
end
|
|
152
163
|
|
|
153
|
-
# ---
|
|
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
|
+
# --- Assemble session context ---
|
|
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?
|
|
158
179
|
|
|
159
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.
|
|
160
184
|
|
|
161
185
|
parts = []
|
|
162
186
|
|
|
163
|
-
if
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
parts << "
|
|
167
|
-
end
|
|
187
|
+
if plastic_md
|
|
188
|
+
# Conventions always loaded first
|
|
189
|
+
parts << plastic_md
|
|
190
|
+
parts << "\n---\n"
|
|
168
191
|
|
|
169
|
-
if
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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"
|
|
177
203
|
end
|
|
178
|
-
parts << ""
|
|
179
|
-
else
|
|
180
|
-
parts << "No active intents. #{future.length} future intents available.\n"
|
|
181
|
-
end
|
|
182
204
|
|
|
183
|
-
if
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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"
|
|
217
|
+
end
|
|
218
|
+
|
|
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."
|
|
187
225
|
end
|
|
188
|
-
parts << "\nWhen appropriate, ask the user what to do with stale intents."
|
|
189
226
|
end
|
|
190
227
|
|
|
191
228
|
if active_deprecations.any?
|
|
@@ -215,6 +252,14 @@ if active_deprecations.any?
|
|
|
215
252
|
end
|
|
216
253
|
end
|
|
217
254
|
|
|
255
|
+
if update_notice
|
|
256
|
+
parts.unshift("! #{update_notice}\n")
|
|
257
|
+
end
|
|
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
|
+
|
|
218
263
|
payload = {
|
|
219
264
|
"hookSpecificOutput" => {
|
|
220
265
|
"hookEventName" => "SessionStart",
|