@zalom/plastic 2.0.0-alpha.9 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/PLASTIC.md +13 -136
  2. package/README.md +357 -133
  3. package/agents/plastic-enforcer.md +20 -15
  4. package/agents/plastic-executor.md +15 -4
  5. package/agents/plastic-node-research.md +30 -0
  6. package/agents/plastic-node-verify.md +28 -0
  7. package/agents/plastic-node-work.md +33 -0
  8. package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +7 -9
  9. package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
  10. package/assets/plastic-logo.svg +1 -0
  11. package/bin/crap +4 -0
  12. package/bin/lib/context_budget.rb +43 -6
  13. package/bin/lib/skill_census.rb +839 -0
  14. package/bin/plastic +6 -0
  15. package/bin/plastic-skill-census +114 -0
  16. package/bin/test +24 -4
  17. package/bin/verify-change +345 -0
  18. package/config_asks.yml +4 -4
  19. package/deprecations.yml +1 -1
  20. package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
  21. package/{skills/auto/references → docs/help}/agent-architecture.md +16 -14
  22. package/{skills/conventions/references → docs/help}/completion-and-done.md +15 -16
  23. package/docs/help/human-report-contract.md +152 -0
  24. package/{skills/conventions/references → docs/help}/knowledge-graph.md +9 -0
  25. package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +11 -13
  26. package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +1 -1
  27. package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
  28. package/{skills/tutorial/references → docs/help}/track-1-guided.md +28 -47
  29. package/{skills/tutorial/references → docs/help}/track-2-auto.md +8 -8
  30. package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
  31. package/hooks/call-budget +4 -0
  32. package/hooks/hooks.json +24 -0
  33. package/hooks/message-display +55 -2
  34. package/hooks/session-start +5 -1
  35. package/hooks/statusline +32 -27
  36. package/hooks/stop +5 -0
  37. package/package.json +5 -3
  38. package/scripts/append-ledger +2 -1
  39. package/scripts/dashboard.rb +267 -16
  40. package/scripts/day-summary +2 -1
  41. package/scripts/doctor.rb +680 -39
  42. package/scripts/end-intent +153 -30
  43. package/scripts/exec-worktree +5 -5
  44. package/scripts/file-session-intent +2 -1
  45. package/scripts/graph-measure +249 -0
  46. package/scripts/hook-call-budget +222 -0
  47. package/scripts/hook-capture +24 -123
  48. package/scripts/hook-close +2 -1
  49. package/scripts/hook-message-display +7 -0
  50. package/scripts/hook-record +24 -16
  51. package/scripts/hook-savepoint +27 -3
  52. package/scripts/hook-session-start +359 -321
  53. package/scripts/hook-stop +58 -0
  54. package/scripts/index-projection +74 -0
  55. package/scripts/insight-append +17 -4
  56. package/scripts/install.rb +9 -7
  57. package/scripts/lib/action_graph_shim.rb +279 -0
  58. package/scripts/lib/active_delivery.rb +105 -0
  59. package/scripts/lib/agent_models.rb +63 -25
  60. package/scripts/lib/arm.rb +48 -83
  61. package/scripts/lib/atomic_write.rb +31 -0
  62. package/scripts/lib/backup.rb +65 -0
  63. package/scripts/lib/cli/command.rb +92 -0
  64. package/scripts/lib/cli/commands/auto.rb +18 -0
  65. package/scripts/lib/cli/commands/auto_brief.rb +44 -0
  66. package/scripts/lib/cli/commands/auto_lock.rb +99 -0
  67. package/scripts/lib/cli/commands/auto_report.rb +50 -0
  68. package/scripts/lib/cli/commands/auto_take.rb +31 -0
  69. package/scripts/lib/cli/commands/backup.rb +43 -0
  70. package/scripts/lib/cli/commands/checkout.rb +25 -0
  71. package/scripts/lib/cli/commands/continue.rb +66 -0
  72. package/scripts/lib/cli/commands/doctor.rb +81 -0
  73. package/scripts/lib/cli/commands/feedback.rb +40 -0
  74. package/scripts/lib/cli/commands/help.rb +69 -0
  75. package/scripts/lib/cli/commands/hook.rb +32 -0
  76. package/scripts/lib/cli/commands/index.rb +23 -0
  77. package/scripts/lib/cli/commands/install.rb +21 -0
  78. package/scripts/lib/cli/commands/installer_verb.rb +37 -0
  79. package/scripts/lib/cli/commands/intent.rb +19 -0
  80. package/scripts/lib/cli/commands/intent_answer.rb +37 -0
  81. package/scripts/lib/cli/commands/intent_command.rb +53 -0
  82. package/scripts/lib/cli/commands/intent_end.rb +62 -0
  83. package/scripts/lib/cli/commands/intent_new.rb +62 -0
  84. package/scripts/lib/cli/commands/intent_note.rb +43 -0
  85. package/scripts/lib/cli/commands/intent_rule.rb +36 -0
  86. package/scripts/lib/cli/commands/intent_show.rb +32 -0
  87. package/scripts/lib/cli/commands/intent_spec.rb +44 -0
  88. package/scripts/lib/cli/commands/intent_step.rb +71 -0
  89. package/scripts/lib/cli/commands/intent_verify.rb +26 -0
  90. package/scripts/lib/cli/commands/migrate.rb +16 -0
  91. package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
  92. package/scripts/lib/cli/commands/next.rb +54 -0
  93. package/scripts/lib/cli/commands/project.rb +19 -0
  94. package/scripts/lib/cli/commands/project_links.rb +42 -0
  95. package/scripts/lib/cli/commands/project_list.rb +20 -0
  96. package/scripts/lib/cli/commands/project_new.rb +72 -0
  97. package/scripts/lib/cli/commands/query.rb +31 -0
  98. package/scripts/lib/cli/commands/render.rb +33 -0
  99. package/scripts/lib/cli/commands/roadmap.rb +19 -0
  100. package/scripts/lib/cli/commands/roadmap_check.rb +54 -0
  101. package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
  102. package/scripts/lib/cli/commands/roadmap_migrate.rb +53 -0
  103. package/scripts/lib/cli/commands/roadmap_next.rb +70 -0
  104. package/scripts/lib/cli/commands/roadmap_show.rb +45 -0
  105. package/scripts/lib/cli/commands/rollback.rb +20 -0
  106. package/scripts/lib/cli/commands/search.rb +60 -0
  107. package/scripts/lib/cli/commands/session.rb +18 -0
  108. package/scripts/lib/cli/commands/session_commit.rb +42 -0
  109. package/scripts/lib/cli/commands/session_handoff.rb +34 -0
  110. package/scripts/lib/cli/commands/session_summary.rb +35 -0
  111. package/scripts/lib/cli/commands/status.rb +68 -0
  112. package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
  113. package/scripts/lib/cli/commands/sync.rb +46 -0
  114. package/scripts/lib/cli/commands/uninstall.rb +20 -0
  115. package/scripts/lib/cli/commands/update.rb +20 -0
  116. package/scripts/lib/cli/commands/version.rb +53 -0
  117. package/scripts/lib/cli/frontier.rb +86 -0
  118. package/scripts/lib/cli/intent_progress.rb +36 -0
  119. package/scripts/lib/cli/legacy.rb +80 -0
  120. package/scripts/lib/cli/output.rb +137 -0
  121. package/scripts/lib/cli/scope.rb +134 -0
  122. package/scripts/lib/cli/table.rb +65 -0
  123. package/scripts/lib/cli.rb +94 -0
  124. package/scripts/lib/codex_adapter.rb +198 -0
  125. package/scripts/lib/compact_instructions.rb +13 -5
  126. package/scripts/lib/core_integrity.rb +71 -0
  127. package/scripts/lib/dashboard_screen.rb +40 -0
  128. package/scripts/lib/data_boundary.rb +132 -0
  129. package/scripts/lib/day_summary.rb +23 -14
  130. package/scripts/lib/doctor_core.rb +114 -38
  131. package/scripts/lib/doctor_session_ledger.rb +5 -53
  132. package/scripts/lib/engine_permissions.rb +88 -0
  133. package/scripts/lib/exec_worktree.rb +24 -21
  134. package/scripts/lib/feedback_report.rb +1 -1
  135. package/scripts/lib/graph_edges.rb +137 -0
  136. package/scripts/lib/graph_file.rb +246 -0
  137. package/scripts/lib/graph_measure.rb +645 -0
  138. package/scripts/lib/graph_measure_budget.rb +409 -0
  139. package/scripts/lib/graph_measure_cohorts.rb +487 -0
  140. package/scripts/lib/graph_measure_models.rb +413 -0
  141. package/scripts/lib/graph_measure_report.rb +532 -0
  142. package/scripts/lib/graph_tree.rb +98 -0
  143. package/scripts/lib/guarded_append.rb +155 -0
  144. package/scripts/lib/handoff.rb +40 -13
  145. package/scripts/lib/harness_adapter.rb +184 -0
  146. package/scripts/lib/hook_registry.rb +27 -3
  147. package/scripts/lib/hook_replay.rb +229 -0
  148. package/scripts/lib/index_entry.rb +62 -0
  149. package/scripts/lib/index_projection.rb +201 -0
  150. package/scripts/lib/insights.rb +1 -1
  151. package/scripts/lib/installer_core.rb +531 -71
  152. package/scripts/lib/intent_screen.rb +4 -4
  153. package/scripts/lib/intent_screen_ansi.rb +73 -12
  154. package/scripts/lib/intent_validator.rb +2 -2
  155. package/scripts/lib/lock.rb +10 -11
  156. package/scripts/lib/message_display.rb +350 -54
  157. package/scripts/lib/meter_watch.rb +185 -0
  158. package/scripts/lib/node_file.rb +234 -0
  159. package/scripts/lib/node_ids.rb +99 -0
  160. package/scripts/lib/node_input.rb +913 -0
  161. package/scripts/lib/node_input_compatibility.rb +62 -0
  162. package/scripts/lib/node_ledger.rb +386 -0
  163. package/scripts/lib/node_progress.rb +153 -0
  164. package/scripts/lib/node_return.rb +204 -0
  165. package/scripts/lib/node_worktree.rb +337 -0
  166. package/scripts/lib/outcome_report.rb +440 -0
  167. package/scripts/lib/preflight.rb +4 -6
  168. package/scripts/lib/project_config.rb +46 -0
  169. package/scripts/lib/project_validator.rb +3 -2
  170. package/scripts/lib/qmd_sync.rb +8 -7
  171. package/scripts/lib/ready_set.rb +462 -0
  172. package/scripts/lib/reference_archive.rb +45 -0
  173. package/scripts/lib/release_guard.rb +18 -0
  174. package/scripts/lib/report_screen.rb +1371 -47
  175. package/scripts/lib/rlm/corpus.rb +13 -0
  176. package/scripts/lib/rlm/probe.rb +29 -0
  177. package/scripts/lib/rlm/query.rb +22 -0
  178. package/scripts/lib/roadmap_graph.rb +210 -0
  179. package/scripts/lib/roadmap_migration.rb +95 -0
  180. package/scripts/lib/roadmap_queue.rb +158 -8
  181. package/scripts/lib/roadmap_render.rb +150 -0
  182. package/scripts/lib/roadmap_savepoint.rb +64 -14
  183. package/scripts/lib/runner_absorb.rb +703 -0
  184. package/scripts/lib/runner_answer.rb +206 -0
  185. package/scripts/lib/runner_core.rb +194 -0
  186. package/scripts/lib/runner_dispatch.rb +525 -0
  187. package/scripts/lib/runner_policy.rb +191 -0
  188. package/scripts/lib/runner_proposals.rb +275 -0
  189. package/scripts/lib/runner_rewind.rb +201 -0
  190. package/scripts/lib/runner_sweep.rb +231 -0
  191. package/scripts/lib/runner_until_empty.rb +252 -0
  192. package/scripts/lib/runner_watch.rb +389 -0
  193. package/scripts/lib/savepoint.rb +141 -19
  194. package/scripts/lib/scaffold_intent.rb +6 -3
  195. package/scripts/lib/screen_paint.rb +365 -28
  196. package/scripts/lib/screens/dashboard.rb +20 -0
  197. package/scripts/lib/screens/plan.rb +18 -0
  198. package/scripts/lib/screens/roadmap.rb +15 -0
  199. package/scripts/lib/search_index.rb +55 -0
  200. package/scripts/lib/session_close.rb +30 -28
  201. package/scripts/lib/session_git.rb +25 -18
  202. package/scripts/lib/session_ledger.rb +48 -4
  203. package/scripts/lib/session_usage.rb +190 -0
  204. package/scripts/lib/sqlite.rb +22 -0
  205. package/scripts/lib/stop_gate.rb +95 -0
  206. package/scripts/lib/store_discovery.rb +7 -6
  207. package/scripts/lib/store_layout.rb +54 -0
  208. package/scripts/lib/store_provisioning.rb +2 -1
  209. package/scripts/lib/store_sync.rb +85 -0
  210. package/scripts/lib/stores_move.rb +93 -0
  211. package/scripts/lib/verify_intent.rb +36 -8
  212. package/scripts/lib/version_number.rb +48 -0
  213. package/scripts/lib/work_graph.rb +59 -0
  214. package/scripts/lib/work_graph_validator.rb +201 -0
  215. package/scripts/lib/worktree.rb +27 -32
  216. package/scripts/lib/worktree_sweep.rb +6 -5
  217. package/scripts/link-suggest +2 -1
  218. package/scripts/meter-watch +57 -0
  219. package/scripts/migrate-to-global +1 -1
  220. package/scripts/new-intent +4 -13
  221. package/scripts/node-input +92 -0
  222. package/scripts/node-run +225 -0
  223. package/scripts/node-transition +291 -0
  224. package/scripts/outcome-report +74 -0
  225. package/scripts/plastic-lock +42 -33
  226. package/scripts/promote-session-item +3 -2
  227. package/scripts/read-config +53 -9
  228. package/scripts/ready-set +126 -0
  229. package/scripts/release-check +123 -0
  230. package/scripts/report-screen +177 -16
  231. package/scripts/roadmap-graph +119 -0
  232. package/scripts/roadmap-savepoint +7 -0
  233. package/scripts/runner +581 -0
  234. package/scripts/savepoint-note +11 -9
  235. package/scripts/session-commit +2 -1
  236. package/scripts/session-usage +56 -0
  237. package/scripts/skill-lint +115 -6
  238. package/scripts/spawn-preamble +2 -2
  239. package/scripts/update.rb +25 -4
  240. package/scripts/validate-work-graph +39 -0
  241. package/scripts/verify-intent +3 -2
  242. package/scripts/write-handoff +2 -1
  243. package/templates/agents.md +7 -7
  244. package/templates/config.yml +16 -9
  245. package/templates/dashboard-screen.md +22 -0
  246. package/templates/display-fixture.md +21 -0
  247. package/templates/graph.md +16 -0
  248. package/templates/index.md +1 -1
  249. package/templates/intent-screen.md +1 -1
  250. package/templates/node-decision.md +11 -0
  251. package/templates/node-research.md +13 -0
  252. package/templates/node-verify.md +13 -0
  253. package/templates/node-work.md +22 -0
  254. package/templates/outcome.md +8 -3
  255. package/templates/project.yml +1 -1
  256. package/templates/render.css +10 -0
  257. package/templates/report-plan.md +15 -0
  258. package/templates/report-roadmap-delivered.md +10 -0
  259. package/templates/report-roadmap-plan.md +9 -0
  260. package/templates/report-roadmap-state.md +9 -0
  261. package/templates/report-state.md +1 -1
  262. package/templates/roadmap.md +13 -0
  263. package/bin/plastic.js +0 -70
  264. package/scripts/lib/bridge.rb +0 -116
  265. package/skills/agent-advisor/SKILL.md +0 -92
  266. package/skills/auto/SKILL.md +0 -296
  267. package/skills/auto/evals/evals.json +0 -255
  268. package/skills/auto/references/end-tail.md +0 -66
  269. package/skills/auto/references/human-report-contract.md +0 -78
  270. package/skills/conventions/SKILL.md +0 -29
  271. package/skills/dashboard/SKILL.md +0 -169
  272. package/skills/dashboard/evals/evals.json +0 -38
  273. package/skills/dashboard/references/classification.md +0 -22
  274. package/skills/dashboard/templates/dashboard-global.md +0 -20
  275. package/skills/dashboard/templates/dashboard-project.md +0 -19
  276. package/skills/direct/SKILL.md +0 -66
  277. package/skills/direct/references/request-signals.md +0 -59
  278. package/skills/doctor/SKILL.md +0 -299
  279. package/skills/doctor/report.md +0 -102
  280. package/skills/feedback/SKILL.md +0 -98
  281. package/skills/feedback/references/transport-and-privacy.md +0 -65
  282. package/skills/feedback/report.md +0 -36
  283. package/skills/install/SKILL.md +0 -217
  284. package/skills/intent-continuing/SKILL.md +0 -154
  285. package/skills/intent-continuing/references/board-fill.md +0 -43
  286. package/skills/intent-continuing/references/boarding-matrix.md +0 -34
  287. package/skills/intent-continuing/references/context-management.md +0 -28
  288. package/skills/intent-continuing/references/liveness-ranking.md +0 -57
  289. package/skills/intent-creating/SKILL.md +0 -164
  290. package/skills/intent-creating/evals/evals.json +0 -72
  291. package/skills/intent-creating/references/lifecycle.md +0 -81
  292. package/skills/intent-creating/references/wikilinks.md +0 -8
  293. package/skills/intent-ending/SKILL.md +0 -176
  294. package/skills/intent-ending/evals/evals.json +0 -74
  295. package/skills/intent-executing/SKILL.md +0 -170
  296. package/skills/intent-executing/evals/evals.json +0 -66
  297. package/skills/intent-executing/implementer-prompt.md +0 -42
  298. package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
  299. package/skills/intent-speccing/SKILL.md +0 -130
  300. package/skills/intent-speccing/evals/evals.json +0 -126
  301. package/skills/intent-speccing/references/design-principles.md +0 -44
  302. package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
  303. package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
  304. package/skills/project-creating/SKILL.md +0 -162
  305. package/skills/project-creating/references/hubs-projects.md +0 -55
  306. package/skills/project-creating/references/project-scaffolding.md +0 -97
  307. package/skills/releasing/SKILL.md +0 -337
  308. package/skills/releasing/references/deprecations.md +0 -60
  309. package/skills/releasing/references/promotion-and-tagging.md +0 -66
  310. package/skills/releasing/references/release-lines.md +0 -105
  311. package/skills/roadmap/SKILL.md +0 -64
  312. package/skills/roadmap/references/file-format.md +0 -124
  313. package/skills/roadmap/references/operations.md +0 -112
  314. package/skills/rollback/SKILL.md +0 -91
  315. package/skills/tutorial/SKILL.md +0 -65
  316. package/skills/tutorial/evals/evals.json +0 -186
  317. package/skills/uninstall/SKILL.md +0 -75
  318. package/skills/update/SKILL.md +0 -126
  319. /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
  320. /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
  321. /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
  322. /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../command"
