@zalom/plastic 2.0.0-alpha.26 → 2.0.0-alpha.28

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 (224) hide show
  1. package/PLASTIC.md +13 -139
  2. package/README.md +337 -133
  3. package/agents/plastic-enforcer.md +12 -12
  4. package/agents/plastic-executor.md +2 -1
  5. package/agents/plastic-node-research.md +4 -2
  6. package/agents/plastic-node-verify.md +1 -0
  7. package/agents/plastic-node-work.md +1 -0
  8. package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +6 -8
  9. package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
  10. package/bin/crap +4 -0
  11. package/bin/lib/context_budget.rb +35 -1
  12. package/bin/lib/skill_census.rb +839 -0
  13. package/bin/plastic +6 -0
  14. package/bin/plastic-skill-census +114 -0
  15. package/bin/verify-change +345 -0
  16. package/config_asks.yml +4 -4
  17. package/deprecations.yml +1 -1
  18. package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
  19. package/{skills/auto/references → docs/help}/agent-architecture.md +7 -7
  20. package/{skills/conventions/references → docs/help}/completion-and-done.md +1 -1
  21. package/{skills/auto/references → docs/help}/human-report-contract.md +17 -19
  22. package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
  23. package/{skills/tutorial/references → docs/help}/track-1-guided.md +8 -8
  24. package/{skills/tutorial/references → docs/help}/track-2-auto.md +5 -5
  25. package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
  26. package/package.json +3 -2
  27. package/scripts/append-ledger +2 -1
  28. package/scripts/dashboard.rb +10 -9
  29. package/scripts/day-summary +2 -1
  30. package/scripts/doctor.rb +57 -49
  31. package/scripts/end-intent +2 -8
  32. package/scripts/file-session-intent +2 -1
  33. package/scripts/hook-capture +4 -3
  34. package/scripts/hook-close +2 -1
  35. package/scripts/hook-record +3 -2
  36. package/scripts/hook-savepoint +3 -2
  37. package/scripts/hook-session-start +5 -4
  38. package/scripts/hook-stop +2 -1
  39. package/scripts/insight-append +1 -2
  40. package/scripts/install.rb +7 -5
  41. package/scripts/lib/action_graph_shim.rb +3 -1
  42. package/scripts/lib/active_delivery.rb +1 -1
  43. package/scripts/lib/agent_models.rb +44 -26
  44. package/scripts/lib/arm.rb +2 -1
  45. package/scripts/lib/backup.rb +63 -0
  46. package/scripts/lib/cli/command.rb +85 -0
  47. package/scripts/lib/cli/commands/auto.rb +18 -0
  48. package/scripts/lib/cli/commands/auto_brief.rb +44 -0
  49. package/scripts/lib/cli/commands/auto_lock.rb +60 -0
  50. package/scripts/lib/cli/commands/auto_report.rb +50 -0
  51. package/scripts/lib/cli/commands/auto_take.rb +25 -0
  52. package/scripts/lib/cli/commands/backup.rb +43 -0
  53. package/scripts/lib/cli/commands/checkout.rb +25 -0
  54. package/scripts/lib/cli/commands/continue.rb +66 -0
  55. package/scripts/lib/cli/commands/doctor.rb +20 -0
  56. package/scripts/lib/cli/commands/feedback.rb +40 -0
  57. package/scripts/lib/cli/commands/help.rb +69 -0
  58. package/scripts/lib/cli/commands/hook.rb +32 -0
  59. package/scripts/lib/cli/commands/index.rb +23 -0
  60. package/scripts/lib/cli/commands/install.rb +21 -0
  61. package/scripts/lib/cli/commands/installer_verb.rb +37 -0
  62. package/scripts/lib/cli/commands/intent.rb +19 -0
  63. package/scripts/lib/cli/commands/intent_answer.rb +37 -0
  64. package/scripts/lib/cli/commands/intent_command.rb +53 -0
  65. package/scripts/lib/cli/commands/intent_end.rb +61 -0
  66. package/scripts/lib/cli/commands/intent_new.rb +62 -0
  67. package/scripts/lib/cli/commands/intent_note.rb +43 -0
  68. package/scripts/lib/cli/commands/intent_rule.rb +36 -0
  69. package/scripts/lib/cli/commands/intent_show.rb +25 -0
  70. package/scripts/lib/cli/commands/intent_spec.rb +44 -0
  71. package/scripts/lib/cli/commands/intent_step.rb +43 -0
  72. package/scripts/lib/cli/commands/intent_verify.rb +26 -0
  73. package/scripts/lib/cli/commands/migrate.rb +16 -0
  74. package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
  75. package/scripts/lib/cli/commands/next.rb +49 -0
  76. package/scripts/lib/cli/commands/project.rb +19 -0
  77. package/scripts/lib/cli/commands/project_links.rb +42 -0
  78. package/scripts/lib/cli/commands/project_list.rb +20 -0
  79. package/scripts/lib/cli/commands/project_new.rb +72 -0
  80. package/scripts/lib/cli/commands/query.rb +31 -0
  81. package/scripts/lib/cli/commands/render.rb +25 -0
  82. package/scripts/lib/cli/commands/roadmap.rb +19 -0
  83. package/scripts/lib/cli/commands/roadmap_check.rb +44 -0
  84. package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
  85. package/scripts/lib/cli/commands/roadmap_next.rb +34 -0
  86. package/scripts/lib/cli/commands/roadmap_show.rb +45 -0
  87. package/scripts/lib/cli/commands/rollback.rb +20 -0
  88. package/scripts/lib/cli/commands/search.rb +60 -0
  89. package/scripts/lib/cli/commands/session.rb +18 -0
  90. package/scripts/lib/cli/commands/session_commit.rb +42 -0
  91. package/scripts/lib/cli/commands/session_handoff.rb +34 -0
  92. package/scripts/lib/cli/commands/session_summary.rb +35 -0
  93. package/scripts/lib/cli/commands/status.rb +68 -0
  94. package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
  95. package/scripts/lib/cli/commands/sync.rb +46 -0
  96. package/scripts/lib/cli/commands/uninstall.rb +20 -0
  97. package/scripts/lib/cli/commands/update.rb +20 -0
  98. package/scripts/lib/cli/commands/version.rb +53 -0
  99. package/scripts/lib/cli/frontier.rb +84 -0
  100. package/scripts/lib/cli/legacy.rb +50 -0
  101. package/scripts/lib/cli/output.rb +102 -0
  102. package/scripts/lib/cli/scope.rb +127 -0
  103. package/scripts/lib/cli/table.rb +64 -0
  104. package/scripts/lib/cli.rb +94 -0
  105. package/scripts/lib/codex_adapter.rb +5 -4
  106. package/scripts/lib/compact_instructions.rb +8 -0
  107. package/scripts/lib/day_summary.rb +4 -3
  108. package/scripts/lib/doctor_core.rb +7 -32
  109. package/scripts/lib/doctor_session_ledger.rb +2 -1
  110. package/scripts/lib/feedback_report.rb +1 -1
  111. package/scripts/lib/graph_measure_models.rb +4 -2
  112. package/scripts/lib/index_entry.rb +9 -0
  113. package/scripts/lib/installer_core.rb +175 -56
  114. package/scripts/lib/intent_screen.rb +3 -3
  115. package/scripts/lib/lock.rb +2 -2
  116. package/scripts/lib/node_file.rb +22 -2
  117. package/scripts/lib/node_input.rb +11 -6
  118. package/scripts/lib/node_return.rb +8 -3
  119. package/scripts/lib/preflight.rb +2 -4
  120. package/scripts/lib/project_config.rb +2 -1
  121. package/scripts/lib/project_validator.rb +3 -2
  122. package/scripts/lib/qmd_sync.rb +8 -7
  123. package/scripts/lib/reference_archive.rb +45 -0
  124. package/scripts/lib/release_guard.rb +2 -0
  125. package/scripts/lib/report_screen.rb +4 -3
  126. package/scripts/lib/rlm/corpus.rb +13 -0
  127. package/scripts/lib/rlm/probe.rb +29 -0
  128. package/scripts/lib/rlm/query.rb +22 -0
  129. package/scripts/lib/roadmap_queue.rb +2 -2
  130. package/scripts/lib/roadmap_savepoint.rb +1 -1
  131. package/scripts/lib/runner_absorb.rb +59 -2
  132. package/scripts/lib/runner_dispatch.rb +12 -8
  133. package/scripts/lib/runner_policy.rb +28 -10
  134. package/scripts/lib/search_index.rb +55 -0
  135. package/scripts/lib/session_git.rb +4 -3
  136. package/scripts/lib/sqlite.rb +22 -0
  137. package/scripts/lib/store_discovery.rb +7 -6
  138. package/scripts/lib/store_layout.rb +54 -0
  139. package/scripts/lib/store_provisioning.rb +2 -1
  140. package/scripts/lib/store_sync.rb +85 -0
  141. package/scripts/lib/stores_move.rb +93 -0
  142. package/scripts/lib/verify_intent.rb +2 -7
  143. package/scripts/lib/version_number.rb +48 -0
  144. package/scripts/lib/work_graph.rb +59 -0
  145. package/scripts/lib/worktree.rb +3 -8
  146. package/scripts/lib/worktree_sweep.rb +3 -2
  147. package/scripts/link-suggest +2 -1
  148. package/scripts/migrate-to-global +1 -1
  149. package/scripts/new-intent +3 -12
  150. package/scripts/node-run +2 -1
  151. package/scripts/plastic-lock +3 -2
  152. package/scripts/promote-session-item +3 -2
  153. package/scripts/read-config +44 -6
  154. package/scripts/release-check +10 -5
  155. package/scripts/report-screen +1 -1
  156. package/scripts/session-commit +2 -1
  157. package/scripts/spawn-preamble +2 -2
  158. package/scripts/update.rb +25 -4
  159. package/scripts/write-handoff +2 -1
  160. package/templates/agents.md +6 -6
  161. package/templates/config.yml +13 -6
  162. package/templates/node-research.md +4 -2
  163. package/templates/render.css +10 -0
  164. package/bin/plastic.js +0 -70
  165. package/skills/agent-advisor/SKILL.md +0 -92
  166. package/skills/auto/SKILL.md +0 -297
  167. package/skills/auto/evals/evals.json +0 -255
  168. package/skills/auto/references/end-tail.md +0 -64
  169. package/skills/conventions/SKILL.md +0 -29
  170. package/skills/dashboard/SKILL.md +0 -180
  171. package/skills/dashboard/evals/evals.json +0 -38
  172. package/skills/dashboard/references/classification.md +0 -22
  173. package/skills/dashboard/templates/dashboard-global.md +0 -20
  174. package/skills/dashboard/templates/dashboard-project.md +0 -19
  175. package/skills/direct/SKILL.md +0 -66
  176. package/skills/direct/references/request-signals.md +0 -59
  177. package/skills/doctor/SKILL.md +0 -305
  178. package/skills/doctor/report.md +0 -102
  179. package/skills/feedback/SKILL.md +0 -98
  180. package/skills/feedback/references/transport-and-privacy.md +0 -65
  181. package/skills/feedback/report.md +0 -36
  182. package/skills/install/SKILL.md +0 -217
  183. package/skills/intent-continuing/SKILL.md +0 -156
  184. package/skills/intent-continuing/references/board-fill.md +0 -52
  185. package/skills/intent-continuing/references/boarding-matrix.md +0 -35
  186. package/skills/intent-continuing/references/context-management.md +0 -28
  187. package/skills/intent-continuing/references/liveness-ranking.md +0 -57
  188. package/skills/intent-creating/SKILL.md +0 -89
  189. package/skills/intent-creating/evals/evals.json +0 -72
  190. package/skills/intent-creating/references/lifecycle.md +0 -81
  191. package/skills/intent-creating/references/wikilinks.md +0 -8
  192. package/skills/intent-ending/SKILL.md +0 -182
  193. package/skills/intent-ending/evals/evals.json +0 -74
  194. package/skills/intent-executing/SKILL.md +0 -87
  195. package/skills/intent-executing/evals/evals.json +0 -66
  196. package/skills/intent-executing/implementer-prompt.md +0 -47
  197. package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
  198. package/skills/intent-speccing/SKILL.md +0 -136
  199. package/skills/intent-speccing/evals/evals.json +0 -126
  200. package/skills/intent-speccing/references/design-principles.md +0 -44
  201. package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
  202. package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
  203. package/skills/project-creating/SKILL.md +0 -162
  204. package/skills/project-creating/references/hubs-projects.md +0 -55
  205. package/skills/project-creating/references/project-scaffolding.md +0 -97
  206. package/skills/releasing/SKILL.md +0 -376
  207. package/skills/releasing/references/deprecations.md +0 -60
  208. package/skills/releasing/references/promotion-and-tagging.md +0 -70
  209. package/skills/releasing/references/release-lines.md +0 -105
  210. package/skills/roadmap/SKILL.md +0 -90
  211. package/skills/roadmap/references/file-format.md +0 -134
  212. package/skills/roadmap/references/operations.md +0 -112
  213. package/skills/rollback/SKILL.md +0 -91
  214. package/skills/tutorial/SKILL.md +0 -66
  215. package/skills/tutorial/evals/evals.json +0 -186
  216. package/skills/uninstall/SKILL.md +0 -75
  217. package/skills/update/SKILL.md +0 -126
  218. /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
  219. /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
  220. /package/{skills/conventions/references → docs/help}/knowledge-graph.md +0 -0
  221. /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
  222. /package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +0 -0
  223. /package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +0 -0
  224. /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
