@zalom/plastic 2.0.0-alpha.27 → 2.0.0-alpha.29

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 (207) hide show
  1. package/PLASTIC.md +13 -139
  2. package/README.md +345 -133
  3. package/agents/plastic-enforcer.md +9 -10
  4. package/agents/plastic-executor.md +1 -1
  5. package/bin/crap +4 -0
  6. package/bin/lib/context_budget.rb +35 -1
  7. package/bin/lib/skill_census.rb +839 -0
  8. package/bin/plastic +6 -0
  9. package/bin/plastic-skill-census +114 -0
  10. package/bin/verify-change +345 -0
  11. package/deprecations.yml +1 -1
  12. package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +4 -7
  13. package/{skills/auto/references → docs/help}/agent-architecture.md +7 -7
  14. package/{skills/conventions/references → docs/help}/completion-and-done.md +1 -1
  15. package/{skills/auto/references → docs/help}/human-report-contract.md +17 -19
  16. package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
  17. package/{skills/tutorial/references → docs/help}/track-1-guided.md +8 -8
  18. package/{skills/tutorial/references → docs/help}/track-2-auto.md +5 -5
  19. package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
  20. package/package.json +3 -2
  21. package/scripts/append-ledger +2 -1
  22. package/scripts/dashboard.rb +10 -9
  23. package/scripts/day-summary +2 -1
  24. package/scripts/doctor.rb +48 -42
  25. package/scripts/end-intent +2 -8
  26. package/scripts/file-session-intent +2 -1
  27. package/scripts/hook-capture +4 -3
  28. package/scripts/hook-close +2 -1
  29. package/scripts/hook-record +3 -2
  30. package/scripts/hook-savepoint +3 -2
  31. package/scripts/hook-session-start +5 -4
  32. package/scripts/hook-stop +2 -1
  33. package/scripts/insight-append +1 -2
  34. package/scripts/install.rb +3 -1
  35. package/scripts/lib/active_delivery.rb +1 -1
  36. package/scripts/lib/arm.rb +2 -1
  37. package/scripts/lib/backup.rb +65 -0
  38. package/scripts/lib/cli/command.rb +85 -0
  39. package/scripts/lib/cli/commands/auto.rb +18 -0
  40. package/scripts/lib/cli/commands/auto_brief.rb +44 -0
  41. package/scripts/lib/cli/commands/auto_lock.rb +60 -0
  42. package/scripts/lib/cli/commands/auto_report.rb +50 -0
  43. package/scripts/lib/cli/commands/auto_take.rb +25 -0
  44. package/scripts/lib/cli/commands/backup.rb +43 -0
  45. package/scripts/lib/cli/commands/checkout.rb +25 -0
  46. package/scripts/lib/cli/commands/continue.rb +66 -0
  47. package/scripts/lib/cli/commands/doctor.rb +20 -0
  48. package/scripts/lib/cli/commands/feedback.rb +40 -0
  49. package/scripts/lib/cli/commands/help.rb +69 -0
  50. package/scripts/lib/cli/commands/hook.rb +32 -0
  51. package/scripts/lib/cli/commands/index.rb +23 -0
  52. package/scripts/lib/cli/commands/install.rb +21 -0
  53. package/scripts/lib/cli/commands/installer_verb.rb +37 -0
  54. package/scripts/lib/cli/commands/intent.rb +19 -0
  55. package/scripts/lib/cli/commands/intent_answer.rb +37 -0
  56. package/scripts/lib/cli/commands/intent_command.rb +53 -0
  57. package/scripts/lib/cli/commands/intent_end.rb +61 -0
  58. package/scripts/lib/cli/commands/intent_new.rb +62 -0
  59. package/scripts/lib/cli/commands/intent_note.rb +43 -0
  60. package/scripts/lib/cli/commands/intent_rule.rb +36 -0
  61. package/scripts/lib/cli/commands/intent_show.rb +25 -0
  62. package/scripts/lib/cli/commands/intent_spec.rb +44 -0
  63. package/scripts/lib/cli/commands/intent_step.rb +43 -0
  64. package/scripts/lib/cli/commands/intent_verify.rb +26 -0
  65. package/scripts/lib/cli/commands/migrate.rb +16 -0
  66. package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
  67. package/scripts/lib/cli/commands/next.rb +49 -0
  68. package/scripts/lib/cli/commands/project.rb +19 -0
  69. package/scripts/lib/cli/commands/project_links.rb +42 -0
  70. package/scripts/lib/cli/commands/project_list.rb +20 -0
  71. package/scripts/lib/cli/commands/project_new.rb +72 -0
  72. package/scripts/lib/cli/commands/query.rb +31 -0
  73. package/scripts/lib/cli/commands/render.rb +25 -0
  74. package/scripts/lib/cli/commands/roadmap.rb +19 -0
  75. package/scripts/lib/cli/commands/roadmap_check.rb +44 -0
  76. package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
  77. package/scripts/lib/cli/commands/roadmap_next.rb +34 -0
  78. package/scripts/lib/cli/commands/roadmap_show.rb +45 -0
  79. package/scripts/lib/cli/commands/rollback.rb +20 -0
  80. package/scripts/lib/cli/commands/search.rb +60 -0
  81. package/scripts/lib/cli/commands/session.rb +18 -0
  82. package/scripts/lib/cli/commands/session_commit.rb +42 -0
  83. package/scripts/lib/cli/commands/session_handoff.rb +34 -0
  84. package/scripts/lib/cli/commands/session_summary.rb +35 -0
  85. package/scripts/lib/cli/commands/status.rb +68 -0
  86. package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
  87. package/scripts/lib/cli/commands/sync.rb +46 -0
  88. package/scripts/lib/cli/commands/uninstall.rb +20 -0
  89. package/scripts/lib/cli/commands/update.rb +20 -0
  90. package/scripts/lib/cli/commands/version.rb +53 -0
  91. package/scripts/lib/cli/frontier.rb +84 -0
  92. package/scripts/lib/cli/legacy.rb +50 -0
  93. package/scripts/lib/cli/output.rb +102 -0
  94. package/scripts/lib/cli/scope.rb +127 -0
  95. package/scripts/lib/cli/table.rb +64 -0
  96. package/scripts/lib/cli.rb +94 -0
  97. package/scripts/lib/compact_instructions.rb +8 -0
  98. package/scripts/lib/day_summary.rb +4 -3
  99. package/scripts/lib/doctor_core.rb +7 -32
  100. package/scripts/lib/doctor_session_ledger.rb +2 -1
  101. package/scripts/lib/feedback_report.rb +1 -1
  102. package/scripts/lib/graph_measure_models.rb +3 -1
  103. package/scripts/lib/index_entry.rb +9 -0
  104. package/scripts/lib/installer_core.rb +73 -19
  105. package/scripts/lib/intent_screen.rb +3 -3
  106. package/scripts/lib/lock.rb +2 -2
  107. package/scripts/lib/node_input.rb +3 -2
  108. package/scripts/lib/preflight.rb +4 -6
  109. package/scripts/lib/project_config.rb +2 -1
  110. package/scripts/lib/project_validator.rb +3 -2
  111. package/scripts/lib/qmd_sync.rb +8 -7
  112. package/scripts/lib/reference_archive.rb +45 -0
  113. package/scripts/lib/release_guard.rb +2 -0
  114. package/scripts/lib/report_screen.rb +4 -3
  115. package/scripts/lib/rlm/corpus.rb +13 -0
  116. package/scripts/lib/rlm/probe.rb +29 -0
  117. package/scripts/lib/rlm/query.rb +22 -0
  118. package/scripts/lib/roadmap_queue.rb +2 -2
  119. package/scripts/lib/roadmap_savepoint.rb +1 -1
  120. package/scripts/lib/runner_absorb.rb +3 -2
  121. package/scripts/lib/search_index.rb +55 -0
  122. package/scripts/lib/session_git.rb +4 -3
  123. package/scripts/lib/sqlite.rb +22 -0
  124. package/scripts/lib/store_discovery.rb +7 -6
  125. package/scripts/lib/store_layout.rb +54 -0
  126. package/scripts/lib/store_provisioning.rb +2 -1
  127. package/scripts/lib/store_sync.rb +85 -0
  128. package/scripts/lib/stores_move.rb +93 -0
  129. package/scripts/lib/verify_intent.rb +2 -7
  130. package/scripts/lib/version_number.rb +48 -0
  131. package/scripts/lib/work_graph.rb +59 -0
  132. package/scripts/lib/worktree.rb +3 -8
  133. package/scripts/lib/worktree_sweep.rb +3 -2
  134. package/scripts/link-suggest +2 -1
  135. package/scripts/migrate-to-global +1 -1
  136. package/scripts/new-intent +3 -12
  137. package/scripts/plastic-lock +3 -2
  138. package/scripts/promote-session-item +3 -2
  139. package/scripts/release-check +10 -5
  140. package/scripts/report-screen +1 -1
  141. package/scripts/session-commit +2 -1
  142. package/scripts/spawn-preamble +2 -2
  143. package/scripts/update.rb +25 -4
  144. package/scripts/write-handoff +2 -1
  145. package/templates/agents.md +6 -6
  146. package/templates/render.css +10 -0
  147. package/bin/plastic.js +0 -70
  148. package/skills/agent-advisor/SKILL.md +0 -84
  149. package/skills/auto/SKILL.md +0 -297
  150. package/skills/auto/evals/evals.json +0 -255
  151. package/skills/auto/references/end-tail.md +0 -64
  152. package/skills/conventions/SKILL.md +0 -29
  153. package/skills/dashboard/SKILL.md +0 -180
  154. package/skills/dashboard/evals/evals.json +0 -38
  155. package/skills/dashboard/references/classification.md +0 -22
  156. package/skills/dashboard/templates/dashboard-global.md +0 -20
  157. package/skills/dashboard/templates/dashboard-project.md +0 -19
  158. package/skills/direct/SKILL.md +0 -66
  159. package/skills/direct/references/request-signals.md +0 -59
  160. package/skills/doctor/SKILL.md +0 -305
  161. package/skills/doctor/report.md +0 -102
  162. package/skills/feedback/SKILL.md +0 -98
  163. package/skills/feedback/references/transport-and-privacy.md +0 -65
  164. package/skills/feedback/report.md +0 -36
  165. package/skills/install/SKILL.md +0 -215
  166. package/skills/intent-continuing/SKILL.md +0 -156
  167. package/skills/intent-continuing/references/board-fill.md +0 -52
  168. package/skills/intent-continuing/references/boarding-matrix.md +0 -35
  169. package/skills/intent-continuing/references/context-management.md +0 -28
  170. package/skills/intent-continuing/references/liveness-ranking.md +0 -57
  171. package/skills/intent-creating/SKILL.md +0 -89
  172. package/skills/intent-creating/evals/evals.json +0 -72
  173. package/skills/intent-creating/references/lifecycle.md +0 -81
  174. package/skills/intent-creating/references/wikilinks.md +0 -8
  175. package/skills/intent-ending/SKILL.md +0 -182
  176. package/skills/intent-ending/evals/evals.json +0 -74
  177. package/skills/intent-executing/SKILL.md +0 -87
  178. package/skills/intent-executing/evals/evals.json +0 -66
  179. package/skills/intent-executing/implementer-prompt.md +0 -47
  180. package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
  181. package/skills/intent-speccing/SKILL.md +0 -136
  182. package/skills/intent-speccing/evals/evals.json +0 -126
  183. package/skills/intent-speccing/references/design-principles.md +0 -44
  184. package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
  185. package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
  186. package/skills/project-creating/SKILL.md +0 -162
  187. package/skills/project-creating/references/hubs-projects.md +0 -55
  188. package/skills/project-creating/references/project-scaffolding.md +0 -97
  189. package/skills/releasing/SKILL.md +0 -376
  190. package/skills/releasing/references/deprecations.md +0 -60
  191. package/skills/releasing/references/promotion-and-tagging.md +0 -70
  192. package/skills/releasing/references/release-lines.md +0 -105
  193. package/skills/roadmap/SKILL.md +0 -90
  194. package/skills/roadmap/references/file-format.md +0 -134
  195. package/skills/roadmap/references/operations.md +0 -112
  196. package/skills/rollback/SKILL.md +0 -91
  197. package/skills/tutorial/SKILL.md +0 -66
  198. package/skills/tutorial/evals/evals.json +0 -186
  199. package/skills/uninstall/SKILL.md +0 -75
  200. package/skills/update/SKILL.md +0 -126
  201. /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
  202. /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
  203. /package/{skills/conventions/references → docs/help}/knowledge-graph.md +0 -0
  204. /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
  205. /package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +0 -0
  206. /package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +0 -0
  207. /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "store_layout"