4
+
5
+ # `plastic status` - every store this machine holds, the intents each one has
6
+ # open, and the one store worth continuing.
7
+ #
8
+ # The store to continue is chosen by a fixed rule, so two runs never disagree:
9
+ # the store whose repository holds the working directory, else the store with
10
+ # the most active intents, and on a tie the first in discovery order, which puts
11
+ # the global store ahead of the projects and the projects in name order.
12
+ module Plastic
13
+ class CLI
14
+ module Commands
15
+ class Status < Command
16
+ USAGE_LINE = "plastic status [--json]"
17
+
18
+ def call
19
+ counted.each { |slug, ids| @output.row(slug, summary(ids)) }
20
+ command, because = decision
21
+ @output.next_step(command, because: because)
22
+ end
23
+
24
+ private
25
+
26
+ def counted
27
+ @counted ||= scope.stores.to_h do |store|
28
+ slug = store[:slug]
29
+ [slug, Scope.new(env: @env, home: @home, slug: slug, directory: @directory).active_ids]
30
+ end
31
+ end
32
+
33
+ def summary(ids)
34
+ return "0 active" if ids.empty?
35
+
36
+ "#{ids.length} active #{ids.join(", ")}"
37
+ end
38
+
39
+ def decision
40
+ here = scope.directory_project
41
+ return [continue_command(here), "the working directory is inside #{here}"] if busy?(here)
42
+
43
+ chosen = busiest
44
+ return ["plastic help", "no store has active work"] unless chosen
45
+
46
+ [continue_command(chosen), "#{chosen} holds the most active work, #{intents(chosen)}"]
47
+ end
48
+
49
+ def busy?(slug)
50
+ slug && !counted.fetch(slug, []).empty?
51
+ end
52
+
53
+ def busiest
54
+ counted.reject { |_slug, ids| ids.empty? }.max_by { |_slug, ids| ids.length }&.first
55
+ end
56
+
57
+ def intents(slug)
58
+ count = counted.fetch(slug).length
59
+ (count == 1) ? "1 intent" : "#{count} intents"
60
+ end
61
+
62
+ def continue_command(slug)
63
+ (slug == Scope::GLOBAL) ? "plastic continue" : "plastic continue --project #{slug}"
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../command"
4
+ require_relative "../table"
5
+
6
+ # Plastic::CLI::Commands::SubcommandList (intent 372, family 3) - the shared
7
+ # base every bare group command (`plastic intent`, `plastic project`,
8
+ # `plastic roadmap`) runs on: list the table rows under its prefix, or a
9
+ # usage error naming that same list for a word that matches none of them.
10
+ module Plastic
11
+ class CLI
12
+ module Commands
13
+ class SubcommandList < Command
14
+ def call
15
+ unless arguments.empty?
16
+ raise Usage, "no subcommand named #{arguments.first.inspect}. Try: #{subcommand_names.join(", ")}"
17
+ end
18
+
19
+ @output.row("usage", self.class::USAGE_LINE)
20
+ subcommands.each { |name, (_file, _const, summary)| @output.row(name, summary) }
21
+ @output.next_step(self.class::NEXT, because: self.class::BECAUSE)
22
+ end
23
+
24
+ private
25
+
26
+ def subcommands
27
+ TABLE.select { |name, _| name.start_with?("#{self.class::PREFIX} ") }.sort.to_h
28
+ end
29
+
30
+ def subcommand_names
31
+ subcommands.keys
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../command"
4
+ require_relative "../../store_sync"
5
+ require_relative "../../work_graph"
6
+
7
+ module Plastic
8
+ class CLI
9
+ module Commands
10
+ class Sync < Command
11
+ USAGE_LINE = "plastic sync [--dry-run] [--json]"
12
+
13
+ def call
14
+ home = scope.plastic_home
15
+ plan = File.exist?(SearchIndex.path(home)) ? StoreSync.plan(home) : [["build", SearchIndex::NAME]]
16
+ refuse(home, plan.filter_map { |action, path| path if action == "conflict" })
17
+ plan.group_by(&:first).each { |action, pairs| @output.row(action, pairs.map(&:last)) }
18
+ @output.row("result", "nothing to do") if plan.empty?
19
+ apply(home, plan) unless options[:dry_run]
20
+ @output.next_step("plastic search TERMS", because: "a changed file is read into its row, a changed row is written out, and both changed is yours to settle")
21
+ rescue Sqlite::Error => e
22
+ raise Failure, e.message
23
+ end
24
+
25
+ private
26
+
27
+ def switches(parser)
28
+ parser.on("--dry-run") { options[:dry_run] = true }
29
+ end
30
+
31
+ def refuse(home, conflicts)
32
+ return if conflicts.empty?
33
+
34
+ diffs = conflicts.map { |path| "#{path}\n#{StoreSync.diff(home, path)}" }
35
+ raise Refusal, "the file and the row both changed; nothing was written\n#{diffs.join("\n")}"
36
+ end
37
+
38
+ def apply(home, plan)
39
+ (plan == [["build", SearchIndex::NAME]]) ? SearchIndex.build(home) : StoreSync.apply(home, plan)
40
+ WorkGraph.build(home)
41
+ ReferenceArchive.build(home)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "installer_verb"
4
+
5
+ # `plastic uninstall` - removes Plastic from this machine's agents and leaves
6
+ # the store alone. The work is still scripts/uninstall.rb, run through Legacy.
7
+ module Plastic
8
+ class CLI
9
+ module Commands
10
+ class Uninstall < InstallerVerb
11
+ USAGE_LINE = "plastic uninstall [--claude] [--codex] [--hermes] [--all]"
12
+ FLAGS = %w[--claude --codex --hermes --all].freeze
13
+
14
+ SCRIPT = "uninstall.rb"
15
+ AFTER = "none"
16
+ BECAUSE = "Plastic is removed from this machine's agents"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "installer_verb"
4
+
5
+ # `plastic update` - moves this machine to the next version on its channel. The
6
+ # work is still scripts/update.rb, run through Legacy.
7
+ module Plastic
8
+ class CLI
9
+ module Commands
10
+ class Update < InstallerVerb
11
+ USAGE_LINE = "plastic update [--claude] [--codex] [--hermes] [--all] [--latest] [--beta] [--alpha] [--yes] [--reinstall] [--full-doctor]"
12
+ FLAGS = %w[--claude --codex --hermes --all --latest --beta --alpha --yes --reinstall --full-doctor].freeze
13
+
14
+ SCRIPT = "update.rb"
15
+ AFTER = "plastic version"
16
+ BECAUSE = "the update is done, and restarting the session picks up the new conventions"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../command"
5
+
6
+ # `plastic version` - the version of the package this command came from, and the
7
+ # file it was read from. The VERSION file wins, because an installed copy
8
+ # carries one; the package manifest answers in a checkout, which has none.
9
+ module Plastic
10
+ class CLI
11
+ module Commands
12
+ class Version < Command
13
+ USAGE_LINE = "plastic version [--json]"
14
+
15
+ SOURCES = ["VERSION", "package.json"].freeze
16
+
17
+ def call
18
+ path, number = found
19
+ raise Failure, "no VERSION file and no package.json under #{package_root}" unless number
20
+
21
+ @output.row("version", number)
22
+ @output.row("source", path)
23
+ @output.next_step("plastic status", because: "the command line works, so read the work next")
24
+ end
25
+
26
+ def found
27
+ SOURCES.each do |name|
28
+ path = File.join(package_root, name)
29
+ number = read(path)
30
+ return [path, number] if number
31
+ end
32
+ [nil, nil]
33
+ end
34
+
35
+ private
36
+
37
+ def read(path)
38
+ return nil unless File.file?(path)
39
+ return File.read(path).strip unless path.end_with?(".json")
40
+
41
+ number = JSON.parse(File.read(path))["version"]
42
+ number&.to_s&.strip
43
+ rescue JSON::ParserError
44
+ nil
45
+ end
46
+
47
+ def package_root
48
+ @package_root ||= @env["PLASTIC_PACKAGE_ROOT"] || File.expand_path("../../../..", __dir__)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../roadmap_queue"
4
+
5
+ # Plastic::CLI::Frontier (intent 363) - the one reader of "what comes next" in a
6
+ # scope. `plastic next` prints it as a line and `plastic continue` prints it
7
+ # inside a project block, so the rule that picks the work is stated once rather
8
+ # than twice.
9
+ #
10
+ # It reads RoadmapQueue's queue mode: the liveliest roadmap, its frontier batch,
11
+ # the entries that can be dispatched, the entries in flight and the blocked
12
+ # ones. With no roadmap at all it answers the "none" state rather than raising,
13
+ # and a roadmap with no grouping heading raises through to the command, which
14
+ # fails loudly instead of parsing it as zero entries.
15
+ module Plastic
16
+ class CLI
17
+ class Frontier
18
+ NONE = "none"
19
+
20
+ def initialize(scope)
21
+ @scope = scope
22
+ end
23
+
24
+ def state
25
+ payload["state"]
26
+ end
27
+
28
+ def roadmap
29
+ payload["roadmap"]
30
+ end
31
+
32
+ def heading
33
+ payload["frontier_wave"]
34
+ end
35
+
36
+ def dispatchable_ids
37
+ ids(payload["dispatchable_queue"])
38
+ end
39
+
40
+ def in_flight_ids
41
+ ids(payload["in_flight"])
42
+ end
43
+
44
+ def blocked_ids
45
+ ids(payload["blocked"])
46
+ end
47
+
48
+ def id
49
+ (dispatchable_ids + in_flight_ids).first
50
+ end
51
+
52
+ def summary
53
+ return NONE unless id
54
+
55
+ "#{id} in #{heading}"
56
+ end
57
+
58
+ def because
59
+ return "#{@scope.slug} has no roadmap" if state == NONE
60
+ return "#{roadmap} has blocked work: #{blocked_ids.join(", ")}" if id.nil? && !blocked_ids.empty?
61
+ return "#{roadmap} has an invalid graph: #{heading}" if state == "error"
62
+ return "every entry on #{roadmap} is delivered" if id.nil?
63
+
64
+ "#{id} is first on the frontier of #{roadmap}"
65
+ end
66
+
67
+ def next_step
68
+ return "none" unless id
69
+
70
+ plan = @scope.intent_dir(id)
71
+ plan ? "plastic intent show #{id}" : "plastic status"
72
+ end
73
+
74
+ private
75
+
76
+ def payload
77
+ @payload ||= RoadmapQueue.new(roadmaps_dir: @scope.roadmaps_dir,
78
+ index_path: @scope.index_path).queue
79
+ end
80
+
81
+ def ids(entries)
82
+ Array(entries).map { |entry| entry["id"] }
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../intent_screen"
4
+
5
+ module Plastic
6
+ class CLI
7
+ class IntentProgress
8
+ def initialize(scope, id)
9
+ @scope = scope
10
+ @id = id
11
+ @dir = scope.intent_dir(id)
12
+ end
13
+
14
+ def decision
15
+ status, = IntentScreen.index_fields(@scope.root, @id)
16
+ return ["none", "the intent is #{status.downcase}"] if %w[Completed Abandoned].include?(status)
17
+ return ["plastic intent step #{@id}", "the graph is ready for its next step"] if File.file?(File.join(@dir, "graph.md"))
18
+ return ["plastic intent spec #{@id}", "write spec.md with the accepted scope"] unless prepared?("spec.md")
19
+ return ["plastic intent spec #{@id}", "write plan.md and checklist.md before execution"] unless prepared?("plan.md") && IntentScreen.items_present?(@dir)
20
+
21
+ items = IntentScreen.checklist_items(@dir)
22
+ return ["plastic intent spec #{@id}", "the checklist needs concrete work items"] if items.empty?
23
+ return ["plastic intent verify #{@id}", "all checklist items are complete"] if items.all? { |item| item[:done] }
24
+
25
+ ["plastic intent step #{@id}", "the checklist has unfinished work"]
26
+ end
27
+
28
+ private
29
+
30
+ def prepared?(name)
31
+ path = File.join(@dir, name)
32
+ File.file?(path) && !File.read(path).strip.empty? && !File.read(path).include?(IntentScreen::PLACEHOLDER_SENTINEL)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+ require "open3"
5
+ require "rbconfig"
6
+ require_relative "command"
7
+
8
+ # Plastic::CLI::Legacy (intent 363) - runs a script that has not moved into a
9
+ # module yet as a child process, and passes its output straight through. It is
10
+ # scaffolding: when the last family is migrated it is deleted, and the commands
11
+ # call their modules in the same process instead.
12
+ #
13
+ # A child that exits 3 refused, and the refusal travels as a Refusal so the
14
+ # agent is told to stop and ask rather than to retry with a flag. Every other
15
+ # status is handed back to the caller unchanged.
16
+ module Plastic
17
+ class CLI
18
+ class Legacy
19
+ # A runner answers with the child's exit status. Asked to capture, it
20
+ # answers with the child's stdout and that status, for a command that
21
+ # renders the script's JSON as a screen. stderr is never captured, so a
22
+ # warning the script writes there still reaches the terminal.
23
+ DEFAULT_RUNNER = lambda do |path, arguments, capture: false|
24
+ unless capture
25
+ system(RbConfig.ruby, path, *arguments)
26
+ next exit_code($CHILD_STATUS)
27
+ end
28
+
29
+ out, process = Open3.capture2(RbConfig.ruby, path, *arguments)
30
+ [out, exit_code(process)]
31
+ end
32
+
33
+ NON_OWNER_EXIT_THREE = %w[verify-intent runner].freeze
34
+
35
+ def self.exit_code(status)
36
+ return Command::OK if status.success?
37
+
38
+ status.exitstatus || Command::FAILED
39
+ end
40
+
41
+ def initialize(env:, runner: nil, output: nil, json: false)
42
+ @output = output
43
+ @json = json
44
+ @env = env
45
+ @runner = runner || DEFAULT_RUNNER
46
+ end
47
+
48
+ def package_root
49
+ @package_root ||= @env["PLASTIC_PACKAGE_ROOT"] || File.expand_path("../../..", __dir__)
50
+ end
51
+
52
+ def script_path(script)
53
+ File.join(package_root, "scripts", script)
54
+ end
55
+
56
+ def run(script, *arguments)
57
+ if @json
58
+ text, status = @runner.call(script_path(script), arguments, capture: true)
59
+ @output.raw(text) unless text.empty?
60
+ else
61
+ status = @runner.call(script_path(script), arguments)
62
+ end
63
+ raise Command::Refusal, "#{script} needs the owner" if owner_refusal?(script, status)
64
+
65
+ status
66
+ end
67
+
68
+ def owner_refusal?(script, status)
69
+ status == Command::REFUSED && !NON_OWNER_EXIT_THREE.include?(script)
70
+ end
71
+
72
+ def capture(script, *arguments)
73
+ out, status = @runner.call(script_path(script), arguments, capture: true)
74
+ raise Command::Refusal, "#{script} needs the owner" if owner_refusal?(script, status)
75
+
76
+ [out, status]
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "shellwords"
5
+
6
+ # Plastic::CLI::Output (intent 363) - one place where a command's answer is
7
+ # printed. A result is rows of label and value; every command ends with a
8
+ # `next:` line and the `because:` line that gives the rule behind it, which is
9
+ # where the step order the skills used to carry now lives. `--json` prints the
10
+ # same three things with stable keys.
11
+ #
12
+ # Results go to the output stream and diagnostics to the error stream, so a
13
+ # caller can pipe one without the other. Nothing here colours its output.
14
+ module Plastic
15
+ class CLI
16
+ class Output
17
+ LABEL_GAP = 2
18
+
19
+ attr_writer :project
20
+
21
+ def initialize(out:, err:, json: false)
22
+ @json = json
23
+ @raw = []
24
+ @out = out
25
+ @err = err
26
+ @rows = []
27
+ @next_step = nil
28
+ @because = nil
29
+ @flushed = false
30
+ end
31
+
32
+ def row(label, value)
33
+ @rows << [label, value]
34
+ self
35
+ end
36
+
37
+ def next_step(command, because:)
38
+ @next_step = command
39
+ @because = because
40
+ self
41
+ end
42
+
43
+ def flush(json: false)
44
+ return self if @flushed
45
+
46
+ @flushed = true
47
+ json ? print_json : print_text
48
+ self
49
+ end
50
+
51
+ def document(value)
52
+ @out.puts JSON.pretty_generate(value)
53
+ @flushed = true
54
+ self
55
+ end
56
+
57
+ def raw(text)
58
+ @json ? @raw << text : @out.puts(text)
59
+ self
60
+ end
61
+
62
+ def usage(message, banner)
63
+ @err.puts "plastic: #{message}"
64
+ @err.puts banner
65
+ error_document(message, "usage")
66
+ self
67
+ end
68
+
69
+ def refused(message)
70
+ @err.puts "plastic: refused, #{message}"
71
+ @err.puts "This step belongs to the owner. Stop and ask; do not retry with a flag."
72
+ error_document(message, "refused")
73
+ self
74
+ end
75
+
76
+ def failed(message)
77
+ @err.puts "plastic: #{message}"
78
+ error_document(message, "failed")
79
+ self
80
+ end
81
+
82
+ private
83
+
84
+ def error_document(message, kind)
85
+ return unless @json
86
+
87
+ row("error", {"kind" => kind, "message" => message})
88
+ next_step("none", because: message)
89
+ flush(json: true)
90
+ end
91
+
92
+ def result
93
+ data = @rows.to_h
94
+ data["output"] = @raw unless @raw.empty?
95
+ data
96
+ end
97
+
98
+ def scoped_next
99
+ return @next_step unless @project && @next_step&.match?(/\Aplastic (?:intent|auto|roadmap|continue|next|search)\b/)
100
+ return @next_step if @next_step.include?("--project")
101
+
102
+ "#{@next_step} --project #{Shellwords.escape(@project)}"
103
+ end
104
+
105
+ def print_json
106
+ @out.puts JSON.pretty_generate("result" => result, "next" => scoped_next,
107
+ "because" => @because)
108
+ end
109
+
110
+ def print_text
111
+ printed = print_rows
112
+ return unless @next_step
113
+
114
+ @out.puts if printed
115
+ @out.puts "next: #{scoped_next}"
116
+ @out.puts "because: #{@because}"
117
+ end
118
+
119
+ def print_rows
120
+ width = label_width
121
+ printed = false
122
+ @rows.each do |label, value|
123
+ Array(value).each_with_index do |item, index|
124
+ @out.puts "#{(index.zero? ? label : "").ljust(width)}#{item}"
125
+ printed = true
126
+ end
127
+ end
128
+ printed
129
+ end
130
+
131
+ def label_width
132
+ widths = @rows.reject { |_label, value| Array(value).empty? }.map { |label, _value| label.length }
133
+ widths.empty? ? 0 : widths.max + LABEL_GAP
134
+ end
135
+ end
136
+ end
137
+ end