@@ -14,8 +14,7 @@ module AgentModels
14
14
  # HarnessAdapter::AGENT_TYPE_BY_KIND. work and research resolve the
15
15
  # executor tier, verify the advisor tier, mirroring RunnerPolicy's
16
16
  # model_role split (327 D12): work and research run on plastic-executor's
17
- # own tier, verify on the same tier plastic-advisor's imitation
18
- # (plastic-faux-advisor) ships, never the cheap tier.
17
+ # own tier, and verify on the lifecycle advisor tier, never the cheap tier.
19
18
  TIER_DEFAULTS = {
20
19
  "plastic-enforcer" => "opus",
21
20
  "plastic-executor" => "sonnet",
@@ -24,32 +23,35 @@ module AgentModels
24
23
  "plastic-node-research" => "sonnet"
25
24
  }.freeze
26
25
 
27
- # The two advisor agents (intent 185 final design): plastic-advisor (the real
28
- # advisor, ships `model: fable`) and plastic-faux-advisor (the imitation
29
- # advisor, an ordinary model carrying the same reasoning discipline inline,
30
- # ships `model: opus`). Both are shipped DEFAULTS in frontmatter, never a
26
+ # The two consultation agents are Primary Advisor and Secondary Advisor.
27
+ # Both ship Fable; Primary uses medium effort and Secondary uses high effort.
28
+ # Both are shipped defaults in frontmatter, never a
31
29
  # hard-wired identity: agents.models.claude.<name> (or the legacy flat form)
