@zalom/plastic 1.14.1 → 2.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.
- package/PLASTIC.md +101 -294
- package/README.md +5 -6
- package/agents/plastic-advisor.md +18 -17
- package/agents/plastic-enforcer.md +57 -69
- package/agents/plastic-executor.md +34 -20
- package/agents/plastic-faux-advisor.md +18 -18
- package/bin/lib/context_budget.rb +453 -0
- package/bin/plastic-bench +78 -0
- package/config_asks.yml +38 -0
- package/hooks/capture +4 -0
- package/hooks/close +5 -0
- package/hooks/hooks.json +14 -42
- package/hooks/message-display +81 -0
- package/hooks/record +4 -0
- package/hooks/savepoint +5 -5
- package/package.json +3 -2
- package/scripts/agent-report +15 -10
- package/scripts/append-ledger +208 -0
- package/scripts/codex-hook +78 -90
- package/scripts/dashboard.rb +43 -14
- package/scripts/day-summary +53 -0
- package/scripts/doctor.rb +269 -43
- package/scripts/end-intent +170 -76
- package/scripts/exec-worktree +11 -20
- package/scripts/file-session-intent +61 -0
- package/scripts/hook-capture +255 -0
- package/scripts/hook-close +36 -0
- package/scripts/hook-message-display +74 -0
- package/scripts/hook-record +157 -0
- package/scripts/hook-savepoint +45 -0
- package/scripts/hook-session-start +128 -12
- package/scripts/install.rb +8 -6
- package/scripts/intent-screen +77 -0
- package/scripts/lib/agent_models.rb +1 -7
- package/scripts/lib/arm.rb +308 -0
- package/scripts/lib/backfill_intent.rb +316 -0
- package/scripts/lib/bridge.rb +24 -1597
- package/scripts/lib/compact_instructions.rb +56 -0
- package/scripts/lib/day_summary.rb +211 -0
- package/scripts/lib/doctor_core.rb +66 -88
- package/scripts/lib/doctor_session_ledger.rb +158 -0
- package/scripts/lib/exec_worktree.rb +14 -60
- package/scripts/lib/handoff.rb +184 -0
- package/scripts/lib/harness_text.rb +5 -4
- package/scripts/lib/hook_registry.rb +57 -106
- package/scripts/lib/insights.rb +3 -4
- package/scripts/lib/installer_core.rb +136 -41
- package/scripts/lib/intent_screen.rb +309 -0
- package/scripts/lib/intent_screen_ansi.rb +262 -0
- package/scripts/lib/lock.rb +6 -26
- package/scripts/lib/message_display.rb +290 -0
- package/scripts/lib/outcome_guard.rb +7 -5
- package/scripts/lib/project_validator.rb +30 -1
- package/scripts/lib/report_screen.rb +648 -0
- package/scripts/lib/roadmap_queue.rb +3 -3
- package/scripts/lib/roadmap_savepoint.rb +1 -1
- package/scripts/lib/rule_catalog.rb +6 -3
- package/scripts/lib/savepoint.rb +334 -0
- package/scripts/lib/scaffold_intent.rb +13 -179
- package/scripts/lib/screen_paint.rb +276 -0
- package/scripts/lib/session_backfill.rb +184 -0
- package/scripts/lib/session_close.rb +104 -0
- package/scripts/lib/session_git.rb +500 -0
- package/scripts/lib/session_ledger.rb +621 -0
- package/scripts/lib/store_discovery.rb +1 -1
- package/scripts/lib/store_provisioning.rb +1 -1
- package/scripts/maintenance-run +7 -7
- package/scripts/new-intent +63 -4
- package/scripts/plastic-lock +101 -41
- package/scripts/promote-session-item +150 -0
- package/scripts/read-config +3 -0
- package/scripts/rebuild-graph +1 -1
- package/scripts/report-screen +120 -0
- package/scripts/roadmap-next +2 -2
- package/scripts/rollback.rb +48 -3
- package/scripts/savepoint-note +67 -0
- package/scripts/scaffold-intent +37 -63
- package/scripts/session-commit +116 -0
- package/scripts/spawn-preamble +14 -7
- package/scripts/update.rb +7 -3
- package/scripts/write-handoff +60 -0
- package/skills/agent-advisor/SKILL.md +3 -3
- package/skills/agent-advisor/references/advisor-protocol.md +20 -20
- package/skills/auto/SKILL.md +223 -317
- package/skills/auto/evals/evals.json +10 -10
- package/skills/auto/references/agent-architecture.md +102 -90
- package/skills/auto/references/agent-report-contract.md +4 -21
- package/skills/auto/references/end-tail.md +47 -53
- package/skills/auto/references/human-report-contract.md +62 -56
- package/skills/conventions/SKILL.md +1 -3
- package/skills/conventions/references/lifecycle-and-savepoints.md +35 -1
- package/skills/conventions/references/locks-and-worktrees.md +62 -58
- package/skills/conventions/references/maintenance-and-revisions.md +12 -12
- package/skills/dashboard/SKILL.md +1 -1
- package/skills/direct/SKILL.md +66 -0
- package/skills/direct/references/request-signals.md +59 -0
- package/skills/doctor/SKILL.md +58 -11
- package/skills/doctor/report.md +3 -1
- package/skills/install/SKILL.md +11 -9
- package/skills/intent-continuing/SKILL.md +142 -91
- package/skills/intent-continuing/references/boarding-matrix.md +34 -0
- package/skills/intent-continuing/references/context-management.md +3 -3
- package/skills/intent-creating/SKILL.md +2 -2
- package/skills/intent-creating/references/lifecycle.md +1 -1
- package/skills/intent-ending/SKILL.md +34 -37
- package/skills/intent-ending/evals/evals.json +4 -4
- package/skills/intent-executing/SKILL.md +15 -46
- package/skills/intent-executing/plan-reviewer-prompt.md +39 -0
- package/skills/intent-speccing/SKILL.md +108 -51
- package/skills/intent-speccing/evals/evals.json +5 -5
- package/skills/intent-speccing/references/per-section-fill-rules.md +4 -4
- package/skills/intent-speccing/references/self-verify-checklist.md +1 -2
- package/skills/project-creating/SKILL.md +1 -1
- package/skills/releasing/SKILL.md +7 -8
- package/skills/tutorial/SKILL.md +2 -2
- package/skills/tutorial/references/track-1-guided.md +16 -18
- package/skills/tutorial/references/track-2-auto.md +12 -13
- package/skills/tutorial/references/track-3-projects-and-roadmaps.md +2 -2
- package/templates/agents.md +4 -7
- package/templates/config.yml +5 -0
- package/templates/intent-screen.md +17 -0
- package/templates/outcome.md +14 -1
- package/templates/project.yml +7 -0
- package/templates/report-state.md +11 -0
- package/templates/roadmap.md +1 -1
- package/templates/savepoint.md +2 -2
- package/templates/session-intent.md +29 -0
- package/agents/plastic-brainstorming.md +0 -48
- package/agents/plastic-future-intent-researcher.md +0 -35
- package/agents/plastic-intent-curator.md +0 -47
- package/agents/plastic-intent-discovery.md +0 -43
- package/agents/plastic-planner.md +0 -56
- package/agents/plastic-spec-specialist.md +0 -53
- package/hooks/auto-arm +0 -5
- package/hooks/bash-gate +0 -3
- package/hooks/continue +0 -31
- package/hooks/edit-gates +0 -3
- package/hooks/future-intent-check +0 -25
- package/hooks/gate-check +0 -12
- package/hooks/power-tools +0 -8
- package/scripts/hook-auto-arm +0 -51
- package/scripts/hook-bash-gate +0 -78
- package/scripts/hook-code-gate +0 -38
- package/scripts/hook-continue +0 -48
- package/scripts/hook-create-gate +0 -51
- package/scripts/hook-edit-gates +0 -58
- package/scripts/hook-future-intent-check +0 -90
- package/scripts/hook-gate-check +0 -169
- package/scripts/hook-links-gate +0 -54
- package/scripts/hook-lock-gate +0 -47
- package/scripts/hook-power-tools +0 -38
- package/scripts/hook-savepoint-pre +0 -32
- package/scripts/lib/codex_edit_gates.rb +0 -138
- package/scripts/lib/edit_gates.rb +0 -398
- package/scripts/lib/links_gate.rb +0 -140
- package/scripts/lib/qmd_hook.rb +0 -41
- package/scripts/lib/spec_header.rb +0 -83
- package/scripts/lib/start_intent.rb +0 -296
- package/scripts/start-intent +0 -89
- package/skills/_active-intent-gate.md +0 -26
- package/skills/auto/references/tiers.md +0 -100
- package/skills/continuing/SKILL.md +0 -34
- package/skills/continuing/evals/evals.json +0 -91
- package/skills/conventions/references/gates-and-enforcement.md +0 -53
- package/skills/conventions/references/tiers-and-dispatch.md +0 -135
- package/skills/doctor/references/gates-stuck-detection.md +0 -43
- package/skills/intent-brainstorming/SKILL.md +0 -118
- package/skills/intent-brainstorming/evals/evals.json +0 -67
- package/skills/intent-continuing/evals/evals.json +0 -145
- package/skills/intent-discovering/SKILL.md +0 -56
- package/skills/intent-grilling/SKILL.md +0 -108
- package/skills/intent-linking/SKILL.md +0 -128
- package/skills/intent-linking/evals/evals.json +0 -22
- package/skills/intent-linking/references/zettelkasten.md +0 -45
- package/skills/intent-locking/SKILL.md +0 -64
- package/skills/intent-planning/SKILL.md +0 -145
- package/skills/intent-planning/evals/evals.json +0 -81
- package/skills/intent-planning/references/plan-format.md +0 -130
- package/skills/intent-researching/SKILL.md +0 -123
- package/skills/intent-researching/evals/evals.json +0 -22
- package/skills/intent-savepoint/SKILL.md +0 -83
- package/skills/intent-savepoint/references/context-management.md +0 -32
- package/skills/intent-starting/SKILL.md +0 -151
- package/skills/intent-starting/evals/evals.json +0 -117
- package/skills/intent-starting/references/boarding-matrix.md +0 -35
- package/skills/project-continuing/SKILL.md +0 -119
- package/skills/project-continuing/evals/evals.json +0 -100
- package/skills/roadmap-continuing/SKILL.md +0 -89
- package/skills/roadmap-continuing/evals/evals.json +0 -82
- package/skills/skill-creating/SKILL.md +0 -75
- package/skills/skill-creating/evals/evals.json +0 -108
- package/skills/skill-creating/references/agents.md +0 -168
- package/skills/skill-creating/references/defaults-first.md +0 -23
- package/skills/skill-creating/references/evals.md +0 -41
- package/skills/skill-creating/references/hooks.md +0 -251
- package/skills/skill-creating/references/progressive-disclosure.md +0 -176
- package/skills/skill-creating/references/scripts.md +0 -166
- package/skills/skill-creating/references/skills.md +0 -169
- package/skills/skill-creating/scripts/scaffold.rb +0 -313
- package/skills/skill-evaluating/SKILL.md +0 -141
- package/skills/skill-evaluating/assets/eval-template.json +0 -12
- package/skills/skill-evaluating/evals/evals.json +0 -75
- package/skills/skill-evaluating/references/convention-checks.md +0 -76
- package/skills/skill-evaluating/references/eval-methodology.md +0 -154
- package/skills/store-curating/SKILL.md +0 -64
- package/skills/store-curating/evals/evals.json +0 -38
- package/skills/store-indexing/SKILL.md +0 -93
- package/skills/store-indexing/evals/evals.json +0 -22
- package/skills/store-indexing/references/zettelkasten-linking.md +0 -32
- package/skills/store-provisioning/SKILL.md +0 -55
- /package/skills/{project-continuing → intent-continuing}/references/board-fill.md +0 -0
- /package/skills/{roadmap-continuing → intent-continuing}/references/liveness-ranking.md +0 -0
- /package/skills/{intent-brainstorming → intent-speccing}/references/design-principles.md +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# Usage: hook-capture (reads the UserPromptSubmit stdin JSON payload)
|
|
6
|
+
# Intent 298. One UserPromptSubmit process replacing hook-continue,
|
|
7
|
+
# hook-future-intent-check, and hook-auto-arm: it appends a pending line to
|
|
8
|
+
# the session day ledger, detects "continue" and "auto" prompts, and hints at
|
|
9
|
+
# matching Future intents. Every job runs in its own rescue and the hook
|
|
10
|
+
# always exits 0 (spec D2).
|
|
11
|
+
|
|
12
|
+
require "json"
|
|
13
|
+
require "yaml"
|
|
14
|
+
require "open3"
|
|
15
|
+
require "fileutils"
|
|
16
|
+
require_relative "lib/session_ledger"
|
|
17
|
+
require_relative "lib/dashboard_banner"
|
|
18
|
+
|
|
19
|
+
raw = begin
|
|
20
|
+
$stdin.read
|
|
21
|
+
rescue StandardError
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
exit 0 if raw.nil? || raw.strip.empty?
|
|
25
|
+
|
|
26
|
+
payload = begin
|
|
27
|
+
JSON.parse(raw)
|
|
28
|
+
rescue StandardError
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
exit 0 unless payload.is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
session_id = payload["session_id"].to_s
|
|
34
|
+
prompt = payload["prompt"].to_s
|
|
35
|
+
prompt = payload["user_prompt"].to_s if prompt.empty?
|
|
36
|
+
cwd = payload["cwd"].to_s
|
|
37
|
+
cwd = Dir.pwd if cwd.empty?
|
|
38
|
+
|
|
39
|
+
plastic_home = File.expand_path(ENV["PLASTIC_HOME"] || "~/.plastic")
|
|
40
|
+
store = File.join(plastic_home, "store")
|
|
41
|
+
templates = File.expand_path("../templates", __dir__)
|
|
42
|
+
sid = SessionLedger.short_session_id(nil, session_id)
|
|
43
|
+
today = SessionLedger.day_id
|
|
44
|
+
|
|
45
|
+
# --- Extracted matching logic, shared by both the global and project store
|
|
46
|
+
# passes of job (f) below (mirrors hook-future-intent-check verbatim). ---
|
|
47
|
+
def future_intent_matches(store_root, message)
|
|
48
|
+
index_path = File.join(store_root, "INDEX.md")
|
|
49
|
+
return [] unless File.exist?(index_path)
|
|
50
|
+
|
|
51
|
+
lines = File.readlines(index_path)
|
|
52
|
+
future_dirs = []
|
|
53
|
+
section = nil
|
|
54
|
+
|
|
55
|
+
lines.each do |line|
|
|
56
|
+
section = :future if line.start_with?("## Future")
|
|
57
|
+
section = nil if line.start_with?("## ") && !line.start_with?("## Future")
|
|
58
|
+
next unless section == :future && line.strip.start_with?("- [")
|
|
59
|
+
|
|
60
|
+
future_dirs << $1 if line =~ /store\/([\w-]+)\//
|
|
61
|
+
end
|
|
62
|
+
return [] if future_dirs.empty?
|
|
63
|
+
|
|
64
|
+
matches = []
|
|
65
|
+
message_words = message.split(/\W+/).reject { |w| w.length < 4 }
|
|
66
|
+
|
|
67
|
+
future_dirs.each do |dir|
|
|
68
|
+
intent_path = File.join(store_root, "store", dir, "#{dir}.md")
|
|
69
|
+
next unless File.exist?(intent_path)
|
|
70
|
+
|
|
71
|
+
content = File.read(intent_path)
|
|
72
|
+
|
|
73
|
+
tags = []
|
|
74
|
+
tags = $1.split(",").map(&:strip).map(&:downcase) if content =~ /^tags:\s*\[([^\]]+)\]/
|
|
75
|
+
|
|
76
|
+
intent_name = ""
|
|
77
|
+
intent_name = $1.downcase if content =~ /^intent:\s*["']?(.+?)["']?\s*$/
|
|
78
|
+
|
|
79
|
+
keywords = (tags + intent_name.split(/\W+/).reject { |w| w.length < 4 }).map(&:downcase).uniq
|
|
80
|
+
|
|
81
|
+
matched_keywords = keywords.select { |kw| message.include?(kw) }
|
|
82
|
+
name_matches = message_words.select { |w| intent_name.include?(w) }
|
|
83
|
+
matched_keywords = (matched_keywords + name_matches).uniq
|
|
84
|
+
|
|
85
|
+
next if matched_keywords.empty?
|
|
86
|
+
|
|
87
|
+
index_line = lines.find { |l| l.include?(dir) }&.strip || "#{dir} - #{intent_name}"
|
|
88
|
+
matches << { "index_line" => index_line, "keywords" => matched_keywords }
|
|
89
|
+
end
|
|
90
|
+
matches
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The store_root that carries the current project's own store, resolved from
|
|
94
|
+
# cwd through projects.yml, mirroring SessionLedger.project_slug's matching
|
|
95
|
+
# but returning nil (not "global") when nothing matches, since there is then
|
|
96
|
+
# no distinct second store to hint against.
|
|
97
|
+
def resolve_project_store_root(cwd, plastic_home)
|
|
98
|
+
projects_path = File.join(plastic_home, "projects.yml")
|
|
99
|
+
return nil unless File.exist?(projects_path)
|
|
100
|
+
|
|
101
|
+
data = begin
|
|
102
|
+
YAML.safe_load(File.read(projects_path))
|
|
103
|
+
rescue StandardError
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
data = {} unless data.is_a?(Hash)
|
|
107
|
+
projects = data["projects"].is_a?(Hash) ? data["projects"] : {}
|
|
108
|
+
expanded_cwd = File.expand_path(cwd)
|
|
109
|
+
|
|
110
|
+
matches = projects.filter_map do |slug, info|
|
|
111
|
+
next unless info.is_a?(Hash) && slug.is_a?(String)
|
|
112
|
+
|
|
113
|
+
path = info["path"]
|
|
114
|
+
next unless path
|
|
115
|
+
|
|
116
|
+
root = File.expand_path(path)
|
|
117
|
+
next unless expanded_cwd == root || expanded_cwd.start_with?("#{root}#{File::SEPARATOR}")
|
|
118
|
+
|
|
119
|
+
[root.length, slug]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
best = matches.max_by { |(length, _slug)| length }
|
|
123
|
+
best ? File.join(plastic_home, "projects", best[1]) : nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def truncate(text, max)
|
|
127
|
+
return text if text.length <= max
|
|
128
|
+
|
|
129
|
+
"#{text[0, max - 3]}..."
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# --- (a) tmp root + heartbeat, guarded on the pointer (spec D9) ------------
|
|
133
|
+
# Session start writes the `current` pointer; a capture with no pointer yet
|
|
134
|
+
# means session start never ran for this session at all (a `-p` print
|
|
135
|
+
# session, a resumed background job, an unregistered SessionStart hook), so
|
|
136
|
+
# neither the per-session dir nor its heartbeat is created. UserPromptSubmit
|
|
137
|
+
# fires before PostToolUse, so capture is the FIRST creator of every orphan
|
|
138
|
+
# `.tmp/<sid>/` this guard prevents; hook-record carries the same guard for
|
|
139
|
+
# its own (later) heartbeat write.
|
|
140
|
+
begin
|
|
141
|
+
if File.exist?(SessionLedger.pointer_path(store, sid))
|
|
142
|
+
SessionLedger.ensure_tmp_root(store)
|
|
143
|
+
FileUtils.mkdir_p(SessionLedger.session_tmp_dir(store, sid))
|
|
144
|
+
File.write(SessionLedger.heartbeat_path(store, sid), "#{Time.now.utc.iso8601}\n")
|
|
145
|
+
end
|
|
146
|
+
rescue StandardError
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# --- (b) read the pointer -----------------------------------------------
|
|
151
|
+
skip_day_ledger = false
|
|
152
|
+
begin
|
|
153
|
+
pointer = SessionLedger.pointer_path(store, sid)
|
|
154
|
+
if File.exist?(pointer)
|
|
155
|
+
current = File.read(pointer).strip
|
|
156
|
+
skip_day_ledger = !current.empty? && current != today
|
|
157
|
+
end
|
|
158
|
+
rescue StandardError
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# --- (c) pending checklist line -------------------------------------------
|
|
163
|
+
# Gated on SessionLedger.capture_worthy?(prompt) (spec D2), a pure predicate
|
|
164
|
+
# over the RAW prompt so it sees the true first character and multi-line
|
|
165
|
+
# shape; the line text itself is still the sanitized, truncated copy.
|
|
166
|
+
unless skip_day_ledger
|
|
167
|
+
begin
|
|
168
|
+
if SessionLedger.capture_worthy?(prompt)
|
|
169
|
+
project = SessionLedger.project_slug(cwd, plastic_home: plastic_home)
|
|
170
|
+
clean = truncate(SessionLedger.sanitize_summary(prompt), 120)
|
|
171
|
+
SessionLedger.open_day(store: store, day: today, templates: templates, author: sid)
|
|
172
|
+
line = SessionLedger.checklist_line(:pending, sid, project, clean)
|
|
173
|
+
SessionLedger.append_line(SessionLedger.checklist_path(store, today), line,
|
|
174
|
+
header: SessionLedger.checklist_header(today))
|
|
175
|
+
end
|
|
176
|
+
rescue StandardError
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
context_parts = []
|
|
182
|
+
system_message = nil
|
|
183
|
+
|
|
184
|
+
# --- (d) "continue" cockpit -------------------------------------------------
|
|
185
|
+
begin
|
|
186
|
+
if prompt.match?(/\bcontinue\b/i)
|
|
187
|
+
dashboard = File.expand_path("dashboard.rb", __dir__)
|
|
188
|
+
if File.exist?(dashboard)
|
|
189
|
+
cockpit, _err, status = Open3.capture3({ "RUBYOPT" => nil }, "ruby", dashboard, "continue")
|
|
190
|
+
if status.success? && !cockpit.strip.empty?
|
|
191
|
+
context_parts << (cockpit.rstrip +
|
|
192
|
+
"\n\nFollow the plastic-intent-continuing skill workflow to resume: " \
|
|
193
|
+
"if active intents exist, read their state and resume; otherwise surface the " \
|
|
194
|
+
"most-valuable next work and any stale or triage intents from the board above.")
|
|
195
|
+
|
|
196
|
+
begin
|
|
197
|
+
data_json, _data_err, data_status = Open3.capture3({ "RUBYOPT" => nil }, "ruby", dashboard,
|
|
198
|
+
"continue", "--data")
|
|
199
|
+
if data_status.success?
|
|
200
|
+
banner = DashboardBanner.render(JSON.parse(data_json))
|
|
201
|
+
system_message = banner if banner
|
|
202
|
+
end
|
|
203
|
+
rescue StandardError
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
rescue StandardError
|
|
210
|
+
nil
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# --- (e) auto-trigger steering (hook-auto-arm job 1 only) ------------------
|
|
214
|
+
begin
|
|
215
|
+
message_lower = prompt.downcase
|
|
216
|
+
auto_triggers = ["take it from here", "deliver this", "/plastic-auto", "auto mode"].freeze
|
|
217
|
+
triggered = auto_triggers.any? { |p| message_lower.include?(p) } || message_lower.match?(/\bauto\b/)
|
|
218
|
+
if triggered
|
|
219
|
+
context_parts << "Auto-mode request detected. Invoke the plastic-auto skill to deliver the " \
|
|
220
|
+
"active intent autonomously - it takes the intent under a lock and a worktree " \
|
|
221
|
+
"and points this session at it."
|
|
222
|
+
end
|
|
223
|
+
rescue StandardError
|
|
224
|
+
nil
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# --- (f) Future-intent hint, global store then the project's own store -----
|
|
228
|
+
begin
|
|
229
|
+
message = prompt.to_s.downcase
|
|
230
|
+
if message.strip.length >= 10 && message.strip != "continue"
|
|
231
|
+
matches = future_intent_matches(plastic_home, message)
|
|
232
|
+
project_root = resolve_project_store_root(cwd, plastic_home)
|
|
233
|
+
matches += future_intent_matches(project_root, message) if project_root
|
|
234
|
+
|
|
235
|
+
if matches.any?
|
|
236
|
+
parts = ["PLASTIC - Future intents related to this message:\n"]
|
|
237
|
+
matches.each { |m| parts << "#{m["index_line"]} (matched: #{m["keywords"].join(", ")})" }
|
|
238
|
+
parts << "\nConsider asking the user if they want to activate any of these, or note the connection."
|
|
239
|
+
context_parts << parts.join("\n")
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
rescue StandardError
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
exit 0 if context_parts.empty?
|
|
247
|
+
|
|
248
|
+
payload_out = {
|
|
249
|
+
"hookSpecificOutput" => {
|
|
250
|
+
"hookEventName" => "UserPromptSubmit",
|
|
251
|
+
"additionalContext" => context_parts.join("\n\n")
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
payload_out["systemMessage"] = system_message if system_message
|
|
255
|
+
puts JSON.generate(payload_out)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# hook-close (intent 301): the SessionEnd hook. Reads the harness payload from
|
|
6
|
+
# stdin (session_id, cwd, reason), takes the Plastic home from argv, and runs
|
|
7
|
+
# SessionClose with the detached filer spawner. Always exits 0, prints nothing.
|
|
8
|
+
#
|
|
9
|
+
# Usage: hook-close <plastic_home> (stdin: the hook JSON)
|
|
10
|
+
|
|
11
|
+
require "json"
|
|
12
|
+
require_relative "lib/session_ledger"
|
|
13
|
+
require_relative "lib/session_close"
|
|
14
|
+
|
|
15
|
+
plastic_home = ARGV[0]
|
|
16
|
+
exit 0 if plastic_home.nil? || plastic_home.empty?
|
|
17
|
+
|
|
18
|
+
payload =
|
|
19
|
+
begin
|
|
20
|
+
raw = $stdin.tty? ? "" : $stdin.read
|
|
21
|
+
raw.to_s.strip.empty? ? {} : JSON.parse(raw)
|
|
22
|
+
rescue JSON::ParserError
|
|
23
|
+
{}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
store = File.join(File.expand_path(plastic_home), "store")
|
|
27
|
+
filer = File.expand_path("file-session-intent", __dir__)
|
|
28
|
+
templates = File.expand_path("../templates", __dir__)
|
|
29
|
+
begin
|
|
30
|
+
SessionClose.run(payload: payload, store: store, today: SessionLedger.day_id,
|
|
31
|
+
spawner: SessionClose.default_spawner(filer),
|
|
32
|
+
handoff: SessionClose.default_handoff(templates))
|
|
33
|
+
rescue StandardError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
exit 0
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# hook-message-display (intent 316a, O4): the MessageDisplay hook CLI. Reads
|
|
6
|
+
# the harness's per-chunk JSON payload from stdin, hands it to MessageDisplay
|
|
7
|
+
# (the pure handler class), and prints the hookSpecificOutput envelope when a
|
|
8
|
+
# String comes back. Always exits 0, whatever happens — a raised exception
|
|
9
|
+
# here must never surface as a non-zero exit or stray stderr on an ordinary
|
|
10
|
+
# chunk of an ordinary message (matrix 31).
|
|
11
|
+
#
|
|
12
|
+
# This script is the one place in the O4 stack allowed to read ENV, the
|
|
13
|
+
# clock, or Dir.tmpdir: MessageDisplay itself takes tmp_root/plastic_home/
|
|
14
|
+
# color/now as constructor arguments and touches none of them directly.
|
|
15
|
+
#
|
|
16
|
+
# Claude adapter: Claude Code only; the core is harness-agnostic.
|
|
17
|
+
|
|
18
|
+
# 317a (B1): the requires sit inside their own rescue - a missing or broken
|
|
19
|
+
# lib file must never surface as a non-zero exit or stderr on a chunk of an
|
|
20
|
+
# ordinary message (LoadError is a ScriptError, outside StandardError).
|
|
21
|
+
begin
|
|
22
|
+
require "json"
|
|
23
|
+
require "time"
|
|
24
|
+
require "tmpdir"
|
|
25
|
+
require "yaml"
|
|
26
|
+
require_relative "lib/message_display"
|
|
27
|
+
rescue ScriptError, StandardError
|
|
28
|
+
exit 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def color_enabled?(plastic_home)
|
|
32
|
+
return false unless ENV["NO_COLOR"].to_s.empty?
|
|
33
|
+
|
|
34
|
+
cfg_path = File.join(plastic_home, "config.yml")
|
|
35
|
+
return true unless File.exist?(cfg_path)
|
|
36
|
+
|
|
37
|
+
cfg = YAML.safe_load(File.read(cfg_path))
|
|
38
|
+
display = cfg.is_a?(Hash) ? cfg["display"] : nil
|
|
39
|
+
return true unless display.is_a?(Hash)
|
|
40
|
+
|
|
41
|
+
display.fetch("ansi_screen", true) != false
|
|
42
|
+
rescue StandardError
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
begin
|
|
47
|
+
raw = $stdin.tty? ? "" : $stdin.read
|
|
48
|
+
payload = raw.to_s.strip.empty? ? nil : JSON.parse(raw)
|
|
49
|
+
|
|
50
|
+
if payload.is_a?(Hash)
|
|
51
|
+
tmp_root = ENV["PLASTIC_TMP"].to_s.empty? ? Dir.tmpdir : ENV["PLASTIC_TMP"]
|
|
52
|
+
plastic_home = File.expand_path(ENV["PLASTIC_HOME"] || "~/.plastic")
|
|
53
|
+
|
|
54
|
+
handler = MessageDisplay.new(
|
|
55
|
+
tmp_root: tmp_root,
|
|
56
|
+
plastic_home: plastic_home,
|
|
57
|
+
color: color_enabled?(plastic_home),
|
|
58
|
+
now: Time.now,
|
|
59
|
+
)
|
|
60
|
+
result = handler.handle(payload)
|
|
61
|
+
|
|
62
|
+
if result.is_a?(String)
|
|
63
|
+
puts JSON.generate(
|
|
64
|
+
"hookSpecificOutput" => {
|
|
65
|
+
"hookEventName" => "MessageDisplay",
|
|
66
|
+
"displayContent" => result,
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
exit 0
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# Usage: hook-record (reads the PostToolUse stdin JSON payload)
|
|
6
|
+
# Intent 298. Replaces hook-gate-check: never blocks, keeps the decoupled
|
|
7
|
+
# intent-dir savepoint append byte for byte, refreshes the delivery-lock lease
|
|
8
|
+
# for the owning session (intent 302, the lock gate's old allow-path job),
|
|
9
|
+
# promotes the session's newest pending day-ledger line when a project file
|
|
10
|
+
# lands, and calls the session-commit seam when that script exists. Always
|
|
11
|
+
# exits 0 (spec D3).
|
|
12
|
+
|
|
13
|
+
require "json"
|
|
14
|
+
require "timeout"
|
|
15
|
+
require "fileutils"
|
|
16
|
+
require_relative "lib/savepoint"
|
|
17
|
+
require_relative "lib/lock"
|
|
18
|
+
require_relative "lib/session_ledger"
|
|
19
|
+
|
|
20
|
+
raw = begin
|
|
21
|
+
$stdin.read
|
|
22
|
+
rescue StandardError
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
exit 0 if raw.nil? || raw.strip.empty?
|
|
26
|
+
|
|
27
|
+
payload = begin
|
|
28
|
+
JSON.parse(raw)
|
|
29
|
+
rescue StandardError
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
exit 0 unless payload.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
file_path = payload.dig("tool_input", "file_path") || payload.dig("tool_params", "file_path")
|
|
35
|
+
exit 0 if file_path.to_s.empty?
|
|
36
|
+
|
|
37
|
+
session_id = payload["session_id"].to_s
|
|
38
|
+
cwd = payload["cwd"].to_s
|
|
39
|
+
cwd = Dir.pwd if cwd.empty?
|
|
40
|
+
|
|
41
|
+
file_path_abs = File.expand_path(file_path)
|
|
42
|
+
|
|
43
|
+
# --- (a) decoupled savepoint ledger, byte for byte -------------------------
|
|
44
|
+
intent_dir_abs = nil
|
|
45
|
+
begin
|
|
46
|
+
intent_dir_abs = Savepoint.intent_dir_for(file_path_abs)
|
|
47
|
+
if intent_dir_abs
|
|
48
|
+
begin
|
|
49
|
+
Savepoint.append_savepoint(intent_dir_abs, file_path_abs)
|
|
50
|
+
if File.basename(file_path_abs) == "checklist.md" && Savepoint.stage_file_present?(file_path_abs)
|
|
51
|
+
Savepoint.append_exec_started(intent_dir_abs)
|
|
52
|
+
end
|
|
53
|
+
rescue StandardError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
# The lease heartbeat (intent 302, spec D10): the lock gate's allow path used
|
|
57
|
+
# to refresh delivery.lock on every edit; record does it now. Lock.heartbeat
|
|
58
|
+
# refuses a session that does not hold the lock, so a stranger's write never
|
|
59
|
+
# refreshes the owner's lease.
|
|
60
|
+
begin
|
|
61
|
+
Lock.heartbeat(intent_dir_abs, session: session_id) unless session_id.empty?
|
|
62
|
+
rescue StandardError
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
rescue StandardError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
plastic_home = File.expand_path(ENV["PLASTIC_HOME"] || "~/.plastic")
|
|
71
|
+
|
|
72
|
+
# --- (b) stop the day-ledger path when inside ~/.plastic or an intent dir --
|
|
73
|
+
inside_plastic = file_path_abs == plastic_home || file_path_abs.start_with?("#{plastic_home}/")
|
|
74
|
+
skip_day_ledger = inside_plastic || !intent_dir_abs.nil?
|
|
75
|
+
|
|
76
|
+
# --- (c) promote the newest pending line, Item savepoint, seam -------------
|
|
77
|
+
unless skip_day_ledger
|
|
78
|
+
begin
|
|
79
|
+
store = File.join(plastic_home, "store")
|
|
80
|
+
sid = SessionLedger.short_session_id(nil, session_id)
|
|
81
|
+
today = SessionLedger.day_id
|
|
82
|
+
|
|
83
|
+
proceed = true
|
|
84
|
+
pointer = SessionLedger.pointer_path(store, sid)
|
|
85
|
+
if File.exist?(pointer)
|
|
86
|
+
current = File.read(pointer).strip
|
|
87
|
+
proceed = current.empty? || current == today
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if proceed
|
|
91
|
+
checklist = SessionLedger.checklist_path(store, today)
|
|
92
|
+
target_summary = begin
|
|
93
|
+
SessionLedger.set_state(checklist, from: :pending, to: :open, session: sid)
|
|
94
|
+
rescue SessionLedger::LockUnavailableError
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if target_summary
|
|
99
|
+
project = SessionLedger.project_slug(cwd, plastic_home: plastic_home)
|
|
100
|
+
line = SessionLedger.savepoint_line("Item", sid, project, target_summary, now: Time.now)
|
|
101
|
+
SessionLedger.append_line(SessionLedger.savepoint_path(store, today), line, header: nil)
|
|
102
|
+
|
|
103
|
+
seam = File.expand_path("session-commit", __dir__)
|
|
104
|
+
if File.executable?(seam)
|
|
105
|
+
begin
|
|
106
|
+
# A bare Timeout.timeout around Open3.capture3 does not reliably bound a
|
|
107
|
+
# genuinely hanging child: capture3's internal wait still blocks on
|
|
108
|
+
# Process.waitpid for the child regardless of the raised Timeout::Error.
|
|
109
|
+
# Spawn directly and kill on timeout instead, so the 10s bound is real.
|
|
110
|
+
pid = Process.spawn({ "RUBYOPT" => nil }, seam, "--cwd", cwd, "--summary", target_summary,
|
|
111
|
+
"--session", sid, "--day", today,
|
|
112
|
+
[:out, :err] => File::NULL)
|
|
113
|
+
begin
|
|
114
|
+
Timeout.timeout(10) { Process.wait(pid) }
|
|
115
|
+
rescue Timeout::Error
|
|
116
|
+
begin
|
|
117
|
+
Process.kill("KILL", pid)
|
|
118
|
+
rescue StandardError
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
begin
|
|
122
|
+
Process.wait(pid)
|
|
123
|
+
rescue StandardError
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
rescue StandardError
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# --- (d) heartbeat, guarded on the pointer (spec D9) -----------------------
|
|
139
|
+
# Session start writes the `current` pointer; with no pointer at all, session
|
|
140
|
+
# start never ran for this session (a `-p` print session, a resumed
|
|
141
|
+
# background job, an unregistered SessionStart hook), so neither the
|
|
142
|
+
# per-session dir nor its heartbeat is created here -- guarding only the
|
|
143
|
+
# write would still leave an empty directory that heartbeat_age's mtime
|
|
144
|
+
# fallback reports as an orphan just the same.
|
|
145
|
+
begin
|
|
146
|
+
store ||= File.join(plastic_home, "store")
|
|
147
|
+
sid ||= SessionLedger.short_session_id(nil, session_id)
|
|
148
|
+
if File.exist?(SessionLedger.pointer_path(store, sid))
|
|
149
|
+
SessionLedger.ensure_tmp_root(store)
|
|
150
|
+
FileUtils.mkdir_p(SessionLedger.session_tmp_dir(store, sid))
|
|
151
|
+
File.write(SessionLedger.heartbeat_path(store, sid), "#{Time.now.utc.iso8601}\n")
|
|
152
|
+
end
|
|
153
|
+
rescue StandardError
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
exit 0
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# hook-savepoint (intent 311): the PreCompact hook body. Reads the harness
|
|
6
|
+
# payload from stdin (session_id, cwd), takes the Plastic home from argv,
|
|
7
|
+
# writes this session's hand-off for the pointer's day, and prints one
|
|
8
|
+
# message: the fixed text, plus the written file's path when a session
|
|
9
|
+
# resolved (both harnesses relay the same launcher and the Codex pin feeds
|
|
10
|
+
# both sides the same payload, so the two stay equal). Always exits 0,
|
|
11
|
+
# always prints the message.
|
|
12
|
+
#
|
|
13
|
+
# Usage: hook-savepoint <plastic_home> (stdin: the hook JSON)
|
|
14
|
+
|
|
15
|
+
require "json"
|
|
16
|
+
require_relative "lib/session_ledger"
|
|
17
|
+
require_relative "lib/handoff"
|
|
18
|
+
|
|
19
|
+
MESSAGE = "PLASTIC SAVEPOINT - context is being compacted. The hand-off for this " \
|
|
20
|
+
"session is written in today's day ledger " \
|
|
21
|
+
"(store/.sessions/<day>/handoff--<session>.md). " \
|
|
22
|
+
"After compaction say continue to resume from it."
|
|
23
|
+
|
|
24
|
+
written = nil
|
|
25
|
+
begin
|
|
26
|
+
plastic_home = ARGV[0].to_s
|
|
27
|
+
raw = $stdin.tty? ? "" : $stdin.read.to_s
|
|
28
|
+
payload = raw.strip.empty? ? {} : JSON.parse(raw)
|
|
29
|
+
payload = {} unless payload.is_a?(Hash)
|
|
30
|
+
session_id = payload["session_id"].to_s
|
|
31
|
+
|
|
32
|
+
unless plastic_home.empty? || session_id.strip.empty?
|
|
33
|
+
store = File.join(File.expand_path(plastic_home), "store")
|
|
34
|
+
session = SessionLedger.short_session_id(session_id, nil)
|
|
35
|
+
day = Handoff.day_for(store, session, today: SessionLedger.day_id)
|
|
36
|
+
written = Handoff.write(store: store, day: day, session: session, trigger: "precompact",
|
|
37
|
+
templates: File.expand_path("../templates", __dir__))
|
|
38
|
+
end
|
|
39
|
+
rescue StandardError, JSON::ParserError
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
message = written ? "#{MESSAGE} This session's file: #{written}" : MESSAGE
|
|
44
|
+
puts JSON.generate("systemMessage" => message)
|
|
45
|
+
exit 0
|