@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.
Files changed (130) hide show
  1. package/PLASTIC-reference.md +11 -11
  2. package/PLASTIC.md +11 -9
  3. package/README.md +48 -1
  4. package/agents/plastic-enforcer.md +8 -5
  5. package/agents/plastic-future-intent-researcher.md +1 -1
  6. package/agents/plastic-intent-curator.md +9 -10
  7. package/agents/plastic-spec-specialist.md +9 -6
  8. package/bin/plastic.js +2 -2
  9. package/hooks/check-update +1 -1
  10. package/hooks/continue +1 -1
  11. package/package.json +1 -1
  12. package/scripts/dashboard.rb +29 -24
  13. package/scripts/doctor.rb +188 -9
  14. package/scripts/end-intent +330 -0
  15. package/scripts/hook-code-gate +1 -1
  16. package/scripts/hook-continue +2 -2
  17. package/scripts/hook-create-gate +2 -2
  18. package/scripts/hook-session-start +1 -1
  19. package/scripts/install.rb +2 -1
  20. package/scripts/lib/bridge.rb +116 -3
  21. package/scripts/lib/dashboard_banner.rb +8 -9
  22. package/scripts/lib/installer_core.rb +17 -8
  23. package/scripts/lib/legacy_bookend_amnesty.rb +35 -0
  24. package/scripts/lib/release_guard.rb +62 -0
  25. package/scripts/lib/roadmap_queue.rb +285 -0
  26. package/scripts/lib/roadmap_savepoint.rb +213 -0
  27. package/scripts/lib/store_provisioning.rb +1 -1
  28. package/scripts/lib/worktree.rb +21 -0
  29. package/scripts/new-intent +2 -1
  30. package/scripts/read-config +3 -3
  31. package/scripts/roadmap-next +44 -0
  32. package/scripts/roadmap-savepoint +64 -0
  33. package/scripts/{versions.rb → rollback.rb} +33 -41
  34. package/scripts/update.rb +1 -1
  35. package/skills/_active-intent-gate.md +1 -1
  36. package/skills/_decision-tables.md +50 -0
  37. package/skills/auto/SKILL.md +69 -68
  38. package/skills/auto/evals/evals.json +4 -4
  39. package/skills/auto/references/agent-architecture.md +2 -2
  40. package/skills/continuing/SKILL.md +23 -130
  41. package/skills/continuing/evals/evals.json +26 -80
  42. package/skills/dashboard/SKILL.md +18 -15
  43. package/skills/dashboard/references/classification.md +3 -3
  44. package/skills/dashboard/templates/dashboard-global.md +8 -23
  45. package/skills/dashboard/templates/dashboard-project.md +7 -26
  46. package/skills/doctor/SKILL.md +3 -3
  47. package/skills/install/SKILL.md +10 -10
  48. package/skills/{brainstorming → intent-brainstorming}/SKILL.md +43 -45
  49. package/skills/intent-brainstorming/evals/evals.json +67 -0
  50. package/skills/{brainstorming → intent-brainstorming}/references/design-principles.md +12 -17
  51. package/skills/intent-continuing/SKILL.md +99 -0
  52. package/skills/intent-continuing/evals/evals.json +145 -0
  53. package/skills/intent-continuing/references/context-management.md +28 -0
  54. package/skills/{creating-intent → intent-creating}/SKILL.md +3 -3
  55. package/skills/{creating-intent → intent-creating}/evals/evals.json +1 -1
  56. package/skills/{intent-discovery → intent-discovering}/SKILL.md +1 -1
  57. package/skills/intent-ending/SKILL.md +151 -0
  58. package/skills/intent-ending/evals/evals.json +74 -0
  59. package/skills/{executing-plan → intent-executing}/SKILL.md +75 -15
  60. package/skills/intent-executing/evals/evals.json +66 -0
  61. package/skills/{brainstorming-grill-me → intent-grilling}/SKILL.md +4 -4
  62. package/skills/{linking-intents → intent-linking}/SKILL.md +1 -1
  63. package/skills/{linking-intents → intent-linking}/evals/evals.json +1 -1
  64. package/skills/{lock → intent-locking}/SKILL.md +1 -1
  65. package/skills/{writing-plans → intent-planning}/SKILL.md +48 -20
  66. package/skills/intent-planning/evals/evals.json +66 -0
  67. package/skills/{writing-plans → intent-planning}/references/plan-format.md +31 -7
  68. package/skills/{research → intent-researching}/SKILL.md +2 -2
  69. package/skills/{research → intent-researching}/evals/evals.json +1 -1
  70. package/skills/{savepoint → intent-savepoint}/SKILL.md +13 -1
  71. package/skills/intent-speccing/SKILL.md +69 -0
  72. package/skills/intent-speccing/evals/evals.json +126 -0
  73. package/skills/intent-speccing/references/per-section-fill-rules.md +92 -0
  74. package/skills/intent-speccing/references/self-verify-checklist.md +38 -0
  75. package/skills/intent-starting/SKILL.md +2 -2
  76. package/skills/intent-starting/evals/evals.json +2 -2
  77. package/skills/project-continuing/SKILL.md +104 -0
  78. package/skills/project-continuing/evals/evals.json +100 -0
  79. package/skills/project-continuing/references/board-fill.md +33 -0
  80. package/skills/{creating-project → project-creating}/SKILL.md +2 -2
  81. package/skills/releasing/SKILL.md +66 -14
  82. package/skills/releasing/references/release-lines.md +105 -0
  83. package/skills/roadmap/SKILL.md +7 -1
  84. package/skills/roadmap/references/file-format.md +30 -1
  85. package/skills/roadmap/references/operations.md +26 -6
  86. package/skills/roadmap-continuing/SKILL.md +85 -0
  87. package/skills/roadmap-continuing/evals/evals.json +82 -0
  88. package/skills/roadmap-continuing/references/liveness-ranking.md +56 -0
  89. package/skills/{versions → rollback}/SKILL.md +30 -20
  90. package/skills/{creating-skills → skill-creating}/SKILL.md +2 -2
  91. package/skills/{creating-skills → skill-creating}/evals/evals.json +5 -5
  92. package/skills/{creating-skills → skill-creating}/references/progressive-disclosure.md +2 -2
  93. package/skills/{evaluating-skills → skill-evaluating}/SKILL.md +1 -1
  94. package/skills/{evaluating-skills → skill-evaluating}/evals/evals.json +3 -3
  95. package/skills/{intent-curator → store-curating}/SKILL.md +6 -7
  96. package/skills/{intent-curator → store-curating}/evals/evals.json +1 -1
  97. package/skills/{managing-index → store-indexing}/SKILL.md +6 -7
  98. package/skills/{managing-index → store-indexing}/evals/evals.json +1 -1
  99. package/skills/{add-project-store → store-provisioning}/SKILL.md +2 -2
  100. package/skills/tutorial/SKILL.md +65 -0
  101. package/skills/tutorial/evals/evals.json +186 -0
  102. package/skills/tutorial/references/track-1-guided.md +123 -0
  103. package/skills/tutorial/references/track-2-auto.md +95 -0
  104. package/skills/tutorial/references/track-3-projects-and-roadmaps.md +110 -0
  105. package/skills/uninstall/SKILL.md +2 -2
  106. package/skills/update/SKILL.md +3 -3
  107. package/templates/agents.md +7 -7
  108. package/templates/config.yml +2 -1
  109. package/templates/index.md +4 -1
  110. package/skills/brainstorming/evals/evals.json +0 -22
  111. package/skills/savepoint/references/context-management.md +0 -32
  112. /package/skills/{creating-intent → intent-creating}/references/lifecycle.md +0 -0
  113. /package/skills/{creating-intent → intent-creating}/references/wikilinks.md +0 -0
  114. /package/skills/{executing-plan → intent-executing}/code-quality-reviewer-prompt.md +0 -0
  115. /package/skills/{executing-plan → intent-executing}/implementer-prompt.md +0 -0
  116. /package/skills/{executing-plan → intent-executing}/spec-reviewer-prompt.md +0 -0
  117. /package/skills/{linking-intents → intent-linking}/references/zettelkasten.md +0 -0
  118. /package/skills/{continuing → intent-savepoint}/references/context-management.md +0 -0
  119. /package/skills/{creating-project → project-creating}/references/hubs-projects.md +0 -0
  120. /package/skills/{creating-project → project-creating}/references/project-scaffolding.md +0 -0
  121. /package/skills/{creating-skills → skill-creating}/references/agents.md +0 -0
  122. /package/skills/{creating-skills → skill-creating}/references/evals.md +0 -0
  123. /package/skills/{creating-skills → skill-creating}/references/hooks.md +0 -0
  124. /package/skills/{creating-skills → skill-creating}/references/scripts.md +0 -0
  125. /package/skills/{creating-skills → skill-creating}/references/skills.md +0 -0
  126. /package/skills/{creating-skills → skill-creating}/scripts/scaffold.rb +0 -0
  127. /package/skills/{evaluating-skills → skill-evaluating}/assets/eval-template.json +0 -0
  128. /package/skills/{evaluating-skills → skill-evaluating}/references/convention-checks.md +0 -0
  129. /package/skills/{evaluating-skills → skill-evaluating}/references/eval-methodology.md +0 -0
  130. /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-add-project-store` skill, the
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
@@ -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
 
@@ -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-intent).
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.
@@ -5,7 +5,7 @@
5
5
  # Returns scalar values as strings, hash/array values as JSON.
6
6
  #
7
7
  # Environment:
8
- # PLASTIC_GLOBAL_ROOT -- override ~/.plastic (for testing)
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("PLASTIC_GLOBAL_ROOT", File.expand_path("~/.plastic"))
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("PLASTIC_GLOBAL_ROOT", File.expand_path("~/.plastic"))
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 — `versions` verb. Runs via `npx @zalom/plastic versions` (bin/plastic.js) or directly.
6
- # Usage: ruby scripts/versions.rb [--downgrade [--version <v>] | --upgrade] [--help]
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 print the ledger table (and offer to continue a rollback)
12
- # - --downgrade step back one version in your history
13
- # - --downgrade --version V jump to a specific previously-run version
14
- # - --upgrade step forward one version (after a rollback)
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 Versions < InstallerCore
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 argv.include?("--downgrade")
37
- explicit = flag_value(argv, "--version")
38
- target = explicit || step_target(timeline, current, :back)
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(target, current)
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
- # No flag: show the table, and detect an in-progress rollback.
52
- print_table(ledger, current)
53
- if ledger.last && ledger.last["action"] == "downgrade"
54
- prev = step_target(timeline, current, :back)
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 versionsmanage and roll back your local Plastic versions
114
+ plastic rollbackread installed and available versions, and switch on request
123
115
 
124
116
  Usage:
125
- npx @zalom/plastic versions [options]
117
+ npx @zalom/plastic rollback [options]
126
118
 
127
119
  Options:
128
- (none) Show the version-history table
129
- --downgrade Roll back one version in your history
130
- --downgrade --version V Roll back to a specific previously-run version
131
- --upgrade Step forward one version in your history
132
- -h, --help Show this 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
- Navigates only versions you have already run (the append-only ledger). To move to a
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(Versions.new(package_root: ENV["PLASTIC_PACKAGE_ROOT"] || File.expand_path("..", __dir__)).cli(ARGV)) if $PROGRAM_NAME == __FILE__
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 `versions` to roll back to a previously-installed version.
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-intent"
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.