@zalom/plastic 2.0.0-alpha.9 → 2.0.2

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 (326) hide show
  1. package/PLASTIC.md +13 -136
  2. package/README.md +357 -133
  3. package/agents/plastic-enforcer.md +21 -16
  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 +8 -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/docs/help/completion-and-done.md +53 -0
  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 +5 -4
  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 +378 -56
  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 +74 -91
  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 +111 -0
  67. package/scripts/lib/cli/commands/auto_report.rb +50 -0
  68. package/scripts/lib/cli/commands/auto_take.rb +68 -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 +58 -0
  82. package/scripts/lib/cli/commands/intent_end.rb +75 -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 +79 -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 +46 -0
  95. package/scripts/lib/cli/commands/project_list.rb +20 -0
  96. package/scripts/lib/cli/commands/project_new.rb +95 -0
  97. package/scripts/lib/cli/commands/query.rb +31 -0
  98. package/scripts/lib/cli/commands/render.rb +35 -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 +77 -0
  104. package/scripts/lib/cli/commands/roadmap_show.rb +65 -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 +55 -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 +235 -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 +543 -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 +921 -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 +170 -12
  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 +526 -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 +67 -19
  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/untouched_scaffold.rb +51 -0
  212. package/scripts/lib/verify_intent.rb +36 -8
  213. package/scripts/lib/version_number.rb +48 -0
  214. package/scripts/lib/work_graph.rb +59 -0
  215. package/scripts/lib/work_graph_validator.rb +201 -0
  216. package/scripts/lib/worktree.rb +27 -32
  217. package/scripts/lib/worktree_sweep.rb +6 -5
  218. package/scripts/link-suggest +2 -1
  219. package/scripts/meter-watch +57 -0
  220. package/scripts/migrate-to-global +1 -1
  221. package/scripts/new-intent +4 -13
  222. package/scripts/node-input +92 -0
  223. package/scripts/node-run +225 -0
  224. package/scripts/node-transition +291 -0
  225. package/scripts/outcome-report +74 -0
  226. package/scripts/plastic-lock +65 -60
  227. package/scripts/project-links +6 -21
  228. package/scripts/promote-session-item +3 -2
  229. package/scripts/read-config +53 -9
  230. package/scripts/ready-set +126 -0
  231. package/scripts/release-check +123 -0
  232. package/scripts/report-screen +177 -16
  233. package/scripts/roadmap-graph +125 -0
  234. package/scripts/roadmap-savepoint +7 -0
  235. package/scripts/rollback.rb +5 -1
  236. package/scripts/runner +581 -0
  237. package/scripts/savepoint-note +11 -9
  238. package/scripts/session-commit +2 -1
  239. package/scripts/session-usage +56 -0
  240. package/scripts/skill-lint +115 -6
  241. package/scripts/spawn-preamble +2 -2
  242. package/scripts/update.rb +31 -4
  243. package/scripts/validate-work-graph +39 -0
  244. package/scripts/verify-intent +3 -2
  245. package/scripts/write-handoff +2 -1
  246. package/templates/agents.md +7 -7
  247. package/templates/config.yml +16 -9
  248. package/templates/dashboard-screen.md +22 -0
  249. package/templates/display-fixture.md +21 -0
  250. package/templates/graph.md +16 -0
  251. package/templates/index.md +1 -1
  252. package/templates/intent-screen.md +1 -1
  253. package/templates/node-decision.md +11 -0
  254. package/templates/node-research.md +13 -0
  255. package/templates/node-verify.md +13 -0
  256. package/templates/node-work.md +22 -0
  257. package/templates/outcome.md +8 -3
  258. package/templates/project.yml +1 -1
  259. package/templates/render.css +10 -0
  260. package/templates/report-plan.md +15 -0
  261. package/templates/report-roadmap-delivered.md +10 -0
  262. package/templates/report-roadmap-plan.md +9 -0
  263. package/templates/report-roadmap-state.md +9 -0
  264. package/templates/report-state.md +1 -1
  265. package/templates/roadmap.md +13 -0
  266. package/bin/plastic.js +0 -70
  267. package/scripts/lib/bridge.rb +0 -116
  268. package/skills/agent-advisor/SKILL.md +0 -92
  269. package/skills/auto/SKILL.md +0 -296
  270. package/skills/auto/evals/evals.json +0 -255
  271. package/skills/auto/references/end-tail.md +0 -66
  272. package/skills/auto/references/human-report-contract.md +0 -78
  273. package/skills/conventions/SKILL.md +0 -29
  274. package/skills/conventions/references/completion-and-done.md +0 -43
  275. package/skills/dashboard/SKILL.md +0 -169
  276. package/skills/dashboard/evals/evals.json +0 -38
  277. package/skills/dashboard/references/classification.md +0 -22
  278. package/skills/dashboard/templates/dashboard-global.md +0 -20
  279. package/skills/dashboard/templates/dashboard-project.md +0 -19
  280. package/skills/direct/SKILL.md +0 -66
  281. package/skills/direct/references/request-signals.md +0 -59
  282. package/skills/doctor/SKILL.md +0 -299
  283. package/skills/doctor/report.md +0 -102
  284. package/skills/feedback/SKILL.md +0 -98
  285. package/skills/feedback/references/transport-and-privacy.md +0 -65
  286. package/skills/feedback/report.md +0 -36
  287. package/skills/install/SKILL.md +0 -217
  288. package/skills/intent-continuing/SKILL.md +0 -154
  289. package/skills/intent-continuing/references/board-fill.md +0 -43
  290. package/skills/intent-continuing/references/boarding-matrix.md +0 -34
  291. package/skills/intent-continuing/references/context-management.md +0 -28
  292. package/skills/intent-continuing/references/liveness-ranking.md +0 -57
  293. package/skills/intent-creating/SKILL.md +0 -164
  294. package/skills/intent-creating/evals/evals.json +0 -72
  295. package/skills/intent-creating/references/lifecycle.md +0 -81
  296. package/skills/intent-creating/references/wikilinks.md +0 -8
  297. package/skills/intent-ending/SKILL.md +0 -176
  298. package/skills/intent-ending/evals/evals.json +0 -74
  299. package/skills/intent-executing/SKILL.md +0 -170
  300. package/skills/intent-executing/evals/evals.json +0 -66
  301. package/skills/intent-executing/implementer-prompt.md +0 -42
  302. package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
  303. package/skills/intent-speccing/SKILL.md +0 -130
  304. package/skills/intent-speccing/evals/evals.json +0 -126
  305. package/skills/intent-speccing/references/design-principles.md +0 -44
  306. package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
  307. package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
  308. package/skills/project-creating/SKILL.md +0 -162
  309. package/skills/project-creating/references/hubs-projects.md +0 -55
  310. package/skills/project-creating/references/project-scaffolding.md +0 -97
  311. package/skills/releasing/SKILL.md +0 -337
  312. package/skills/releasing/references/deprecations.md +0 -60
  313. package/skills/releasing/references/promotion-and-tagging.md +0 -66
  314. package/skills/releasing/references/release-lines.md +0 -105
  315. package/skills/roadmap/SKILL.md +0 -64
  316. package/skills/roadmap/references/file-format.md +0 -124
  317. package/skills/roadmap/references/operations.md +0 -112
  318. package/skills/rollback/SKILL.md +0 -91
  319. package/skills/tutorial/SKILL.md +0 -65
  320. package/skills/tutorial/evals/evals.json +0 -186
  321. package/skills/uninstall/SKILL.md +0 -75
  322. package/skills/update/SKILL.md +0 -126
  323. /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
  324. /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
  325. /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
  326. /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../command"