32
30
  # overrides either one through the same install-time frontmatter rewrite
33
31
  # every agent override uses. Neither is a lifecycle-stage role: never
34
- # dispatched by the auto pipeline, not part of TIER_DEFAULTS. Claude-only for
35
- # this release (generate_codex_agents skips both by name; the Codex advisor
36
- # case is intent 186, not a permanent exclusion).
37
- #
38
- # Intent 186 DEFINES the advisor Codex pairing but keeps emission deferred: when the skip is
39
- # lifted, plastic-advisor pairs to gpt-5.6-sol at xhigh (the deepest) and plastic-faux-advisor
40
- # to gpt-5.6-terra at high (cheaper). Neither is in TIER_DEFAULTS and neither is auto-dispatched.
41
- CONSULTATION_AGENTS = %w[plastic-advisor plastic-faux-advisor].freeze
42
-
43
- # Codex reasoning-effort per tier alias (intent 102a). model_reasoning_effort is a
44
- # depth-of-thinking dial independent of model selection (181 line 317-318), so mapping
45
- # the tier here never encodes a rotting Codex model id (116 D1). opus is the deepest
46
- # reasoning tier -> the deepest generally-safe effort (high, not the model-dependent
47
- # xhigh); sonnet the mid execution tier -> medium; haiku the lightest -> low. minimal is
48
- # unused.
32
+ # dispatched by the auto pipeline, not part of TIER_DEFAULTS. Codex installs
33
+ # generate both advisors with Astra and preserve each role's effort.
34
+ CONSULTATION_AGENTS = %w[plastic-primary-advisor plastic-secondary-advisor].freeze
35
+
36
+ SHIPPED_MODEL_DEFAULTS = TIER_DEFAULTS.merge(
37
+ "plastic-primary-advisor" => "fable",
38
+ "plastic-secondary-advisor" => "fable"
39
+ ).freeze
40
+
41
+ # Codex reasoning effort per tier alias. Model choice and reasoning effort are independent:
42
+ # aliases select the recommended OpenAI model, while every Plastic role starts at medium.
43
+ # A valid harness-scoped user override can still choose another effort for one agent.
49
44
  EFFORT_BY_ALIAS = {
50
- "opus" => "high",
45
+ "opus" => "medium",
51
46
  "sonnet" => "medium",
52
- "haiku" => "low"
47
+ "haiku" => "medium"
48
+ }.freeze
49
+
50
+ DEFAULT_EFFORT = "medium"
51
+
52
+ SHIPPED_EFFORT_DEFAULTS = {
53
+ "plastic-primary-advisor" => "medium",
54
+ "plastic-secondary-advisor" => "high"
53
55
  }.freeze
