@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
@@ -10,6 +10,7 @@
10
10
  # lands, and calls the session-commit seam when that script exists. Always
11
11
  # exits 0 (spec D3).
12
12
 
13
+ require_relative "lib/store_layout"
13
14
  require "json"
14
15
  require "timeout"
15
16
  require "fileutils"
@@ -77,7 +78,7 @@ skip_day_ledger = inside_plastic || !intent_dir_abs.nil?
77
78
  # --- (c) promote the newest pending line, Item savepoint, seam -------------
78
79
  unless skip_day_ledger
79
80
  begin
80
- store = File.join(plastic_home, "store")
81
+ store = Plastic::StoreLayout.global_store(plastic_home)
81
82
  sid = SessionLedger.short_session_id(nil, session_id)
82
83
  today = SessionLedger.day_id
83
84
 
@@ -152,7 +153,7 @@ end
152
153
  # the heartbeat is not written and no directory is created here either --
153
154
  # the session tmp directory, created by session start, is the guard.
154
155
  begin
155
- store ||= File.join(plastic_home, "store")
156
+ store ||= Plastic::StoreLayout.global_store(plastic_home)
156
157
  sid ||= SessionLedger.short_session_id(nil, session_id)
157
158
  if Dir.exist?(SessionLedger.session_tmp_dir(store, sid))
158
159
  File.write(SessionLedger.heartbeat_path(store, sid), "#{Time.now.utc.iso8601}\n")
@@ -13,6 +13,7 @@
13
13
  #
14
14
  # Usage: hook-savepoint <plastic_home> (stdin: the hook JSON)
15
15
 
16
+ require_relative "lib/store_layout"
16
17
  require "json"
17
18
  require_relative "lib/session_ledger"
18
19
  require_relative "lib/handoff"
@@ -31,7 +32,7 @@ MESSAGE = "PLASTIC SAVEPOINT - context is being compacted. The hand-off for this
31
32
  def resolve_intent(plastic_home, session_id, read_config_path)
32
33
  return nil if session_id.strip.empty?
33
34
 
34
- global_store = File.join(plastic_home, "store")
35
+ global_store = Plastic::StoreLayout.global_store(plastic_home)
35
36
  roots_raw = IO.popen({ "PLASTIC_HOME" => plastic_home }, [read_config_path, "project_roots"],
36
37
  err: File::NULL, &:read).to_s.strip
37
38
  project_roots = roots_raw.empty? ? [] : Array(JSON.parse(roots_raw))
@@ -51,7 +52,7 @@ begin
51
52
 
52
53
  unless plastic_home.empty? || session_id.strip.empty?
53
54
  home = File.expand_path(plastic_home)
54
- store = File.join(home, "store")
55
+ store = Plastic::StoreLayout.global_store(home)
55
56
  session = SessionLedger.short_session_id(session_id, nil)
56
57
  day = Handoff.day_for(store, session, today: SessionLedger.day_id)
57
58
  read_config_path = File.expand_path("read-config", __dir__)
@@ -3,6 +3,7 @@
3
3
  # Usage: hook-session-start <index_path> <plastic_home> <mode> [plugin_root]
4
4
  # Outputs hook JSON for the session-start hook. Exits silently if nothing to show.
5
5
 
6
+ require_relative "lib/store_layout"
6
7
  require "json"
7
8
  require "date"
8
9
  require "yaml"
@@ -58,7 +59,7 @@ end
58
59
  # home (~/.plastic) as argument 2; the store lives one level below it. Compose the
59
60
  # store exactly once here, so no later line re-derives it and no path can gain a
60
61
  # doubled store/ segment.
61
- store_dir = File.join(plastic_home, "store")
62
+ store_dir = Plastic::StoreLayout.global_store(plastic_home)
62
63
 
63
64
  # --- Current version ---
64
65
  # Read early, before any of the richer INDEX/project parsing below, so the
@@ -154,7 +155,7 @@ begin
154
155
  project_path = File.expand_path(info["path"])
155
156
  if cwd.start_with?(project_path)
156
157
  current_project = { "slug" => slug, "parent" => info["parent"], "path" => project_path }
157
- project_index = "#{plastic_home}/projects/#{slug}/INDEX.md"
158
+ project_index = File.join(Plastic::StoreLayout.project_root(plastic_home, slug), "INDEX.md")
158
159
  if File.exist?(project_index)