4
+ require_relative "../legacy"
5
+
6
+ # `plastic session commit` - one commit for a verified checklist item, on the
7
+ # directory this command was invoked from (`session-commit --cwd DIR
8
+ # --summary SUMMARY`, run through Legacy; fail-open by contract, so a Failure
9
+ # here means the script's own usage error, not a git failure).
10
+ module Plastic
11
+ class CLI
12
+ module Commands
13
+ class SessionCommit < Command
14
+ USAGE_LINE = 'plastic session commit "SUMMARY" [--json]'
15
+
16
+ def initialize(argv, runner: nil, **streams)
17
+ super(argv, **streams)
18
+ @runner = runner
19
+ end
20
+
21
+ def call
22
+ raise Usage, "SUMMARY is required" if summary.to_s.empty?
23
+
24
+ status = legacy.run("session-commit", "--cwd", @directory, "--summary", summary)
25
+ raise Failure, "session-commit exited #{status}" unless status.zero?
26
+
27
+ @output.next_step("plastic session handoff", because: "the line above says whether a commit landed; a handoff keeps the day ledger current")
28
+ end
29
+
30
+ private
31
+
32
+ def summary
33
+ arguments.first
34
+ end
35
+
36
+ def legacy
37
+ @legacy ||= Legacy.new(env: @env, runner: @runner, output: @output, json: options[:json])
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../command"
4
+ require_relative "../legacy"
5
+
6
+ # `plastic session handoff` - writes this session's hand-off into the day
7
+ # ledger (`write-handoff --trigger tick`, run through Legacy).
8
+ module Plastic
9
+ class CLI
10
+ module Commands
11
+ class SessionHandoff < Command
12
+ USAGE_LINE = "plastic session handoff [--json]"
13
+
14
+ def initialize(argv, runner: nil, **streams)
15
+ super(argv, **streams)
16
+ @runner = runner
17
+ end
18
+
19
+ def call
20
+ status = legacy.run("write-handoff", "--trigger", "tick")
21
+ raise Failure, "write-handoff exited #{status}" unless status.zero?
22
+
23
+ @output.next_step("plastic session summary", because: "the summary is what a fresh handoff feeds")
24
+ end
25
+
26
+ private
27
+
28
+ def legacy
29
+ @legacy ||= Legacy.new(env: @env, runner: @runner, output: @output, json: options[:json])
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../command"
4
+ require_relative "../legacy"
5
+
6
+ # `plastic session summary` - the block SessionStart injects at boot: open
7
+ # items, the last five done, live auto intents, other active sessions
8
+ # (`day-summary`, run through Legacy).
9
+ module Plastic
10
+ class CLI
11
+ module Commands
12
+ class SessionSummary < Command
13
+ USAGE_LINE = "plastic session summary [--json]"
14
+
15
+ def initialize(argv, runner: nil, **streams)
16
+ super(argv, **streams)
17
+ @runner = runner
18
+ end
19
+
20
+ def call
21
+ status = legacy.run("day-summary")
22
+ raise Failure, "day-summary exited #{status}" unless status.zero?
23
+
24
+ @output.next_step("plastic status", because: "the board is where the summary's open items are worked from")
25
+ end
26
+
27
+ private
28
+
29
+ def legacy
30
+ @legacy ||= Legacy.new(env: @env, runner: @runner, output: @output, json: options[:json])
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -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,55 @@
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
+ # Rebuilt on every sync; a missing one is named in the plan, so the
14
+ # preview and the sync report the same work (acceptance N7).
15
+ DERIVED = [WorkGraph, ReferenceArchive].freeze
16
+
17
+ def call
18
+ home = scope.plastic_home
19
+ plan = File.exist?(SearchIndex.path(home)) ? StoreSync.plan(home) : [["build", SearchIndex::NAME]]
20
+ plan += DERIVED.reject { |database| File.exist?(database.path(home)) }.map { |database| ["build", database::NAME] }
21
+ refuse(home, plan.filter_map { |action, path| path if action == "conflict" })
22
+ plan.group_by(&:first).each { |action, pairs| @output.row(action, pairs.map(&:last)) }
23
+ @output.row("result", "nothing to do") if plan.empty?
24
+ apply(home, plan) unless options[:dry_run]
25
+ @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")
26
+ rescue Sqlite::Error => e
27
+ raise Failure, e.message
28
+ end
29
+
30
+ private
31
+
32
+ def switches(parser)
33
+ parser.on("--dry-run") { options[:dry_run] = true }
34
+ end
35
+
36
+ def refuse(home, conflicts)
37
+ return if conflicts.empty?
38
+
39
+ diffs = conflicts.map { |path| "#{path}\n#{StoreSync.diff(home, path)}" }
40
+ raise Refusal, "the file and the row both changed; nothing was written\n#{diffs.join("\n")}"
41
+ end
42
+
43
+ def apply(home, plan)
44
+ if plan.include?(["build", SearchIndex::NAME])
45
+ SearchIndex.build(home)
46
+ else
47
+ StoreSync.apply(home, plan.reject { |step| step.first == "build" })
48
+ end
49
+ WorkGraph.build(home)
50
+ ReferenceArchive.build(home)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ 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