5
+ require_relative "sqlite"
6
+ require_relative "lock"
7
+
8
+ module Plastic
9
+ class StoresMove
10
+ Refused = Class.new(StandardError)
11
+
12
+ GLOBAL_ENTRIES = %w[store INDEX.md roadmaps].freeze
13
+ PATH_COLUMNS = {"knowledge_graph.db" => %w[doc path], "references.db" => %w[sqlar name]}.freeze
14
+ LOCKS = "{store,projects/*/store}/*/delivery.lock"
15
+
16
+ def initialize(home, qmd_index:, dry_run: false)
17
+ @home = home
18
+ @qmd_index = qmd_index
19
+ @dry_run = dry_run
20
+ end
21
+
22
+ def call
23
+ refuse
24
+ plan = [["copy", copy]] + moves.map { |from, to| ["move", "#{from} -> #{to}"] } + rewrites.map { |file| ["rewrite", file] }
25
+ run unless @dry_run
26
+ plan
27
+ end
28
+
29
+ def copy
30
+ "#{@home}-before-stores-move"
31
+ end
32
+
33
+ private
34
+
35
+ def refuse
36
+ raise Refused, "#{File.join(@home, "stores")}/ exists, so this home has moved already" if StoreLayout.moved?(@home)
37
+ raise Refused, "a delivery lock is held: #{held.join(", ")}" unless held.empty?
38
+ raise Refused, "#{copy} exists; remove it or move it away first" if File.exist?(copy)
39
+ end
40
+
41
+ def held
42
+ Dir.glob(LOCKS, base: @home).select { |lock| Time.now - File.mtime(File.join(@home, lock)) < Lock::TTL_SECONDS }
43
+ end
44
+
45
+ def moves
46
+ @moves ||= GLOBAL_ENTRIES.select { |name| File.exist?(File.join(@home, name)) }.map { |name| [name, "stores/global/#{name}"] } +
47
+ project_entries.map { |entry| ["projects/#{entry}", "stores/#{entry}"] }
48
+ end
49
+
50
+ def project_entries
51
+ Dir.exist?(projects) ? Dir.children(projects).sort : []
52
+ end
53
+
54
+ def projects
55
+ File.join(@home, "projects")
56
+ end
57
+
58
+ def rewrites
59
+ [File.join(@home, "config.yml"), @qmd_index, *PATH_COLUMNS.keys.map { |name| File.join(@home, name) }].select { |file| File.file?(file) }
60
+ end
61
+
62
+ def run
63
+ files = rewrites
64
+ FileUtils.cp_r(@home, copy, preserve: true)
65
+ moves.each { |from, to| move(from, to) }
66
+ FileUtils.rmdir(projects) if Dir.exist?(projects)
67
+ files.each { |file| PATH_COLUMNS.key?(File.basename(file)) ? rewrite_rows(file) : rewrite_text(file) }
68
+ end
69
+
70
+ def move(from, to)
71
+ FileUtils.mkdir_p(File.dirname(File.join(@home, to)))
72
+ FileUtils.mv(File.join(@home, from), File.join(@home, to))
73
+ end
74
+
75
+ def rewrite_text(file)
76
+ FileUtils.cp(file, "#{file}.before-stores-move", preserve: true)
77
+ File.write(file, [@home, "~/.plastic"].inject(File.read(file)) { |text, base| relocate(text, base) })
78
+ end
79
+
80
+ def relocate(text, base)
81
+ text.gsub(%r{#{Regexp.escape(base)}/(store|roadmaps|INDEX\.md)\b}) { "#{base}/stores/global/#{Regexp.last_match(1)}" }
82
+ .gsub(%r{#{Regexp.escape(base)}/projects\b}) { "#{base}/stores" }
83
+ end
84
+
85
+ def rewrite_rows(file)
86
+ table, column = PATH_COLUMNS.fetch(File.basename(file))
87
+ Sqlite.call(file, <<~SQL)
88
+ UPDATE #{table} SET #{column} = 'stores/' || substr(#{column}, 10) WHERE #{column} LIKE 'projects/%';
89
+ UPDATE #{table} SET #{column} = 'stores/global/' || #{column} WHERE #{column} LIKE 'store/%';
90
+ SQL
91
+ end
92
+ end
93
+ end
@@ -1,6 +1,7 @@
1
1
  # encoding: UTF-8
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative "store_layout"
4
5
  require_relative "worktree"
5
6
  require_relative "scaffold_intent"
6
7
 
@@ -96,13 +97,7 @@ module VerifyIntent
96
97
  # shapes are told apart by whether store's grandparent is literally named "projects" (the
97
98
  # StoreDiscovery convention), never by hardcoding ".plastic" as a name.
98
99
  def resolve_plastic_home_and_scope(store)
99
- parent = File.dirname(store)
100
- grandparent = File.dirname(parent)
101
- if File.basename(grandparent) == "projects"
102
- [File.dirname(grandparent), "project:#{File.basename(parent)}"]
103
- else
104
- [parent, "global"]
105
- end
100
+ Plastic::StoreLayout.home_and_scope(store)
106
101
  end
107
102
 
108
103
  # --- check 1: doctor, scoped to the intent --------------------------------------
@@ -0,0 +1,48 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # VersionNumber - dotted version comparison without RubyGems (intent 363).
5
+ #
6
+ # The launcher starts as `ruby --disable-gems`, which saves about 26 of the 40
7
+ # milliseconds a plain Ruby process costs, so no file a command loads may reach
8
+ # for Gem::Version. Parses the leading run of dotted integers, ignoring a
9
+ # leading "v" and anything after the digits, and compares two of them segment by
10
+ # segment with the shorter one zero-padded. A string with no leading integer
11
+ # parses to nil, so a caller answers "not found" instead of raising.
12
+ #
13
+ # It deliberately does not order pre-release suffixes: 2.0.0-alpha.27 and 2.0.0
14
+ # compare equal here. A caller that needs RubyGems' pre-release order, such as
15
+ # the release-tag reader in scripts/report-screen, keeps Gem::Version.
16
+ class VersionNumber
17
+ include Comparable
18
+
19
+ LEADING_DIGITS = /\A(\d+(?:\.\d+)*)/
20
+
21
+ attr_reader :segments
22
+
23
+ def self.parse(value)
24
+ match = LEADING_DIGITS.match(value.to_s.strip.sub(/\Av/, ""))
25
+ match && new(match[1].split(".").map(&:to_i))
26
+ end
27
+
28
+ def initialize(segments)
29
+ @segments = segments
30
+ end
31
+
32
+ def <=>(other)
33
+ return nil unless other.is_a?(VersionNumber)
34
+
35
+ width = [segments.length, other.segments.length].max
36
+ padded(width) <=> other.padded(width)
37
+ end
38
+
39
+ def to_s
40
+ segments.join(".")
41
+ end
42
+
43
+ protected
44
+
45
+ def padded(width)
46
+ segments + Array.new(width - segments.length, 0)
47
+ end
48
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "sqlite"
5
+ require_relative "index_entry"
6
+ require_relative "store_discovery"
7
+
8
+ module Plastic
9
+ module WorkGraph
10
+ NAME = "work_graph.db"
11
+ SCHEMA = <<~SQL
12
+ CREATE TABLE intent(id TEXT, store TEXT, slug TEXT, status TEXT, title TEXT);
13
+ CREATE TABLE ledger(intent_id TEXT, store TEXT, at TEXT, stage TEXT, text TEXT);
14
+ SQL
15
+ STATUSES = %w[active future completed abandoned].freeze
16
+ LEDGER_LINE = /\A(\S+)\s{2,}(\S+)\s{2,}(.+)\z/
17
+
18
+ def self.path(home)
19
+ File.join(home, NAME)
20
+ end
21
+
22
+ def self.build(home)
23
+ draft = "#{path(home)}.tmp"
24
+ FileUtils.rm_f(draft)
25
+ rows = StoreDiscovery.discover(home)[:stores].flat_map { |store| intents(store) + ledger(store) }
26
+ Sqlite.call(draft, [SCHEMA, "BEGIN;", *rows, "COMMIT;"].join("\n"))
27
+ File.rename(draft, path(home))
28
+ end
29
+
30
+ def self.intents(store)
31
+ return [] unless File.exist?(store[:index])
32
+
33
+ status = nil
34
+ File.readlines(store[:index], chomp: true, encoding: "UTF-8").filter_map do |line|
35
+ status = line.delete_prefix("## ").downcase if line.start_with?("## ")
36
+ entry = STATUSES.include?(status) && IndexEntry.match(line)
37
+ entry && insert("intent", entry[1], store[:slug], File.basename(File.dirname(entry[3])).split("--", 2).last, status, entry[2])
38
+ end
39
+ end
40
+
41
+ def self.ledger(store)
42
+ Dir.glob("*/savepoint.md", base: store[:store]).sort.flat_map do |file|
43
+ id = file.split("--").first
44
+ File.readlines(File.join(store[:store], file), chomp: true, encoding: "UTF-8").filter_map do |line|
45
+ entry = line.match(LEDGER_LINE)
46
+ entry && insert("ledger", id, store[:slug], entry[1], entry[2], entry[3])
47
+ end
48
+ end
49
+ end
50
+
51
+ def self.insert(table, *values)
52
+ "INSERT INTO #{table} VALUES(#{values.map { |value| Sqlite.quote(value.to_s) }.join(", ")});"
53
+ end
54
+
55
+ def self.known(home)
56
+ Sqlite.call(path(home), "SELECT store, id FROM intent;", readonly: true).map { |row| "#{row["store"]}:#{row["id"]}" }
57
+ end
58
+ end
59
+ end
@@ -1,6 +1,7 @@
1
1
  # encoding: UTF-8
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative "store_layout"
4
5
  require "json"
5
6
  require "yaml"
6
7
  require "socket"
@@ -377,14 +378,8 @@ module Worktree
377
378
  # (no project repo). Mirrors qmd_sync's slug_for_store fallback.
378
379
  def slug_for_store(store_dir, home: Dir.home)
379
380
  return nil if blank?(store_dir)
380
- plastic_home = File.expand_path(File.join(home, ".plastic"))
381
- store_dir = File.expand_path(store_dir)
382
- return nil if store_dir == File.join(plastic_home, "store")
383
-
384
- parts = store_dir.split(File::SEPARATOR)
385
- idx = parts.rindex("projects")
386
- return parts[idx + 1] if idx && parts[idx + 1] && parts[idx + 2] == "store"
387
- nil
381
+ slug = Plastic::StoreLayout.locate(store_dir).last
382
+ (slug == Plastic::StoreLayout::GLOBAL) ? nil : slug
388
383
  end
389
384
 
390
385
  # Best-effort slug for the worktree dir-name from an intent dir/store path:
@@ -1,6 +1,7 @@
1
1
  # encoding: UTF-8
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative "store_layout"
4
5
  require_relative "worktree"
5
6
  require_relative "index_entry"
6
7
 
@@ -55,8 +56,8 @@ module WorktreeSweep
55
56
  # Matching on the full name, not just the numeric id, sidesteps id
56
57
  # collisions across projects (ids are only unique WITHIN one store).
57
58
  def resolve_intent_dir(plastic_home, name)
58
- candidates = [File.join(plastic_home, "store", name)] +
59
- Dir.glob(File.join(plastic_home, "projects", "*", "store", name))
59
+ candidates = [File.join(Plastic::StoreLayout.global_store(plastic_home), name)] +
60
+ Dir.glob(File.join(Plastic::StoreLayout.projects_root(plastic_home), "*", "store", name))
60
61
  candidates.find { |d| Dir.exist?(d) }
61
62
  end
62
63
 
@@ -26,6 +26,7 @@
26
26
  # nothing. --edge is explicit and required (sources is decided by origin, never
27
27
  # inferred); --rating is required when --edge is chain.
28
28
 
29
+ require_relative "lib/store_layout"
29
30
  require_relative "lib/link_suggestions"
30
31
 
31
32
  module LinkSuggestCLI
@@ -69,7 +70,7 @@ module LinkSuggestCLI
69
70
  i += 1
70
71
  end
71
72
  end
72
- opts[:store_dir] ||= File.join(opts[:home], "store")
73
+ opts[:store_dir] ||= Plastic::StoreLayout.global_store(opts[:home])
73
74
  opts
74
75
  end
75
76
 
@@ -17,7 +17,7 @@ GLOBAL_ROOT="$HOME/.plastic"
17
17
  LOCAL_PLASTIC="$PROJECT_PATH/.plastic"
18
18
 
19
19
  if [ ! -d "$GLOBAL_ROOT" ]; then
20
- echo "Error: Global Plastic not installed. Run /plastic-install first." >&2
20
+ echo "Error: Global Plastic not installed. Run 'plastic install' first." >&2
21
21
  exit 1
22
22
  fi
23
23
 
@@ -35,6 +35,7 @@
35
35
  # exactly one line, `created <dir>` or `joined <dir>`; exit 0 either way,
36
36
  # exit 1 on bad input or failed self-validation.
37
37
 
38
+ require_relative "lib/store_layout"
38
39
  require "fileutils"
39
40
  require "date"
40
41
  require_relative "lib/savepoint"
@@ -167,17 +168,7 @@ end
167
168
  # [nil, "global"] only if the layout is unrecognized, in which case Links
168
169
  # projection falls back to single-store resolution rooted at this store.
169
170
  def store_context(store)
170
- store = File.expand_path(store)
171
- parent = File.dirname(store) # `<home>` or `<home>/projects/<slug>`
172
- if File.basename(store) == "store" && File.basename(File.dirname(parent)) == "projects"
173
- slug = File.basename(parent)
174
- home = File.dirname(File.dirname(parent)) # strip projects/<slug>
175
- [home, "project:#{slug}"]
176
- elsif File.basename(store) == "store"
177
- [parent, "global"] # `<home>/store`
178
- else
179
- [parent, "global"]
180
- end
171
+ Plastic::StoreLayout.home_and_scope(store)
181
172
  end
182
173
 
183
174
  # The in-scope stores under `plastic_home`, each { key:, store: }. Delegates to
@@ -283,7 +274,7 @@ def run_tmp(opts)
283
274
  abort "new-intent: --sources is not valid with --tmp" unless opts[:sources].empty?
284
275
  abort "new-intent: --tags is not valid with --tmp" unless opts[:tags].empty?
285
276
 
286
- store = opts[:store] ? expand(opts[:store]) : expand(File.join(ENV.fetch("PLASTIC_HOME", "~/.plastic"), "store"))
277
+ store = opts[:store] ? expand(opts[:store]) : Plastic::StoreLayout.global_store(expand(ENV.fetch("PLASTIC_HOME", "~/.plastic")))
287
278
  templates = opts[:templates] ? expand(opts[:templates]) : default_templates_dir
288
279
  abort "new-intent: templates dir not found: #{templates}" unless Dir.exist?(templates)
289
280
 
@@ -38,6 +38,7 @@
38
38
  # file; that path was retired in 2.0 (intent 307) and 344 G11.
39
39
  # Exit 0 on success/report; exit 1 when the verb is blocked.
40
40
 
41
+ require_relative "lib/store_layout"
41
42
  require "json"
42
43
  require "yaml"
43
44
  require_relative "lib/lock"
@@ -96,7 +97,7 @@ if dir.nil? || dir.strip.empty?
96
97
  exit 1
97
98
  end
98
99
  roots = ActiveDelivery.project_roots(plastic_home)
99
- global_store = File.join(plastic_home, "store")
100
+ global_store = Plastic::StoreLayout.global_store(plastic_home)
100
101
  dir = ActiveDelivery.resolve(global_store: global_store, project_roots: roots, session: session)
101
102
  if dir.nil?
102
103
  candidates = ActiveDelivery.candidate_intent_dirs(global_store: global_store, project_roots: roots).uniq
@@ -188,7 +189,7 @@ when "arm"
188
189
  end
189
190
  when "status"
190
191
  lock = Lock.read(dir)
191
- resolved = ActiveDelivery.resolve(global_store: File.join(plastic_home, "store"),
192
+ resolved = ActiveDelivery.resolve(global_store: Plastic::StoreLayout.global_store(plastic_home),
192
193
  project_roots: ActiveDelivery.project_roots(plastic_home),
193
194
  session: key)
194
195
  delivering = !resolved.nil? && File.expand_path(resolved) == File.expand_path(dir)
@@ -21,6 +21,7 @@
21
21
  # Exit codes: 0 promoted (prints the intent dir last), 1 new-intent failed,
22
22
  # 2 usage error or nothing to promote.
23
23
 
24
+ require_relative "lib/store_layout"
24
25
  require "open3"
25
26
  require "yaml"
26
27
  require_relative "lib/session_ledger"
@@ -61,7 +62,7 @@ usage_abort("--day must be a real calendar date, YYYYMMDD") unless SessionLedger
61
62
  usage_abort("--match is required") if opts[:match].nil? || opts[:match].empty?
62
63
 
63
64
  plastic_home = File.expand_path(opts[:plastic_home] || ENV.fetch("PLASTIC_HOME", "~/.plastic"))
64
- store = opts[:store] || File.join(plastic_home, "store")
65
+ store = opts[:store] || Plastic::StoreLayout.global_store(plastic_home)
65
66
  templates = opts[:templates] || File.expand_path("../templates", __dir__)
66
67
  session = opts[:session] ? SessionLedger.short_session_id(opts[:session]) : nil
67
68
 
@@ -69,7 +70,7 @@ target_store = store
69
70
  if opts[:project]
70
71
  projects = StoreProvisioning.load_projects(plastic_home)
71
72
  usage_abort("--project #{opts[:project]} is not a registered project") unless projects.key?(opts[:project])
72
- target_store = File.join(plastic_home, "projects", opts[:project], "store")
73
+ target_store = File.join(Plastic::StoreLayout.project_root(plastic_home, opts[:project]), "store")
73
74
  end
74
75
 
75
76
  # --- pick the target line ---------------------------------------------------
@@ -13,7 +13,7 @@
13
13
  # suite.
14
14
  #
15
15
  # Usage:
16
- # release-check --tag <tag> --npm-version <version>
16
+ # release-check (--tag <tag> | --branch <branch>) --npm-version <version>
17
17
  # [--github-output <path>] [--root <dir>]
18
18
  #
19
19
  # Exit codes: 0 (clean), 1 (violations; reported on stderr, one per line,
@@ -24,10 +24,10 @@ require "json"
24
24
  require_relative "lib/release_guard"
25
25
 
26
26
  NPM_FLOOR = [11, 5, 1].freeze
27
- KNOWN_FLAGS = %w[--tag --npm-version --github-output --root].freeze
27
+ KNOWN_FLAGS = %w[--tag --branch --npm-version --github-output --root].freeze
28
28
 
29
29
  def usage
30
- warn "usage: release-check --tag <tag> --npm-version <version> [--github-output <path>] [--root <dir>]"
30
+ warn "usage: release-check (--tag <tag> | --branch <branch>) --npm-version <version> [--github-output <path>] [--root <dir>]"
31
31
  exit 2
32
32
  end
33
33
 
@@ -54,7 +54,7 @@ rescue Errno::ENOENT, JSON::ParserError
54
54
  end
55
55
 
56
56
  opts = parse_args(ARGV)
57
- usage unless opts["--tag"] && opts["--npm-version"]
57
+ usage unless (opts["--tag"] || opts["--branch"]) && opts["--npm-version"]
58
58
 
59
59
  root = opts["--root"] ? File.expand_path(opts["--root"]) : File.expand_path("..", __dir__)
60
60
  package_json_path = File.join(root, "package.json")
@@ -66,16 +66,20 @@ violations = []
66
66
  canonical_version = read_version(package_json_path)
67
67
  violations << "could not read a version from #{package_json_path}" if canonical_version.nil?
68
68
 
69
- tag = opts["--tag"].sub(%r{\Arefs/tags/}, "")
69
+ branch = opts["--branch"]
70
70
  dist_tag = nil
71
71
 
72
72
  if canonical_version
73
73
  expected_tag = "v#{canonical_version}"
74
+ tag = opts.fetch("--tag", expected_tag).sub(%r{\Arefs/tags/}, "")
74
75
  if tag != expected_tag
75
76
  violations << "tag #{tag} does not match package.json version #{canonical_version} (expected #{expected_tag})"
76
77
  end
77
78
 
78
79
  dist_tag = ReleaseGuard.dist_tag(canonical_version)
80
+ if branch && ReleaseGuard::CHANNELS[branch] != dist_tag
81
+ violations << "branch #{branch} publishes #{ReleaseGuard::CHANNELS[branch] || "nothing"}, and version #{canonical_version} is #{dist_tag}"
82
+ end
79
83
  stable = dist_tag == "latest"
80
84
 
81
85
  result = ReleaseGuard.check(
@@ -107,6 +111,7 @@ if violations.empty?
107
111
  File.open(opts["--github-output"], "a") do |f|
108
112
  f.puts "version=#{canonical_version}"
109
113
  f.puts "dist_tag=#{dist_tag}"
114
+ f.puts "tag=#{tag}"
110
115
  end
111
116
  end
112
117
  puts "OK: tag #{tag}, version #{canonical_version}, dist-tag #{dist_tag}"
@@ -63,7 +63,7 @@ end
63
63
  def resolve_repo(intent_dir, explicit_repo, script_dir, include_self: true)
64
64
  candidates = []
65
65
  candidates << File.expand_path(explicit_repo) if explicit_repo
66
- if (m = intent_dir.match(%r{\A(.*)/projects/([^/]+)/store/[^/]+\z}))
66
+ if (m = intent_dir.match(%r{\A(.*)/(?:projects|stores)/([^/]+)/store/[^/]+\z}))
67
67
  home, slug = m[1], m[2]
68
68
  projects_yml = File.join(home, "projects.yml")
69
69
  if File.exist?(projects_yml)
@@ -18,6 +18,7 @@
18
18
  # 0 - always, except a usage error
19
19
  # 2 - usage error (missing --cwd or --summary); nothing is written
20
20
 
21
+ require_relative "lib/store_layout"
21
22
  require "fileutils"
22
23
  require_relative "lib/session_git"
23
24
  require_relative "lib/session_ledger"
@@ -78,7 +79,7 @@ def main(argv)
78
79
  usage_abort("--summary is required") if opts[:summary].nil?
79
80
 
80
81
  plastic_home = expand(opts[:plastic_home] || ENV.fetch("PLASTIC_HOME", "~/.plastic"))
81
- store = opts[:store] ? expand(opts[:store]) : File.join(plastic_home, "store")
82
+ store = opts[:store] ? expand(opts[:store]) : Plastic::StoreLayout.global_store(plastic_home)
82
83
  cwd = expand(opts[:cwd])
83
84
 
84
85
  day = opts[:day] || SessionLedger.day_id
@@ -44,7 +44,7 @@ WORKTREE_FALLBACK_INSTRUCTION =
44
44
  "directory, cd directly to the code worktree path above before editing project code."
45
45
 
46
46
  # Verbatim completion-report contract (intent 74). Kept as one constant so the
47
- # contract doc (skills/auto/references/agent-report-contract.md), the role prompts,
47
+ # contract doc (docs/help/agent-report-contract.md), the role prompts,
48
48
  # and the test assert against the exact same string. SINGLE SOURCE OF TRUTH for the
49
49
  # report wording: the work output is lifecycle artifacts, the final message is a
50
50
  # structured report about them. Both are required and they do not contradict.
@@ -64,7 +64,7 @@ REPORT_CONTRACT =
64
64
  "depends on the discovering session's file-write access. Keep the report " \
65
65
  "prose-stripped: the envelope and payload only, no greeting, no preamble, " \
66
66
  "no end-recap, no restating of the task; reasoning stays in the thinking channel. " \
67
- "See skills/auto/references/agent-report-contract.md for the per-role format."
67
+ "See docs/help/agent-report-contract.md for the per-role format."
68
68
 
69
69
  def parse_args(argv)
70
70
  role = nil
package/scripts/update.rb CHANGED
@@ -231,12 +231,33 @@ class Update < InstallerCore
231
231
  ($stdin.gets&.strip || "").downcase.start_with?("y")
232
232
  end
233
233
 
234
- # Thin npx-exec glue (not unit-tested; the decision above is). Delegates the file-sync to
235
- # the target version's install verb, recording the ledger action as `update`.
236
- def perform_switch(target, agent_flags)
234
+ # Thin npx-exec glue (the decision above is unit-tested). Delegates the file-sync to
235
+ # the target version's install verb, recording the ledger action as `update`. On
236
+ # success, commits the re-synced core files and clears the update-check cache
237
+ # (former update skill, lines 124-125).
238
+ def perform_switch(target, agent_flags, switch_runner: ->(cmd) { system(*cmd) })
237
239
  cmd = ["npx", "#{PKG}@#{target}", "install", "--reinstall", "--ledger-action", "update", *agent_flags]
238
240
  puts " $ #{cmd.join(" ")}"
239
- system(*cmd) ? 0 : 1
241
+ return 1 unless switch_runner.call(cmd)
242
+
243
+ commit_core_files(target)
244
+ clear_update_check_cache
245
+ 0
246
+ end
247
+
248
+ # Intent 372 (former update skill, lines 124-125): `~/.plastic` is its own git
249
+ # repository (install.rb's git_init_if_absent), so a successful update commits the
250
+ # core files it just re-synced. Empty commits are allowed: a same-version repair
251
+ # re-syncs nothing new, and that is still worth a ledger-adjacent commit.
252
+ def commit_core_files(target, runner: ->(cmd) { system(*cmd) })
253
+ runner.call(["git", "-C", plastic_home, "add", "PLASTIC.md", "scripts", "AGENTS.md", "VERSION",
254
+ "versions.json", "deprecations.yml", "config_asks.yml"])
255
+ runner.call(["git", "-C", plastic_home, "commit", "-m", "chore: update Plastic to #{target}",
256
+ "--allow-empty"])
257
+ end
258
+
259
+ def clear_update_check_cache
260
+ FileUtils.rm_f(File.join(plastic_home, ".cache", "update-check.json"))
240
261
  end
241
262
 
242
263
  def show_help
@@ -16,6 +16,7 @@
16
16
  #
17
17
  # Exit codes: 0 done; 2 usage error.
18
18
 
19
+ require_relative "lib/store_layout"
19
20
  require_relative "lib/session_ledger"
20
21
  require_relative "lib/handoff"
21
22
 
@@ -48,7 +49,7 @@ usage_abort("--trigger is required (#{Handoff::TRIGGERS.join(', ')})") unless op
48
49
  usage_abort("--trigger must be one of #{Handoff::TRIGGERS.join(', ')}") unless Handoff::TRIGGERS.include?(opts[:trigger])
49
50
 
50
51
  plastic_home = expand(ENV.fetch("PLASTIC_HOME", "~/.plastic"))
51
- store = opts[:store] ? expand(opts[:store]) : File.join(plastic_home, "store")
52
+ store = opts[:store] ? expand(opts[:store]) : Plastic::StoreLayout.global_store(plastic_home)
52
53
  templates = opts[:templates] ? expand(opts[:templates]) : File.expand_path("../templates", __dir__)
53
54
  usage_abort("templates dir not found: #{templates}") unless Dir.exist?(templates)
54
55
 
@@ -42,7 +42,7 @@ Active/Future/Completed placement is managed in INDEX.md, not in frontmatter.
42
42
 
43
43
  ## Creating Tactical Intents
44
44
 
45
- Create through the `plastic-intent-creating` skill, which scaffolds with one call:
45
+ Create through the `plastic intent new` command, which scaffolds with one call:
46
46
  `ruby ~/.plastic/scripts/new-intent --store .plastic/store --intent "<one-line>" --slug <slug> [--sources <governing-id>]`.
47
47
  It allocates the Folgezettel ID, creates `.plastic/store/ID--slug/ID--slug.md` born
48
48
  complete, and wires the links. Never hand-author the files: `new-intent` validates the
@@ -54,11 +54,11 @@ Plastic has its own lifecycle skills. When a Plastic skill exists for the curren
54
54
 
55
55
  | Phase | Skill | Produces |
56
56
  |-------|-------|----------|
57
- | What | `plastic-intent-creating` | Intent file |
58
- | Why | `plastic-intent-speccing` | Rulings as insights, `resources/*.md`, `spec.md` |
59
- | How | `plastic-intent-speccing` | `actions/`, `plan.md`, `checklist.md` |
60
- | Exec | `plastic-intent-executing` | Code + `outcome.md` |
61
- | End | `plastic-intent-ending` | Lifecycle transition |
57
+ | What | `plastic intent new` | Intent file |
58
+ | Why | `plastic intent spec` | Rulings as insights, `resources/*.md`, `spec.md` |
59
+ | How | `plastic intent spec` | `actions/`, `plan.md`, `checklist.md` |
60
+ | Exec | `plastic intent step` | Code + `outcome.md` |
61
+ | End | `plastic intent end` | Lifecycle transition |
62
62
 
63
63
  **Artifact convention:** ALL lifecycle artifacts go to the active intent directory (`store/{id}--{slug}/`). Never write specs to `docs/superpowers/specs/` or plans to `docs/superpowers/plans/`.
64
64
 
@@ -0,0 +1,10 @@
1
+ body { max-width: 46rem; margin: 2rem auto; padding: 0 1rem; font: 16px/1.6 system-ui, sans-serif; color: #1f2933; background: #f4f5f7; }
2
+ h1, h2, h3 { line-height: 1.25; }
3
+ table { border-collapse: collapse; display: block; overflow-x: auto; }
4
+ th, td { border: 1px solid #9aa5b1; padding: 0.3rem 0.6rem; text-align: left; }
5
+ pre { background: #e4e7eb; padding: 0.8rem; overflow-x: auto; }
6
+ code { font-family: ui-monospace, monospace; }
7
+ @media (prefers-color-scheme: dark) {
8
+ body { color: #e4e7eb; background: #1f2933; }
9
+ pre { background: #323f4b; }
10
+ }
package/bin/plastic.js DELETED
@@ -1,70 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // Thin shim — npx entry point that dispatches a subcommand to the matching Ruby verb script.
4
- // All logic lives in scripts/<verb>.rb. JS is only the distribution + dispatch mechanism.
5
- //
6
- // npx @zalom/plastic install [flags]
7
- // npx @zalom/plastic update [flags]
8
- // npx @zalom/plastic uninstall [flags]
9
- // npx @zalom/plastic rollback [flags]
10
- //
11
- // Back-compat: a bare `--uninstall` (no subcommand) routes to uninstall with a deprecation
12
- // warning; no subcommand at all defaults to install (legacy behaviour, one release).
13
-
14
- import { execFileSync } from 'node:child_process'
15
- import { resolve } from 'node:path'
16
- import { existsSync } from 'node:fs'
17
-
18
- const VERBS = ['install', 'update', 'uninstall', 'rollback']
19
- const packageRoot = new URL('..', import.meta.url).pathname
20
- const argv = process.argv.slice(2)
21
-
22
- let verb
23
- let rest
24
-
25
- if (VERBS.includes(argv[0])) {
26
- verb = argv[0]
27
- rest = argv.slice(1)
28
- } else if (argv.includes('--uninstall')) {
29
- // Deprecated: `--uninstall` as a flag instead of the `uninstall` subcommand.
30
- console.error('! plastic: `--uninstall` is deprecated — use `npx @zalom/plastic uninstall`. (works for now)')
31
- verb = 'uninstall'
32
- rest = argv.filter((a) => a !== '--uninstall')
33
- } else {
34
- // No subcommand: legacy default to install (one release of grace).
35
- verb = 'install'
36
- rest = argv
37
- }
38
-
39
- const script = resolve(packageRoot, 'scripts', `${verb}.rb`)
40
-
41
- if (!existsSync(script)) {
42
- console.error(`Error: scripts/${verb}.rb not found in package.`)
43
- process.exit(1)
44
- }
45
-
46
- try {
47
- execFileSync('ruby', [script, ...rest], {
48
- stdio: 'inherit',
49
- // Clear RUBYOPT last, after the spread, so a global setting (for example --yjit)
50
- // cannot reach a ruby that does not know the flag. A machine with an old ruby
51
- // must get preflight's real message, not a crash.
52
- env: { ...process.env, PLASTIC_PACKAGE_ROOT: packageRoot, RUBYOPT: '' },
53
- })
54
- } catch (err) {
55
- if (err.status) process.exit(err.status)
56
- // We only get here when ruby could not be executed at all. A ruby that runs but is
57
- // too old exits with a status, handled on the line above, and prints its own real
58
- // message from scripts/lib/preflight.rb naming the version actually found.
59
- if (err.code === 'ENOENT') {
60
- console.error('Plastic needs Ruby 3.0.0 or newer to run its scripts (ruby was not found on PATH).')
61
- console.error('Install a pinned Ruby with mise:')
62
- console.error(' curl https://mise.run | sh # only if mise is not installed yet')
63
- console.error(' mise use --global ruby@3.3')
64
- console.error('Then re-run the Plastic installer.')
65
- } else {
66
- console.error(`Plastic could not run ruby: ${err.message}`)
67
- console.error('Check that `ruby -v` works in this shell, then re-run the Plastic installer.')
68
- }
69
- process.exit(1)
70
- }