159
160
  p_section = nil
160
161
  File.readlines(project_index).each do |pline|
@@ -220,7 +221,7 @@ begin
220
221
  if File.exist?(cache_file)
221
222
  cache = JSON.parse(File.read(cache_file)) rescue {}
222
223
  if cache["updateAvailable"]
223
- update_notice = "Plastic update available: #{cache["current"]} -> #{cache["latest"]} — run /plastic-update"
224
+ update_notice = "Plastic update available: #{cache["current"]} -> #{cache["latest"]} — run `plastic update`"
224
225
  end
225
226
  end
226
227
 
@@ -291,7 +292,7 @@ begin
291
292
  if plastic_md && !subagent_session
292
293
  if current_project
293
294
  slug = current_project["slug"]
294
- banner = "Project: #{slug} | Store: ~/.plastic/projects/#{slug}/store/"
295
+ banner = "Project: #{slug} | Store: #{File.join(Plastic::StoreLayout.project_root(plastic_home, slug), "store").sub(Dir.home, "~")}/"
295
296
  if project_active.any? && project_active.first =~ /\[([^\]]+)\].*store\/([\w-]+)\//
296
297
  intent_name, dir_name = $1, $2
297
298
  intent_id = dir_name.split("--").first
package/scripts/hook-stop CHANGED
@@ -14,6 +14,7 @@
14
14
  #
15
15
  # Usage: hook-stop <plastic_home> (stdin: the hook JSON)
16
16
 
17
+ require_relative "lib/store_layout"
17
18
  require "json"
18
19
  require_relative "lib/stop_gate"
19
20
 
@@ -32,7 +33,7 @@ begin
32
33
  payload = {} unless payload.is_a?(Hash)
33
34
 
34
35
  home = File.expand_path(plastic_home)
35
- global_store = File.join(home, "store")
36
+ global_store = Plastic::StoreLayout.global_store(home)
36
37
  read_config = File.expand_path("read-config", __dir__)
37
38
 
38
39
  config_stop_hook = read_config_value("runner.stop_hook", home, read_config)
@@ -16,8 +16,7 @@
16
16
  # ever reaches Insights.append_insight; the library itself stays untouched,
17
17
  # since a tag is a text-level convention, not a new field on the ledger.
18
18
  # Doctor's unpromoted_rules check (scripts/doctor.rb) later lists any tagged
19
- # entry whose exact text no skills/conventions/references/*.md chapter
20
- # carries yet.
19
+ # entry whose exact text no docs/help/*.md chapter carries yet.
21
20
  #
22
21
  # Usage:
23
22
  # insight-append <intent_dir> <text> --stage S --author A [--rule]
@@ -75,12 +75,14 @@ class Install < InstallerCore
75
75
  # they are reported, never re-installed, so the summary line and the Codex
76
76
  # trust reminder only ever count agents that actually changed this run.
77
77
  def run(selected:, force: false, reinstall: false, ledger_action: nil, argv: ARGV, input: $stdin,
78
- already_registered: [])
78
+ already_registered: [], qmd_runner: QmdSync.default_runner, qmd_detector: QmdSync.method(:detect))
79
79
  fresh = !installed?
80
80
  mode = fresh ? :install : :update # :update here means "re-sync, skip bootstrap"
81
81
 
82
82
  distribute(mode)
83
83
  bootstrap if fresh
84
+ git_init_if_absent
85
+ register_with_qmd(runner: qmd_runner, detector: qmd_detector)
84
86
  migrate_advisor_config_file(File.join(plastic_home, "config.yml"))
85
87
  apply_config_flags(argv)
86
88
 
@@ -60,7 +60,7 @@ module ActiveDelivery
60
60
  value.nil? || value.to_s.strip.empty?
61
61
  end
62
62
 
63
- DEFAULT_PROJECT_ROOTS = ["~/.plastic/projects"].freeze
63
+ DEFAULT_PROJECT_ROOTS = ["~/.plastic/projects", "~/.plastic/stores"].freeze
64
64
 
65
65
  # The configured project roots (344 n2, D6): the `project_roots` list in
66
66
  # `<plastic_home>/config.yml`, each expanded, or the default
@@ -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 "digest"
5
6
  require "fileutils"
6
7
  require_relative "lock"