54
56
 
55
57
  # Codex model id per tier alias (intent 186). Codex has NO vendor alias layer: every model id
@@ -58,17 +60,26 @@ module AgentModels
58
60
  # centralizing every id in ONE map: Plastic owns the alias, so per-role identity costs a single
59
61
  # line to refresh on a Codex deprecation plus a Plastic release, and no per-role file carries a
60
62
  # raw id. opus (deepest reasoning tier) -> the flagship Sol; sonnet (execution tier) -> the
61
- # balanced Terra; haiku (lightest) -> the fast/cheap Luna. Paired with EFFORT_BY_ALIAS so
62
- # reasoning roles get a stronger model AND higher effort than executors. This is a shipped
63
- # DEFAULT, fully overridable via agents.models.codex.<name>.
63
+ # balanced Terra; haiku (lightest) -> the fast/cheap Luna. This is a shipped
64
+ # DEFAULT, fully overridable via agents.models.codex.<name>. Model strength does not change
65
+ # effort: all three aliases use medium unless agents.efforts.codex.<name> overrides it.
64
66
  CODEX_MODEL_BY_ALIAS = {
65
67
  "opus" => "gpt-5.6-sol",
66
68
  "sonnet" => "gpt-5.6-terra",
67
69
  "haiku" => "gpt-5.6-luna"
68
70
  }.freeze
