@zalom/plastic 1.0.3 → 1.1.1
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-reference.md +11 -11
- package/PLASTIC.md +11 -9
- package/README.md +48 -1
- package/agents/plastic-enforcer.md +8 -5
- package/agents/plastic-future-intent-researcher.md +1 -1
- package/agents/plastic-intent-curator.md +9 -10
- package/agents/plastic-spec-specialist.md +9 -6
- package/bin/plastic.js +2 -2
- package/hooks/check-update +1 -1
- package/hooks/continue +1 -1
- package/package.json +1 -1
- package/scripts/dashboard.rb +29 -24
- package/scripts/doctor.rb +188 -9
- package/scripts/end-intent +330 -0
- package/scripts/hook-code-gate +1 -1
- package/scripts/hook-continue +2 -2
- package/scripts/hook-create-gate +2 -2
- package/scripts/hook-session-start +1 -1
- package/scripts/install.rb +2 -1
- package/scripts/lib/bridge.rb +116 -3
- package/scripts/lib/dashboard_banner.rb +8 -9
- package/scripts/lib/installer_core.rb +17 -8
- package/scripts/lib/legacy_bookend_amnesty.rb +35 -0
- package/scripts/lib/release_guard.rb +62 -0
- package/scripts/lib/roadmap_queue.rb +285 -0
- package/scripts/lib/roadmap_savepoint.rb +213 -0
- package/scripts/lib/store_provisioning.rb +1 -1
- package/scripts/lib/worktree.rb +21 -0
- package/scripts/new-intent +2 -1
- package/scripts/read-config +3 -3
- package/scripts/roadmap-next +44 -0
- package/scripts/roadmap-savepoint +64 -0
- package/scripts/{versions.rb → rollback.rb} +33 -41
- package/scripts/update.rb +1 -1
- package/skills/_active-intent-gate.md +1 -1
- package/skills/_decision-tables.md +50 -0
- package/skills/auto/SKILL.md +69 -68
- package/skills/auto/evals/evals.json +4 -4
- package/skills/auto/references/agent-architecture.md +2 -2
- package/skills/continuing/SKILL.md +23 -130
- package/skills/continuing/evals/evals.json +26 -80
- package/skills/dashboard/SKILL.md +18 -15
- package/skills/dashboard/references/classification.md +3 -3
- package/skills/dashboard/templates/dashboard-global.md +8 -23
- package/skills/dashboard/templates/dashboard-project.md +7 -26
- package/skills/doctor/SKILL.md +3 -3
- package/skills/install/SKILL.md +10 -10
- package/skills/{brainstorming → intent-brainstorming}/SKILL.md +43 -45
- package/skills/intent-brainstorming/evals/evals.json +67 -0
- package/skills/{brainstorming → intent-brainstorming}/references/design-principles.md +12 -17
- package/skills/intent-continuing/SKILL.md +99 -0
- package/skills/intent-continuing/evals/evals.json +145 -0
- package/skills/intent-continuing/references/context-management.md +28 -0
- package/skills/{creating-intent → intent-creating}/SKILL.md +3 -3
- package/skills/{creating-intent → intent-creating}/evals/evals.json +1 -1
- package/skills/{intent-discovery → intent-discovering}/SKILL.md +1 -1
- package/skills/intent-ending/SKILL.md +151 -0
- package/skills/intent-ending/evals/evals.json +74 -0
- package/skills/{executing-plan → intent-executing}/SKILL.md +75 -15
- package/skills/intent-executing/evals/evals.json +66 -0
- package/skills/{brainstorming-grill-me → intent-grilling}/SKILL.md +4 -4
- package/skills/{linking-intents → intent-linking}/SKILL.md +1 -1
- package/skills/{linking-intents → intent-linking}/evals/evals.json +1 -1
- package/skills/{lock → intent-locking}/SKILL.md +1 -1
- package/skills/{writing-plans → intent-planning}/SKILL.md +48 -20
- package/skills/intent-planning/evals/evals.json +66 -0
- package/skills/{writing-plans → intent-planning}/references/plan-format.md +31 -7
- package/skills/{research → intent-researching}/SKILL.md +2 -2
- package/skills/{research → intent-researching}/evals/evals.json +1 -1
- package/skills/{savepoint → intent-savepoint}/SKILL.md +13 -1
- package/skills/intent-speccing/SKILL.md +69 -0
- package/skills/intent-speccing/evals/evals.json +126 -0
- package/skills/intent-speccing/references/per-section-fill-rules.md +92 -0
- package/skills/intent-speccing/references/self-verify-checklist.md +38 -0
- package/skills/intent-starting/SKILL.md +2 -2
- package/skills/intent-starting/evals/evals.json +2 -2
- package/skills/project-continuing/SKILL.md +104 -0
- package/skills/project-continuing/evals/evals.json +100 -0
- package/skills/project-continuing/references/board-fill.md +33 -0
- package/skills/{creating-project → project-creating}/SKILL.md +2 -2
- package/skills/releasing/SKILL.md +66 -14
- package/skills/releasing/references/release-lines.md +105 -0
- package/skills/roadmap/SKILL.md +7 -1
- package/skills/roadmap/references/file-format.md +30 -1
- package/skills/roadmap/references/operations.md +26 -6
- package/skills/roadmap-continuing/SKILL.md +85 -0
- package/skills/roadmap-continuing/evals/evals.json +82 -0
- package/skills/roadmap-continuing/references/liveness-ranking.md +56 -0
- package/skills/{versions → rollback}/SKILL.md +30 -20
- package/skills/{creating-skills → skill-creating}/SKILL.md +2 -2
- package/skills/{creating-skills → skill-creating}/evals/evals.json +5 -5
- package/skills/{creating-skills → skill-creating}/references/progressive-disclosure.md +2 -2
- package/skills/{evaluating-skills → skill-evaluating}/SKILL.md +1 -1
- package/skills/{evaluating-skills → skill-evaluating}/evals/evals.json +3 -3
- package/skills/{intent-curator → store-curating}/SKILL.md +6 -7
- package/skills/{intent-curator → store-curating}/evals/evals.json +1 -1
- package/skills/{managing-index → store-indexing}/SKILL.md +6 -7
- package/skills/{managing-index → store-indexing}/evals/evals.json +1 -1
- package/skills/{add-project-store → store-provisioning}/SKILL.md +2 -2
- package/skills/tutorial/SKILL.md +65 -0
- package/skills/tutorial/evals/evals.json +186 -0
- package/skills/tutorial/references/track-1-guided.md +123 -0
- package/skills/tutorial/references/track-2-auto.md +95 -0
- package/skills/tutorial/references/track-3-projects-and-roadmaps.md +110 -0
- package/skills/uninstall/SKILL.md +2 -2
- package/skills/update/SKILL.md +3 -3
- package/templates/agents.md +7 -7
- package/templates/config.yml +2 -1
- package/templates/index.md +4 -1
- package/skills/brainstorming/evals/evals.json +0 -22
- package/skills/savepoint/references/context-management.md +0 -32
- /package/skills/{creating-intent → intent-creating}/references/lifecycle.md +0 -0
- /package/skills/{creating-intent → intent-creating}/references/wikilinks.md +0 -0
- /package/skills/{executing-plan → intent-executing}/code-quality-reviewer-prompt.md +0 -0
- /package/skills/{executing-plan → intent-executing}/implementer-prompt.md +0 -0
- /package/skills/{executing-plan → intent-executing}/spec-reviewer-prompt.md +0 -0
- /package/skills/{linking-intents → intent-linking}/references/zettelkasten.md +0 -0
- /package/skills/{continuing → intent-savepoint}/references/context-management.md +0 -0
- /package/skills/{creating-project → project-creating}/references/hubs-projects.md +0 -0
- /package/skills/{creating-project → project-creating}/references/project-scaffolding.md +0 -0
- /package/skills/{creating-skills → skill-creating}/references/agents.md +0 -0
- /package/skills/{creating-skills → skill-creating}/references/evals.md +0 -0
- /package/skills/{creating-skills → skill-creating}/references/hooks.md +0 -0
- /package/skills/{creating-skills → skill-creating}/references/scripts.md +0 -0
- /package/skills/{creating-skills → skill-creating}/references/skills.md +0 -0
- /package/skills/{creating-skills → skill-creating}/scripts/scaffold.rb +0 -0
- /package/skills/{evaluating-skills → skill-evaluating}/assets/eval-template.json +0 -0
- /package/skills/{evaluating-skills → skill-evaluating}/references/convention-checks.md +0 -0
- /package/skills/{evaluating-skills → skill-evaluating}/references/eval-methodology.md +0 -0
- /package/skills/{managing-index → store-indexing}/references/zettelkasten-linking.md +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "time"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
|
|
7
|
+
# RoadmapSavepoint - the roadmap's machine counterpart to its human `## Log` (intent 134).
|
|
8
|
+
#
|
|
9
|
+
# Mirrors scripts/lib/qmd_sync.rb's class shape and the cycle-step savepoint ledger in
|
|
10
|
+
# scripts/lib/bridge.rb (append semantics: idempotent `(event, detail)` pair dedup, one
|
|
11
|
+
# deterministic append primitive). Constructor-DI, hermetic: no eval, no ENV or global config
|
|
12
|
+
# seam, clock injected through `now:`. Two operations:
|
|
13
|
+
#
|
|
14
|
+
# append(roadmap_path, event, detail, now:) - one deterministic, idempotent append
|
|
15
|
+
# rebuild(roadmap_path) - reconstruct the ledger from the roadmap's
|
|
16
|
+
# `## Log` (and, for delivered wave entries with
|
|
17
|
+
# no matching Log line, from INDEX `## Completed`)
|
|
18
|
+
#
|
|
19
|
+
# The ledger is sugar on top of the roadmap file: derived, rebuildable, never a status source.
|
|
20
|
+
# INDEX.md stays the single status writer.
|
|
21
|
+
module RoadmapSavepoint
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
EVENTS = %w[created dispatched parked merged release handoff closed added reordered wave].freeze
|
|
25
|
+
|
|
26
|
+
# Keyword -> event classification for `rebuild`, checked top to bottom, first match wins.
|
|
27
|
+
# Kept small and deterministic (action 1). Order matters: more specific/rarer words are
|
|
28
|
+
# checked before the broader "wave" fallback so an incidental "wave" mention in an otherwise
|
|
29
|
+
# classifiable line never shadows its real event.
|
|
30
|
+
KEYWORD_TABLE = [
|
|
31
|
+
[/\bclosed\b/i, "closed"],
|
|
32
|
+
[/\bhanded off\b|\bhandoff\b/i, "handoff"],
|
|
33
|
+
[/\breleased?\b|\bcut\b|\btagged\b/i, "release"],
|
|
34
|
+
[/\bparked\b|\bblocked\b|\bholds?\b/i, "parked"],
|
|
35
|
+
[/\bdelivered\b|\bmerged\b|\bshipped\b/i, "merged"],
|
|
36
|
+
[/\bdelivering\b|\bdispatch(?:ed)?\b/i, "dispatched"],
|
|
37
|
+
[/\breordered\b/i, "reordered"],
|
|
38
|
+
[/\badded\b|\badds\b/i, "added"],
|
|
39
|
+
[/\bcreated\b/i, "created"],
|
|
40
|
+
[/\bwave\b/i, "wave"],
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
# --- append -----------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
# The name-paired sibling ledger for a roadmap path: roadmaps/<slug>.md becomes
|
|
46
|
+
# roadmaps/<slug>.savepoint.md. Resolves correctly for a live roadmap and for one already
|
|
47
|
+
# moved under roadmaps/archived/, because both are plain path substitutions.
|
|
48
|
+
def ledger_path_for(roadmap_path)
|
|
49
|
+
roadmap_path.sub(/\.md\z/, ".savepoint.md")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# `(event, detail)` pairs already recorded in a ledger file, the dedup key (D2): two
|
|
53
|
+
# `dispatched` events with different details are distinct, so the event word alone would be
|
|
54
|
+
# too coarse a key.
|
|
55
|
+
def recorded_pairs(ledger_path)
|
|
56
|
+
return [] unless File.exist?(ledger_path)
|
|
57
|
+
File.read(ledger_path).each_line.filter_map { |line| parse_pair(line) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def parse_pair(line)
|
|
61
|
+
parts = line.strip.split(/\s{2,}/)
|
|
62
|
+
parts.length >= 3 ? [parts[1], parts[2]] : nil
|
|
63
|
+
end
|
|
64
|
+
private_class_method :parse_pair
|
|
65
|
+
|
|
66
|
+
# Append one ledger line "<iso8601> <event> <detail>" unless `(event, detail)` is already
|
|
67
|
+
# recorded (no-op). Creates the paired ledger file (and its directory) lazily. Raises
|
|
68
|
+
# ArgumentError when `event` is outside the controlled vocabulary. Returns true when a line
|
|
69
|
+
# was written, false on a dedup no-op.
|
|
70
|
+
def append(roadmap_path, event, detail, now: Time.now)
|
|
71
|
+
unless EVENTS.include?(event)
|
|
72
|
+
raise ArgumentError, "event must be one of #{EVENTS.join(', ')}, got #{event.inspect}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
ledger_path = ledger_path_for(roadmap_path)
|
|
76
|
+
return false if recorded_pairs(ledger_path).include?([event, detail])
|
|
77
|
+
|
|
78
|
+
FileUtils.mkdir_p(File.dirname(ledger_path))
|
|
79
|
+
File.open(ledger_path, "a") { |io| io.write(format_line(now, event, detail)) }
|
|
80
|
+
true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def format_line(time, event, detail)
|
|
84
|
+
"#{time.utc.iso8601} #{event} #{detail.to_s.strip}\n"
|
|
85
|
+
end
|
|
86
|
+
private_class_method :format_line
|
|
87
|
+
|
|
88
|
+
# --- rebuild ------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
# Reconstruct the paired ledger deterministically from the roadmap file's `## Log` (never the
|
|
91
|
+
# roadmap `.md`, which is read-only here), cross-checked against `## Waves` and the tier's
|
|
92
|
+
# INDEX so every `delivered` wave entry has a `merged` line. Every timestamp comes from an
|
|
93
|
+
# on-disk source (the Log, or INDEX `## Completed`); an entry with no recoverable timestamp is
|
|
94
|
+
# not emitted (D4, never invented). Overwrites the ledger (the one operation allowed to rewrite
|
|
95
|
+
# it, matching `Bridge.rebuild_savepoint`). Returns the number of lines written.
|
|
96
|
+
def rebuild(roadmap_path)
|
|
97
|
+
text = File.read(roadmap_path)
|
|
98
|
+
log_lines = classify_log(section_body(text, "Log"))
|
|
99
|
+
delivered_ids = delivered_wave_ids(section_body(text, "Waves"))
|
|
100
|
+
backfilled = backfill_merged_lines(log_lines, delivered_ids, roadmap_path)
|
|
101
|
+
|
|
102
|
+
lines = dedup_pairs(log_lines + backfilled)
|
|
103
|
+
File.write(ledger_path_for(roadmap_path), lines.map { |t, e, d| format_line(t, e, d) }.join)
|
|
104
|
+
lines.length
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Every `- YYYY-MM-DD HH:MM UTC ...` line in the `## Log` body, classified into
|
|
108
|
+
# [time, event, detail]. A line with no keyword match is dropped (no event to record); a
|
|
109
|
+
# continuation line (no date prefix) never matches the header pattern, so it is inherently
|
|
110
|
+
# ignored for classification, per action 1.
|
|
111
|
+
def classify_log(log_body)
|
|
112
|
+
log_body.each_line.filter_map { |line| classify_log_line(line) }
|
|
113
|
+
end
|
|
114
|
+
private_class_method :classify_log
|
|
115
|
+
|
|
116
|
+
LOG_LINE = /\A-\s+(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\s+UTC\s+(.*)\z/.freeze
|
|
117
|
+
|
|
118
|
+
def classify_log_line(line)
|
|
119
|
+
m = line.strip.match(LOG_LINE)
|
|
120
|
+
return nil unless m
|
|
121
|
+
event = classify_event(m[3])
|
|
122
|
+
return nil unless event
|
|
123
|
+
[parse_log_time(m[1], m[2]), event, m[3].strip]
|
|
124
|
+
end
|
|
125
|
+
private_class_method :classify_log_line
|
|
126
|
+
|
|
127
|
+
def parse_log_time(date, hhmm)
|
|
128
|
+
y, mo, d = date.split("-").map(&:to_i)
|
|
129
|
+
h, mi = hhmm.split(":").map(&:to_i)
|
|
130
|
+
Time.utc(y, mo, d, h, mi, 0)
|
|
131
|
+
end
|
|
132
|
+
private_class_method :parse_log_time
|
|
133
|
+
|
|
134
|
+
def classify_event(text)
|
|
135
|
+
hit = KEYWORD_TABLE.find { |regex, _event| text =~ regex }
|
|
136
|
+
hit && hit[1]
|
|
137
|
+
end
|
|
138
|
+
private_class_method :classify_event
|
|
139
|
+
|
|
140
|
+
WAVE_ENTRY = /\A-\s*\[([ xX])\]\s+(\S+)\s+.+—\s*(\S+)\s*\z/.freeze
|
|
141
|
+
|
|
142
|
+
# Intent ids of every `[x] ... — delivered` entry in the `## Waves` body.
|
|
143
|
+
def delivered_wave_ids(waves_body)
|
|
144
|
+
waves_body.each_line.filter_map do |line|
|
|
145
|
+
m = line.strip.match(WAVE_ENTRY)
|
|
146
|
+
next nil unless m
|
|
147
|
+
checked = m[1].strip.downcase == "x"
|
|
148
|
+
status = m[3].strip.downcase
|
|
149
|
+
m[2] if checked && status == "delivered"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
private_class_method :delivered_wave_ids
|
|
153
|
+
|
|
154
|
+
# For every delivered wave id with no `merged` line already among `log_lines` (the "matching
|
|
155
|
+
# Log line" source, D4), fall back to the tier's INDEX `## Completed` section (the second
|
|
156
|
+
# on-disk source D4 allows). No source at all -> the id is silently skipped, never invented.
|
|
157
|
+
# Backfilled lines are appended after the Log-derived pass (a reconciliation pass run after
|
|
158
|
+
# reconstruction), each carrying its own real on-disk-sourced timestamp even when that
|
|
159
|
+
# timestamp sorts earlier than same-day Log lines above it (INDEX only carries a date, so the
|
|
160
|
+
# time floors to midnight UTC rather than inventing a time-of-day).
|
|
161
|
+
def backfill_merged_lines(log_lines, delivered_ids, roadmap_path)
|
|
162
|
+
index_path = index_path_for(roadmap_path)
|
|
163
|
+
delivered_ids.filter_map do |id|
|
|
164
|
+
next nil if log_lines.any? { |_t, event, detail| event == "merged" && detail =~ /\b#{Regexp.escape(id)}\b/ }
|
|
165
|
+
date = index_completed_date(index_path, id)
|
|
166
|
+
next nil unless date
|
|
167
|
+
[Time.utc(*date.split("-").map(&:to_i)), "merged", "#{id} (from INDEX Completed)"]
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
private_class_method :backfill_merged_lines
|
|
171
|
+
|
|
172
|
+
# The tier root is the parent of `roadmaps/` (a live roadmap's grandparent, or, for one
|
|
173
|
+
# already moved to `roadmaps/archived/`, its great-grandparent); INDEX.md is that root's
|
|
174
|
+
# sibling file, matching the layout `plastic-roadmap`'s file-format doc defines.
|
|
175
|
+
def index_path_for(roadmap_path)
|
|
176
|
+
dir = File.dirname(roadmap_path)
|
|
177
|
+
dir = File.dirname(dir) if File.basename(dir) == "archived"
|
|
178
|
+
File.join(File.dirname(dir), "INDEX.md")
|
|
179
|
+
end
|
|
180
|
+
private_class_method :index_path_for
|
|
181
|
+
|
|
182
|
+
def index_completed_date(index_path, id)
|
|
183
|
+
return nil unless index_path && File.exist?(index_path)
|
|
184
|
+
section_body(File.read(index_path), "Completed").each_line do |line|
|
|
185
|
+
next unless line.strip.start_with?("- [#{id} ")
|
|
186
|
+
m = line.match(/\)\s*—\s*(\d{4}-\d{2}-\d{2})\b/)
|
|
187
|
+
return m[1] if m
|
|
188
|
+
end
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
private_class_method :index_completed_date
|
|
192
|
+
|
|
193
|
+
# The body text of a `## <heading>` section: everything after the heading line up to (but not
|
|
194
|
+
# including) the next `## ` heading or end of file.
|
|
195
|
+
def section_body(text, heading)
|
|
196
|
+
m = text.match(/^##\s+#{Regexp.escape(heading)}\s*$(.*?)(?=^##\s|\z)/m)
|
|
197
|
+
m ? m[1] : ""
|
|
198
|
+
end
|
|
199
|
+
private_class_method :section_body
|
|
200
|
+
|
|
201
|
+
# Stable dedup on the `(event, detail)` pair, keeping the first occurrence in the given
|
|
202
|
+
# (already chronological-then-backfill-appended) order.
|
|
203
|
+
def dedup_pairs(lines)
|
|
204
|
+
seen = []
|
|
205
|
+
lines.select do |_t, event, detail|
|
|
206
|
+
pair = [event, detail]
|
|
207
|
+
next false if seen.include?(pair)
|
|
208
|
+
seen << pair
|
|
209
|
+
true
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
private_class_method :dedup_pairs
|
|
213
|
+
end
|
|
@@ -14,7 +14,7 @@ require "yaml"
|
|
|
14
14
|
# `project.yml` (from templates/project.yml). Re-running never clobbers existing
|
|
15
15
|
# files. The logic was migrated here from the orphaned
|
|
16
16
|
# `InstallerCore#bootstrap_project_store` so there is one definition that the
|
|
17
|
-
# `provision-project-store` CLI, the `plastic-
|
|
17
|
+
# `provision-project-store` CLI, the `plastic-store-provisioning` skill, the
|
|
18
18
|
# doctor fix hint, and the project skills all consult.
|
|
19
19
|
#
|
|
20
20
|
# Pure filesystem and dependency-injected: `provision` accepts an injectable
|
package/scripts/lib/worktree.rb
CHANGED
|
@@ -79,6 +79,21 @@ module Worktree
|
|
|
79
79
|
}
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
# Derive the OS-HOME level (the PARENT of `.plastic`) from an intent store
|
|
83
|
+
# path, anchored on the `.plastic` path segment (Defect 1, intent 169): a
|
|
84
|
+
# store is always `<plastic_home>/store` or
|
|
85
|
+
# `<plastic_home>/projects/<slug>/store`, and `<plastic_home>` is always the
|
|
86
|
+
# `.plastic` dir. Returns nil when `store` is blank or carries no `.plastic`
|
|
87
|
+
# segment, so callers fall back to their own `home:` default rather than
|
|
88
|
+
# resolving anything. Pure: no `ENV[...]` read, no I/O.
|
|
89
|
+
def home_from_store(store)
|
|
90
|
+
return nil if blank?(store)
|
|
91
|
+
parts = File.expand_path(store.to_s).split(File::SEPARATOR)
|
|
92
|
+
idx = parts.rindex(".plastic")
|
|
93
|
+
return nil unless idx
|
|
94
|
+
parts[0...idx].join(File::SEPARATOR)
|
|
95
|
+
end
|
|
96
|
+
|
|
82
97
|
# Absolute repo path for a project slug from `~/.plastic/projects.yml`, or nil.
|
|
83
98
|
# Reuses the qmd_sync safe-loader pattern: any failure yields nil.
|
|
84
99
|
def repo_for(slug, home: Dir.home)
|
|
@@ -106,6 +121,12 @@ module Worktree
|
|
|
106
121
|
store = intent["store"].to_s
|
|
107
122
|
intent_slug = slug_from_dir(intent["dir"]) || slug_from_dir(store)
|
|
108
123
|
|
|
124
|
+
# Defect 1 fix (intent 169): derive plastic_home from the already-sandboxed
|
|
125
|
+
# store path when possible, so a sandboxed board never falls to the real
|
|
126
|
+
# `Dir.home` default. `home:` remains the fallback only when the store is
|
|
127
|
+
# blank or carries no recognizable `.plastic` segment.
|
|
128
|
+
home = home_from_store(store) || home
|
|
129
|
+
|
|
109
130
|
slug = slug_for_store(store, home: home)
|
|
110
131
|
p = paths(slug: slug, intent_id: intent_id, intent_slug: intent_slug, home: home)
|
|
111
132
|
|
package/scripts/new-intent
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
# IntentValidator (exit non-zero if not born complete).
|
|
14
14
|
#
|
|
15
15
|
# It does NOT touch INDEX.md, git, or project creation: those remain skill/agent
|
|
16
|
-
# responsibilities (see plastic-creating
|
|
16
|
+
# responsibilities (see plastic-intent-creating).
|
|
17
17
|
#
|
|
18
18
|
# Usage:
|
|
19
19
|
# new-intent --store <store_path> --intent "<one-line>" --slug <slug> \
|
|
@@ -282,6 +282,7 @@ def main(argv)
|
|
|
282
282
|
|
|
283
283
|
# 2. Create dirs.
|
|
284
284
|
FileUtils.mkdir_p(File.join(intent_dir, "actions"))
|
|
285
|
+
File.write(File.join(intent_dir, "actions", ".gitkeep"), "")
|
|
285
286
|
FileUtils.mkdir_p(File.join(intent_dir, "resources"))
|
|
286
287
|
|
|
287
288
|
# 3. Render the born-complete intent file from templates/intent.md.
|
package/scripts/read-config
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Returns scalar values as strings, hash/array values as JSON.
|
|
6
6
|
#
|
|
7
7
|
# Environment:
|
|
8
|
-
#
|
|
8
|
+
# PLASTIC_HOME -- override ~/.plastic (for testing)
|
|
9
9
|
|
|
10
10
|
require "yaml"
|
|
11
11
|
require "json"
|
|
@@ -88,7 +88,7 @@ end
|
|
|
88
88
|
|
|
89
89
|
# Handle --migrate mode
|
|
90
90
|
if ARGV.include?("--migrate")
|
|
91
|
-
global_root = ENV.fetch("
|
|
91
|
+
global_root = ENV.fetch("PLASTIC_HOME", File.expand_path("~/.plastic"))
|
|
92
92
|
config_path = File.join(global_root, "config.yml")
|
|
93
93
|
config = load_yaml(config_path)
|
|
94
94
|
|
|
@@ -116,7 +116,7 @@ if key.nil? || key.empty?
|
|
|
116
116
|
exit 1
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
-
global_root = ENV.fetch("
|
|
119
|
+
global_root = ENV.fetch("PLASTIC_HOME", File.expand_path("~/.plastic"))
|
|
120
120
|
global_config = load_yaml(File.join(global_root, "config.yml"))
|
|
121
121
|
|
|
122
122
|
project_config = {}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# roadmap-next - thin CLI over RoadmapQueue (intent 148).
|
|
6
|
+
#
|
|
7
|
+
# The auto loop and plastic-roadmap-continuing both call this instead of ranking roadmap
|
|
8
|
+
# liveness in prose. Queue mode (default) is for the auto loop: it breaks a tie
|
|
9
|
+
# deterministically and reports the winner's frontier state. Which mode (--which) is for
|
|
10
|
+
# plastic-roadmap-continuing: it returns tie_candidates so the skill's single ask can resolve
|
|
11
|
+
# a genuine tie.
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# roadmap-next --roadmaps-dir <dir> [--index <path>] [--which]
|
|
15
|
+
#
|
|
16
|
+
# Exits 0 on any successful analysis, including state "none" and "exhausted" (valid answers,
|
|
17
|
+
# not errors). Exits non-zero only on a missing required flag.
|
|
18
|
+
|
|
19
|
+
require_relative "lib/roadmap_queue"
|
|
20
|
+
require "json"
|
|
21
|
+
|
|
22
|
+
def opt(args, name)
|
|
23
|
+
(i = args.index(name)) && args[i + 1]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def usage
|
|
27
|
+
warn "Usage: roadmap-next --roadmaps-dir <dir> [--index <path>] [--which]"
|
|
28
|
+
warn " default mode is the auto-loop queue; --which returns tie_candidates for continuing."
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
which = !!ARGV.delete("--which")
|
|
32
|
+
roadmaps_dir = opt(ARGV, "--roadmaps-dir")
|
|
33
|
+
index = opt(ARGV, "--index")
|
|
34
|
+
|
|
35
|
+
if roadmaps_dir.nil?
|
|
36
|
+
warn "roadmap-next: pass --roadmaps-dir <dir>"
|
|
37
|
+
usage
|
|
38
|
+
exit 2
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
reader = RoadmapQueue.new(roadmaps_dir: roadmaps_dir, index_path: index)
|
|
42
|
+
payload = which ? reader.which : reader.queue
|
|
43
|
+
puts JSON.generate(payload)
|
|
44
|
+
exit 0
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# roadmap-savepoint - deterministic CLI over RoadmapSavepoint (intent 134).
|
|
6
|
+
#
|
|
7
|
+
# The plastic-roadmap skill verbs and the coordinator flows call this instead of hand-writing
|
|
8
|
+
# ledger lines. append is idempotent on the (event, detail) pair; rebuild is the one operation
|
|
9
|
+
# allowed to overwrite the ledger, reconstructing it from the roadmap's own `## Log`.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# roadmap-savepoint append --roadmap <path> --event <event> --detail "<text>"
|
|
13
|
+
# roadmap-savepoint rebuild --roadmap <path>
|
|
14
|
+
#
|
|
15
|
+
# Exits non-zero with a usage line on stderr for an unknown verb or a missing required flag.
|
|
16
|
+
|
|
17
|
+
require_relative "lib/roadmap_savepoint"
|
|
18
|
+
|
|
19
|
+
def opt(args, name)
|
|
20
|
+
(i = args.index(name)) && args[i + 1]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def usage
|
|
24
|
+
warn "Usage: roadmap-savepoint append --roadmap <path> --event <event> --detail \"<text>\""
|
|
25
|
+
warn " roadmap-savepoint rebuild --roadmap <path>"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
verb = ARGV.shift
|
|
29
|
+
|
|
30
|
+
case verb
|
|
31
|
+
when "append"
|
|
32
|
+
roadmap = opt(ARGV, "--roadmap")
|
|
33
|
+
event = opt(ARGV, "--event")
|
|
34
|
+
detail = opt(ARGV, "--detail")
|
|
35
|
+
if roadmap.nil? || event.nil? || detail.nil?
|
|
36
|
+
warn "append: pass --roadmap <path> --event <event> --detail \"<text>\""
|
|
37
|
+
usage
|
|
38
|
+
exit 2
|
|
39
|
+
end
|
|
40
|
+
begin
|
|
41
|
+
wrote = RoadmapSavepoint.append(roadmap, event, detail)
|
|
42
|
+
puts wrote ? "appended #{event} #{detail}" : "no-op (already recorded): #{event} #{detail}"
|
|
43
|
+
exit 0
|
|
44
|
+
rescue ArgumentError => e
|
|
45
|
+
warn "append: #{e.message}"
|
|
46
|
+
exit 2
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
when "rebuild"
|
|
50
|
+
roadmap = opt(ARGV, "--roadmap")
|
|
51
|
+
if roadmap.nil?
|
|
52
|
+
warn "rebuild: pass --roadmap <path>"
|
|
53
|
+
usage
|
|
54
|
+
exit 2
|
|
55
|
+
end
|
|
56
|
+
count = RoadmapSavepoint.rebuild(roadmap)
|
|
57
|
+
puts "rebuilt #{count} line#{count == 1 ? '' : 's'}"
|
|
58
|
+
exit 0
|
|
59
|
+
|
|
60
|
+
else
|
|
61
|
+
warn "roadmap-savepoint: unknown verb #{verb.inspect}. Use append|rebuild."
|
|
62
|
+
usage
|
|
63
|
+
exit 2
|
|
64
|
+
end
|
|
@@ -2,20 +2,24 @@
|
|
|
2
2
|
# encoding: UTF-8
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
|
-
# Plastic — `
|
|
6
|
-
# Usage: ruby scripts/
|
|
5
|
+
# Plastic — `rollback` verb. Runs via `npx @zalom/plastic rollback` (bin/plastic.js) or directly.
|
|
6
|
+
# Usage: ruby scripts/rollback.rb [--version <v> [--downgrade | --upgrade]] [--help]
|
|
7
7
|
#
|
|
8
8
|
# Local time-machine over the append-only versions.json ledger. Navigates ONLY versions you
|
|
9
9
|
# have actually run (the ledger), never npm's full list — fetching brand-new versions is
|
|
10
10
|
# `update`'s job. Lets you roll back to a known-good build after a bad release.
|
|
11
|
-
# - no flag
|
|
12
|
-
# - --
|
|
13
|
-
# - --downgrade --version V
|
|
14
|
-
# - --upgrade
|
|
11
|
+
# - no flag read-only: print the version-history table, never switch
|
|
12
|
+
# - --version V switch to a specific previously-run version (direction derived)
|
|
13
|
+
# - --downgrade --version V synonym of --version V (direction flag is descriptive only)
|
|
14
|
+
# - --upgrade --version V synonym of --version V (direction flag is descriptive only)
|
|
15
|
+
# - --downgrade / --upgrade alone no target given: error, no switch
|
|
16
|
+
#
|
|
17
|
+
# Switching always requires an explicit --version V. A rollback tool whose job is safety
|
|
18
|
+
# during a bad release must never switch versions without a named target.
|
|
15
19
|
|
|
16
20
|
require_relative "lib/installer_core"
|
|
17
21
|
|
|
18
|
-
class
|
|
22
|
+
class Rollback < InstallerCore
|
|
19
23
|
PKG = "@zalom/plastic"
|
|
20
24
|
|
|
21
25
|
def cli(argv = ARGV)
|
|
@@ -32,31 +36,24 @@ class Versions < InstallerCore
|
|
|
32
36
|
|
|
33
37
|
timeline = version_timeline(ledger)
|
|
34
38
|
current = installed_version || timeline.last
|
|
39
|
+
explicit = flag_value(argv, "--version")
|
|
35
40
|
|
|
36
|
-
if
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return no_target("No earlier version in your history.") unless target
|
|
40
|
-
unless timeline.include?(target)
|
|
41
|
-
warn "#{target} is not in your version history. Choose one of: #{timeline.join(", ")}"
|
|
41
|
+
if explicit
|
|
42
|
+
unless timeline.include?(explicit)
|
|
43
|
+
warn "#{explicit} is not in your version history. Choose one of: #{timeline.join(", ")}"
|
|
42
44
|
return 1
|
|
43
45
|
end
|
|
44
|
-
return switch_to(
|
|
45
|
-
elsif argv.include?("--upgrade")
|
|
46
|
-
target = step_target(timeline, current, :forward)
|
|
47
|
-
return no_target("No later version in your history. Use `update` for new releases.") unless target
|
|
48
|
-
return switch_to(target, current)
|
|
46
|
+
return switch_to(explicit, current)
|
|
49
47
|
end
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if prev && $stdin.tty?
|
|
56
|
-
print "\nYou recently rolled back. Go back further to #{prev}? [y/N]: "
|
|
57
|
-
return switch_to(prev, current) if ($stdin.gets&.strip || "").downcase.start_with?("y")
|
|
58
|
-
end
|
|
49
|
+
if argv.include?("--downgrade") || argv.include?("--upgrade")
|
|
50
|
+
warn "An explicit target is required: pass --version <v>. Run `rollback` with no " \
|
|
51
|
+
"flags to see your version history and pick a target."
|
|
52
|
+
return 1
|
|
59
53
|
end
|
|
54
|
+
|
|
55
|
+
# No flags at all: read-only. Print the table and stop; never switch, never prompt.
|
|
56
|
+
print_table(ledger, current)
|
|
60
57
|
0
|
|
61
58
|
end
|
|
62
59
|
|
|
@@ -93,11 +90,6 @@ class Versions < InstallerCore
|
|
|
93
90
|
argv[i + 1]
|
|
94
91
|
end
|
|
95
92
|
|
|
96
|
-
def no_target(msg)
|
|
97
|
-
puts msg
|
|
98
|
-
0
|
|
99
|
-
end
|
|
100
|
-
|
|
101
93
|
def switch_to(target, current)
|
|
102
94
|
action = action_for(target, current)
|
|
103
95
|
puts "#{action == "downgrade" ? "\u{23ea}" : "\u{23e9}"} #{current} \u{2192} #{target} (#{action})"
|
|
@@ -119,23 +111,23 @@ class Versions < InstallerCore
|
|
|
119
111
|
def show_help
|
|
120
112
|
puts <<~HELP
|
|
121
113
|
|
|
122
|
-
plastic
|
|
114
|
+
plastic rollback — read installed and available versions, and switch on request
|
|
123
115
|
|
|
124
116
|
Usage:
|
|
125
|
-
npx @zalom/plastic
|
|
117
|
+
npx @zalom/plastic rollback [options]
|
|
126
118
|
|
|
127
119
|
Options:
|
|
128
|
-
(none)
|
|
129
|
-
--
|
|
130
|
-
--downgrade --version V
|
|
131
|
-
--upgrade
|
|
132
|
-
-h, --help
|
|
120
|
+
(none) Show the version-history table (read-only, no switch)
|
|
121
|
+
--version V Switch to version V (direction derived automatically)
|
|
122
|
+
--downgrade --version V Synonym of --version V
|
|
123
|
+
--upgrade --version V Synonym of --version V
|
|
124
|
+
-h, --help Show this help
|
|
133
125
|
|
|
134
|
-
|
|
135
|
-
brand-new release, use `update`.
|
|
126
|
+
Switching always requires an explicit --version V. Navigates only versions you have
|
|
127
|
+
already run (the append-only ledger). To move to a brand-new release, use `update`.
|
|
136
128
|
|
|
137
129
|
HELP
|
|
138
130
|
end
|
|
139
131
|
end
|
|
140
132
|
|
|
141
|
-
exit(
|
|
133
|
+
exit(Rollback.new(package_root: ENV["PLASTIC_PACKAGE_ROOT"] || File.expand_path("..", __dir__)).cli(ARGV)) if $PROGRAM_NAME == __FILE__
|
package/scripts/update.rb
CHANGED
|
@@ -164,7 +164,7 @@ class Update < InstallerCore
|
|
|
164
164
|
Behaviour:
|
|
165
165
|
No flag advances to the next version on your current channel. Switching toward a
|
|
166
166
|
more stable channel is frictionless; switching toward bleeding edge is confirmed.
|
|
167
|
-
Use `
|
|
167
|
+
Use `rollback` to roll back to a previously-installed version.
|
|
168
168
|
|
|
169
169
|
HELP
|
|
170
170
|
end
|
|
@@ -13,7 +13,7 @@ Before proceeding, resolve the active intent:
|
|
|
13
13
|
- Look under `## Active` for intent entries
|
|
14
14
|
- If exactly one active intent → use it
|
|
15
15
|
- If multiple active intents → ask user which one
|
|
16
|
-
- If no active intent → refuse: "No active intent. Create one first with /plastic-creating
|
|
16
|
+
- If no active intent → refuse: "No active intent. Create one first with /plastic-intent-creating"
|
|
17
17
|
|
|
18
18
|
3. **Resolve paths:**
|
|
19
19
|
- Intent directory: `{store}/store/{id}--{slug}/`
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
## Numbered Decision Tables
|
|
2
|
+
|
|
3
|
+
The shared procedure for collecting owner rulings during any stage (Why, How, Exec).
|
|
4
|
+
Any stage skill that needs the owner to choose between options or rule on a batch of
|
|
5
|
+
open questions follows this procedure instead of improvising its own format.
|
|
6
|
+
|
|
7
|
+
Read this when a stage skill's own text says to.
|
|
8
|
+
|
|
9
|
+
### The procedure
|
|
10
|
+
|
|
11
|
+
1. **Collect the candidate decisions.** Gather every open question or option set that
|
|
12
|
+
needs an owner ruling right now. Do not present them one at a time across separate
|
|
13
|
+
messages; batch them into a single collection first.
|
|
14
|
+
|
|
15
|
+
2. **Present exactly ONE table.** Its first column is the row number. One decision per
|
|
16
|
+
row. Add a recommendation column stating the agent's recommended choice and a short
|
|
17
|
+
reason. Example shape:
|
|
18
|
+
|
|
19
|
+
| # | Decision | Recommendation |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| 1 | Ship the setting as a CLI flag or a config file? | Config file: survives across invocations |
|
|
22
|
+
| 2 | Retry, circuit breaker, or fallback for the flaky call? | Retry: matches existing error handling elsewhere in this module |
|
|
23
|
+
|
|
24
|
+
3. **Let the owner rule by row number.** The owner responds with a ruling per row
|
|
25
|
+
number (e.g. "1: config file, 2: retry"). Do not require prose paragraphs back; a
|
|
26
|
+
row-number ruling is enough.
|
|
27
|
+
|
|
28
|
+
4. **Persist each ruling immediately, one at a time.** For every ruling, before moving
|
|
29
|
+
to the next row, run:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
ruby ~/.plastic/scripts/insight-append {intent_dir} "<ruling text>" --stage <stage> --author human
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Never batch rulings into a single call and never wait until all rows are ruled to
|
|
36
|
+
start persisting. `<stage>` is the current stage (Why, How, or Exec). If a ruling
|
|
37
|
+
supersedes an earlier one already on record, append a second insight that names the
|
|
38
|
+
superseded decision and states plainly that this ruling supersedes it. Both insights
|
|
39
|
+
stay on record; the later one wins.
|
|
40
|
+
|
|
41
|
+
5. **Write the full ruling set to a rulings file.** After the last row is ruled and
|
|
42
|
+
persisted, write the complete table plus every ruling to
|
|
43
|
+
`{intent_dir}/resources/rulings--<slug>.md` so the full set is readable in one place
|
|
44
|
+
alongside the per-row insights.
|
|
45
|
+
|
|
46
|
+
### Why one table, not a chip per row
|
|
47
|
+
|
|
48
|
+
A single table lets the owner rule on everything in one pass and reference row numbers
|
|
49
|
+
in their reply. Multiple small prompts force the owner to context-switch per decision
|
|
50
|
+
and make later replies ambiguous about which decision they answer.
|