@@ -63,7 +64,7 @@ module Arm
63
64
  end
64
65
 
65
66
  def global_store(home)
66
- File.join(File.expand_path(home), ".plastic", "store")
67
+ Plastic::StoreLayout.global_store(File.join(File.expand_path(home), ".plastic"))
67
68
  end
68
69
 
69
70
  # The minimal hash Worktree.provision, release, and finish consume: the
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "digest"
5
+ require "json"
6
+ require "tmpdir"
7
+ require "zlib"
8
+ require_relative "store_sync"
9
+ require_relative "worktree"
10
+
11
+ module Plastic
12
+ module Backup
13
+ DATABASES = [SearchIndex::NAME, WorkGraph::NAME, ReferenceArchive::NAME].freeze
14
+ ROOT_FILES = %w[config.yml projects.yml INDEX.md].freeze
15
+ Missing = Class.new(StandardError)
16
+
17
+ def self.dir(home)
18
+ File.join(home, "backups")
19
+ end
20
+
21
+ def self.list(home)
22
+ Dir.glob(File.join(dir(home), "plastic-backup--*.tar.gz")).sort
23
+ end
24
+
25
+ def self.call(home, version:, now: Time.now.utc)
26
+ missing = DATABASES.reject { |name| File.exist?(File.join(home, name)) }
27
+ raise Missing, missing.join(", ") unless missing.empty?
28
+
29
+ Worktree.ensure_gitignored(home, "backups/")
30
+ Dir.mktmpdir("plastic-backup") do |work|
31
+ DATABASES.each { |name| Sqlite.call(File.join(home, name), "VACUUM INTO #{Sqlite.quote(File.join(work, name))};", readonly: true) }
32
+ FileUtils.cp(ROOT_FILES.map { |name| File.join(home, name) }.select { |path| File.exist?(path) }, work)
33
+ File.write(File.join(work, "manifest.json"), JSON.pretty_generate(manifest(work, version, now)))
34
+ pack(work, File.join(dir(home), "plastic-backup--#{now.strftime("%Y%m%dT%H%M%SZ")}.tar.gz"))
35
+ end
36
+ end
37
+
38
+ def self.manifest(work, version, now)
39
+ files = Dir.children(work).sort.map do |name|
40
+ path = File.join(work, name)
41
+ {"name" => name, "bytes" => File.size(path), "sha256" => Digest::SHA256.file(path).hexdigest}
42
+ end
43
+ {"version" => version, "at" => now.strftime("%Y-%m-%dT%H:%M:%SZ"), "files" => files}
44
+ end
45
+
46
+ def self.pack(work, archive)
47
+ require "rubygems"
48
+ require "rubygems/package"
49
+ FileUtils.mkdir_p(File.dirname(archive))
50
+ draft = "#{archive}.tmp"
51
+ Zlib::GzipWriter.open(draft) do |gzip|
52
+ Gem::Package::TarWriter.new(gzip) do |tar|
53
+ Dir.children(work).sort.each do |name|
54
+ path = File.join(work, name)
55
+ tar.add_file_simple(name, 0o644, File.size(path)) { |entry| IO.copy_stream(path, entry) }
56
+ end
57
+ end
58
+ end
59
+ File.rename(draft, archive)
60
+ archive
61
+ ensure
62
+ FileUtils.rm_f(draft.to_s)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "output"
5
+ require_relative "scope"
6
+
7
+ module Plastic
8
+ class CLI
9
+ class Command
10
+ OK = 0
11
+ FAILED = 1
12
+ USAGE = 2
13
+ REFUSED = 3
14
+
15
+ Usage = Class.new(StandardError)
16
+ Refusal = Class.new(StandardError)
17
+ Failure = Class.new(StandardError)
18
+
19
+ def self.call(...)
20
+ command = new(...)
21
+ command.call
22
+ command.flush
23
+ OK
24
+ rescue OptionParser::ParseError, Scope::UnknownProject, Usage => e
25
+ command.output.usage(e.message, self::USAGE_LINE)
26
+ USAGE
27
+ rescue Refusal => e
28
+ command.output.refused(e.message)
29
+ REFUSED
30
+ rescue Failure => e
31
+ command.output.failed(e.message)
32
+ FAILED
33
+ end
34
+
35
+ attr_reader :output
36
+
37
+ def initialize(argv, out:, err:, env: ENV, home: Dir.home, directory: Dir.pwd)
38
+ @argv = argv.dup
39
+ @env = env
40
+ @home = home
41
+ @directory = directory
42
+ @output = Output.new(out: out, err: err)
43
+ end
44
+
45
+ def call
46
+ raise NoMethodError, "#{self.class} must define call"
47
+ end
48
+
49
+ def flush
50
+ @output.flush(json: options[:json])
51
+ end
52
+
53
+ private
54
+
55
+ def options
56
+ return @options if @options
57
+
58
+ @options = {}
59
+ parser.parse!(@argv)
60
+ @options
61
+ end
62
+
63
+ def arguments
64
+ options
65
+ @argv
66
+ end
67
+
68
+ def parser
69
+ @parser ||= OptionParser.new do |o|
70
+ o.banner = self.class::USAGE_LINE
71
+ o.on("--json") { @options[:json] = true }
72
+ switches(o)
73
+ end
74
+ end
75
+
76
+ def switches(_parser)
77
+ nil
78
+ end
79
+
80
+ def scope
81
+ @scope ||= Scope.new(env: @env, home: @home, slug: options[:project], directory: @directory)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "subcommand_list"
4
+
5
+ # `plastic auto` - the list of `plastic auto ...` subcommands, straight out of
6
+ # the table.
7
+ module Plastic
8
+ class CLI
9
+ module Commands
10
+ class Auto < SubcommandList
11
+ USAGE_LINE = "plastic auto SUBCOMMAND [options]"
12
+ PREFIX = "auto"
13
+ NEXT = "plastic auto take ID"
14
+ BECAUSE = "taking the intent is what every other auto subcommand needs first"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "intent_command"
4
+
5
+ # `plastic auto brief` - an intent's spawn preamble (`spawn-preamble`, run
6
+ # through Legacy). `--role advisor` also prints the three shapes an advisor
7
+ # request takes, so a dispatching lead never has to look them up.
8
+ module Plastic
9
+ class CLI
10
+ module Commands
11
+ class AutoBrief < IntentCommand
12
+ USAGE_LINE = "plastic auto brief ID [--role ROLE] [--json]"
13
+
14
+ SCRIPT = "spawn-preamble"
15
+ AFTER = "plastic auto report ID"
16
+ BECAUSE = "the report closes the turn the preamble opened"
17
+
18
+ ADVISOR_SHAPES =
19
+ "State the shape you need: a verdict plus the biggest risk for one bounded " \
20
+ "decision; a stepped plan plus a risk map for a plan or plan review; rival " \
21
+ "approaches and kill criteria for architecture, one-way doors, or deadlocks."
22
+
23
+ def call
24
+ super
25
+ @output.row("advisor request shapes", ADVISOR_SHAPES) if role == "advisor"
26
+ end
27
+
28
+ private
29
+
30
+ def role
31
+ options[:role]
32
+ end
33
+
34
+ def switches(parser)
35
+ parser.on("--role ROLE") { |v| @options[:role] = v }
36
+ end
37
+
38
+ def script_arguments
39
+ role ? [intent_dir, "--role", role] : [intent_dir]
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../command"
4
+ require_relative "../legacy"
5
+
6
+ # `plastic auto lock` - inspects, repairs, or releases an intent's delivery
7
+ # lock (`plastic-lock status|fix|release --intent-dir DIR`, run through
8
+ # Legacy). The verb comes first, then the id, since the verb is what the
9
+ # table's two-token match consumes as part of the command name.
10
+ module Plastic
11
+ class CLI
12
+ module Commands
13
+ class AutoLock < Command
14
+ USAGE_LINE = "plastic auto lock status|fix|release ID [--json]"
15
+
16
+ VERBS = %w[status fix release].freeze
17
+ AFTER = {"status" => "plastic auto lock fix ID", "fix" => "plastic auto brief ID",
18
+ "release" => "plastic session summary"}.freeze
19
+ BECAUSE = {"status" => "a stale or held lock is repaired by fix, not by re-arming",
20
+ "fix" => "a repaired lock is ready for the preamble again",
21
+ "release" => "the summary is what a freed lock leads to"}.freeze
22
+
23
+ def initialize(argv, runner: nil, **streams)
24
+ super(argv, **streams)
25
+ @runner = runner
26
+ end
27
+
28
+ def call
29
+ raise Usage, "VERB must be one of #{VERBS.join(", ")}" unless VERBS.include?(verb)
30
+
31
+ status = legacy.run("plastic-lock", verb, "--intent-dir", intent_dir)
32
+ raise Failure, "plastic-lock exited #{status}" unless status.zero?
33
+
34
+ @output.next_step(AFTER.fetch(verb).sub("ID", id.to_s), because: BECAUSE.fetch(verb))
35
+ end
36
+
37
+ private
38
+
39
+ def verb
40
+ arguments[0]
41
+ end
42
+
43
+ def id
44
+ arguments[1]
45
+ end
46
+
47
+ def intent_dir
48
+ dir = scope.intent_dir(id)
49
+ raise Failure, "no intent #{id.inspect} in #{scope.slug}; plastic status lists the ones that exist" unless dir
50
+
51
+ dir
52
+ end
53
+
54
+ def legacy
55
+ @legacy ||= Legacy.new(env: @env, runner: @runner)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "intent_command"
4
+
5
+ # `plastic auto report` - an intent's completion report (`agent-report`, run
6
+ # through Legacy), then the review-by-risk rules a lead checks it against.
7
+ module Plastic
8
+ class CLI
9
+ module Commands
10
+ class AutoReport < IntentCommand
11
+ USAGE_LINE = "plastic auto report ID [--role ROLE] [--json]"
12
+
13
+ SCRIPT = "agent-report"
14
+ AFTER = "plastic auto lock release ID"
15
+ BECAUSE = "closing the lock is what a delivered or abandoned report leads to"
16
+
17
+ REVIEW_RULES = <<~TEXT.chomp
18
+ Review by risk, boot 3, only when a rule fires; otherwise the green suite is
19
+ the review. Dispatch the post-execution reviewer when any of these holds:
20
+ 1. the diff touches a path on the risk list (hooks, the lock, the arming
21
+ module, the installer, a release file).
22
+ 2. a failure-mode matrix row names a test not in the diff, or a test the
23
+ green run did not execute.
24
+ 3. the completion report carries a status other than delivered, or a
25
+ non-empty deviations or blockers field.
26
+ The reviewer returns a pass or fixes; at most one review-fix round, ever.
27
+ TEXT
28
+
29
+ def call
30
+ super
31
+ @output.row("review rules", REVIEW_RULES)
32
+ end
33
+
34
+ private
35
+
36
+ def role
37
+ options[:role]
38
+ end
39
+
40
+ def switches(parser)
41
+ parser.on("--role ROLE") { |v| @options[:role] = v }
42
+ end
43
+
44
+ def script_arguments
45
+ role ? [intent_dir, "--role", role] : [intent_dir]
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "intent_command"
4
+
5
+ # `plastic auto take` - arms the intent's delivery lock for this session
6
+ # (`plastic-lock arm --intent-dir DIR --mode auto`, run through Legacy).
7
+ module Plastic
8
+ class CLI
9
+ module Commands
10
+ class AutoTake < IntentCommand
11
+ USAGE_LINE = "plastic auto take ID [--json]"
12
+
13
+ SCRIPT = "plastic-lock"
14
+ AFTER = "plastic auto brief ID"
15
+ BECAUSE = "the preamble is the live state a taken intent is read from next"
16
+
17
+ private
18
+
19
+ def script_arguments
20
+ ["arm", "--intent-dir", intent_dir, "--mode", "auto"]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../command"
4
+ require_relative "version"
5
+ require_relative "../../backup"
6
+
7
+ module Plastic
8
+ class CLI
9
+ module Commands
10
+ class Backup < Command
11
+ USAGE_LINE = "plastic backup [--list] [--json]"
12
+
13
+ def call
14
+ options[:list] ? list : write
15
+ rescue Plastic::Backup::Missing => e
16
+ @output.next_step("plastic sync", because: "a backup copies the three databases, and sync builds them")
17
+ raise Failure, "no #{e.message} at #{scope.plastic_home}; run plastic sync"
18
+ rescue Sqlite::Error => e
19
+ raise Failure, e.message
20
+ end
21
+
22
+ private
23
+
24
+ def switches(parser)
25
+ parser.on("--list") { options[:list] = true }
26
+ end
27
+
28
+ def write
29
+ version = Version.new([], out: nil, err: nil, env: @env).found.last
30
+ @output.row("archive", Plastic::Backup.call(scope.plastic_home, version: version))
31
+ @output.next_step("plastic backup --list", because: "to restore, unpack the archive into an empty ~/.plastic and run plastic checkout")
32
+ end
33
+
34
+ def list
35
+ archives = Plastic::Backup.list(scope.plastic_home)
36
+ archives.each { |path| @output.row(File.basename(path), "#{File.size(path)} bytes #{File.mtime(path).strftime("%Y-%m-%d %H:%M")}") }
37
+ @output.row("result", "no backup yet") if archives.empty?
38
+ @output.next_step("plastic backup", because: "copying backups/ off this machine is your choice; Plastic sends nothing")
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../command"
4
+ require_relative "../../store_sync"
5
+
6
+ module Plastic
7
+ class CLI
8
+ module Commands
9
+ class Checkout < Command
10
+ USAGE_LINE = "plastic checkout [--json]"
11
+
12
+ def call
13
+ home = scope.plastic_home
14
+ raise Failure, "no databases at #{home}; run plastic sync" unless File.exist?(ReferenceArchive.path(home))
15
+
16
+ restored = StoreSync.checkout(home)
17
+ @output.row("restored", restored.empty? ? "nothing was missing" : restored)
18
+ @output.next_step("plastic sync --dry-run", because: "checkout restores missing files only; a changed file is never overwritten")
19
+ rescue Sqlite::Error => e
20
+ raise Failure, e.message
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../command"
4
+ require_relative "../frontier"
5
+ require_relative "../scope"
6
+
7
+ # `plastic continue` - where one project stands and what runs next: the store it
8
+ # uses, the intents it has open, its liveliest roadmap and that roadmap's
9
+ # frontier, then the step to take and the rule behind it.
10
+ #
11
+ # With no roadmap the first active intent answers instead, so a project that
12
+ # never grew a roadmap still gets a next step.
13
+ module Plastic
14
+ class CLI
15
+ module Commands
16
+ class Continue < Command
17
+ USAGE_LINE = "plastic continue [--project SLUG] [--json]"
18
+
19
+ def call
20
+ @output.row("project", scope.slug)
21
+ @output.row("root", scope.root)
22
+ @output.row("active", active)
23
+ @output.row("roadmap", roadmap)
24
+ command, because = decision
25
+ @output.next_step(command, because: because)
26
+ rescue RoadmapSavepoint::MissingGroupingHeading => e
27
+ raise Failure, e.message
28
+ end
29
+
30
+ private
31
+
32
+ def switches(parser)
33
+ parser.on("--project SLUG") { |slug| @options[:project] = slug }
34
+ end
35
+
36
+ def active
37
+ scope.active_entries.map { |id, title| "#{id} #{title}" }
38
+ end
39
+
40
+ def roadmap
41
+ return Frontier::NONE unless frontier.roadmap
42
+
43
+ "#{frontier.roadmap} frontier #{frontier.heading}"
44
+ end
45
+
46
+ def decision
47
+ return [frontier.next_step, frontier.because] if frontier.id
48
+
49
+ id = scope.active_ids.first
50
+ return [plan_for(id), "#{id} is the first active intent in #{scope.slug}"] if id
51
+
52
+ ["plastic help", "#{scope.slug} has no active work and no roadmap"]
53
+ end
54
+
55
+ def plan_for(id)
56
+ dir = scope.intent_dir(id)
57
+ dir ? "read #{File.join(dir, "plan.md")}" : "plastic status"
58
+ end
59
+
60
+ def frontier
61
+ @frontier ||= Frontier.new(scope)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "installer_verb"
4
+
5
+ # `plastic doctor` - diagnoses Plastic installation health. The work is still
6
+ # scripts/doctor.rb, run through Legacy; each finding prints its own repair.
7
+ module Plastic
8
+ class CLI
9
+ module Commands
10
+ class Doctor < InstallerVerb
11
+ USAGE_LINE = "plastic doctor [--core] [--store WHICH]"
12
+ FLAGS = %w[--core --store].freeze
13
+
14
+ SCRIPT = "doctor.rb"
15
+ AFTER = "none"
16
+ BECAUSE = "the findings are the whole answer, and each one names its own repair"
17
+ end
18
+ end
19
+ end
20
+ end