69
71
 
72
+ CODEX_MODEL_BY_AGENT = {
73
+ "plastic-primary-advisor" => "gpt-6-astra",
74
+ "plastic-secondary-advisor" => "gpt-6-astra"
75
+ }.freeze
76
+
70
77
  module_function
71
78
 
79
+ def shipped_effort_for(agent)
80
+ SHIPPED_EFFORT_DEFAULTS.fetch(agent.to_s, DEFAULT_EFFORT)
81
+ end
82
+
72
83
  # Pull { basename => model } out of a loaded config hash's `agents.models`
73
84
  # section, scoped to `harness` ("claude" or "codex"), tolerating a missing or
74
85
  # malformed shape. `agents.models` can mix two shapes: legacy FLAT scalar
@@ -124,4 +135,11 @@ module AgentModels
124
135
  def codex_model_for(value)
125
136
  CODEX_MODEL_BY_ALIAS[value.to_s]
126
137
  end
138
+
139
+ def shipped_model_for(agent, harness: "claude")
140
+ value = SHIPPED_MODEL_DEFAULTS[agent.to_s]
141
+ return value unless harness.to_s == "codex"
142
+
143
+ CODEX_MODEL_BY_AGENT[agent.to_s] || codex_model_for(value) || value
144
+ end
127
145
  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 "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,63 @@
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
+
10
+ module Plastic
11
+ module Backup
12
+ DATABASES = [SearchIndex::NAME, WorkGraph::NAME, ReferenceArchive::NAME].freeze
13
+ ROOT_FILES = %w[config.yml projects.yml INDEX.md].freeze
14
+ Missing = Class.new(StandardError)
15
+
16
+ def self.dir(home)
17
+ File.join(home, "backups")
18
+ end
19
+
20
+ def self.list(home)
21
+ Dir.glob(File.join(dir(home), "plastic-backup--*.tar.gz")).sort
22
+ end
23
+
24
+ def self.call(home, version:, now: Time.now.utc)
25
+ missing = DATABASES.reject { |name| File.exist?(File.join(home, name)) }
26
+ raise Missing, missing.join(", ") unless missing.empty?
27
+
28
+ Dir.mktmpdir("plastic-backup") do |work|
29
+ DATABASES.each { |name| Sqlite.call(File.join(home, name), "VACUUM INTO #{Sqlite.quote(File.join(work, name))};", readonly: true) }
30
+ FileUtils.cp(ROOT_FILES.map { |name| File.join(home, name) }.select { |path| File.exist?(path) }, work)
31
+ File.write(File.join(work, "manifest.json"), JSON.pretty_generate(manifest(work, version, now)))
32
+ pack(work, File.join(dir(home), "plastic-backup--#{now.strftime("%Y%m%dT%H%M%SZ")}.tar.gz"))
33
+ end
34
+ end
35
+
36
+ def self.manifest(work, version, now)
37
+ files = Dir.children(work).sort.map do |name|
38
+ path = File.join(work, name)
39
+ {"name" => name, "bytes" => File.size(path), "sha256" => Digest::SHA256.file(path).hexdigest}
40
+ end
41
+ {"version" => version, "at" => now.strftime("%Y-%m-%dT%H:%M:%SZ"), "files" => files}
42
+ end
43
+
44
+ def self.pack(work, archive)
45
+ require "rubygems"
46
+ require "rubygems/package"
47
+ FileUtils.mkdir_p(File.dirname(archive))
48
+ draft = "#{archive}.tmp"
49
+ Zlib::GzipWriter.open(draft) do |gzip|
50
+ Gem::Package::TarWriter.new(gzip) do |tar|
51
+ Dir.children(work).sort.each do |name|
52
+ path = File.join(work, name)
53
+ tar.add_file_simple(name, 0o644, File.size(path)) { |entry| IO.copy_stream(path, entry) }
54
+ end
55
+ end
56
+ end
57
+ File.rename(draft, archive)
58
+ archive
59
+ ensure
60
+ FileUtils.rm_f(draft.to_s)
61
+ end
62
+ end
63
+ 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
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../command"
4
+ require_relative "../legacy"
5
+
6
+ # `plastic feedback "TITLE"` - files a report with feedback-report, the body
7
+ # read from standard input and passed to the child process unchanged.
8
+ module Plastic
9
+ class CLI
10
+ module Commands
11
+ class Feedback < Command
12
+ USAGE_LINE = 'plastic feedback "TITLE"'
13
+
14
+ def initialize(argv, runner: nil, **streams)
15
+ super(argv, **streams)
16
+ @runner = runner
17
+ end
18
+
19
+ def call
20
+ raise Usage, "plastic feedback needs a title" if title.nil? || title.strip.empty?
21
+
22
+ status = legacy.run("feedback-report", "--title", title)
23
+ raise Failure, "feedback-report exited #{status}" unless status.zero?
24
+
25
+ @output.next_step("none", because: "the report is saved as a draft; open the printed URL to send it")
26
+ end
27
+
28
+ private
29
+
30
+ def title
31
+ arguments.first
32
+ end
33
+
34
+ def legacy
35
+ @legacy ||= Legacy.new(env: @env, runner: @runner)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end