@zalom/plastic 2.0.0-alpha.8 → 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 +23 -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 +62 -1
  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 +238 -22
  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 +19 -5
  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 +30 -16
  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 +532 -71
  152. package/scripts/lib/intent_screen.rb +9 -6
  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 +377 -173
  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 +1460 -74
  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 +555 -0
  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 +192 -30
  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 +15 -2
  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 -295
  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 -173
  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,126 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ # ready-set - the CLI over ReadySet (intent 336, G3). No computed field this
6
+ # intent adds ships without a script that reads it (327 D18, C19): prints the
7
+ # ready order, the blockers for every unready node, the batches, and every
8
+ # maximal-length critical path; emits JSON for a caller like the runner (G7).
9
+ #
10
+ # Usage:
11
+ # ready-set <intent_dir> [--json] [--ranker finish-first|critical-path]
12
+ #
13
+ # Exit codes (matching scripts/validate-work-graph's shape):
14
+ # 0 - printed (or emitted as JSON)
15
+ # 1 - the graph is invalid (missing, cyclic, or a malformed node file)
16
+ # 2 - usage: not an intent directory, or an unknown --ranker value
17
+
18
+ require "json"
19
+ require_relative "lib/ready_set"
20
+ require_relative "lib/savepoint"
21
+
22
+ module ReadySetCli
23
+ module_function
24
+
25
+ RANKERS = {
26
+ "finish-first" => -> { ReadySet::FinishFirstRanker.new },
27
+ "critical-path" => -> { ReadySet::CriticalPathRanker.new },
28
+ }.freeze
29
+
30
+ def usage
31
+ warn "Usage: ready-set <intent_dir> [--json] [--ranker #{RANKERS.keys.join('|')}]"
32
+ end
33
+
34
+ def intent_directory?(dir)
35
+ dir && File.directory?(dir) && File.exist?(Savepoint.intent_file(dir))
36
+ end
37
+
38
+ def main(argv)
39
+ args = argv.dup
40
+ json = !!args.delete("--json")
41
+
42
+ ranker_name = nil
43
+ if (i = args.index("--ranker"))
44
+ ranker_name = args[i + 1]
45
+ args.slice!(i, 2)
46
+ end
47
+
48
+ intent_dir_arg = args.shift
49
+ unless intent_directory?(intent_dir_arg && File.expand_path(intent_dir_arg))
50
+ warn "ready-set: #{intent_dir_arg.inspect} is not an intent directory"
51
+ usage
52
+ return 2
53
+ end
54
+ intent_dir = File.expand_path(intent_dir_arg)
55
+
56
+ ranker = nil
57
+ if ranker_name
58
+ factory = RANKERS[ranker_name]
59
+ unless factory
60
+ warn "ready-set: unknown ranker #{ranker_name.inspect} (use #{RANKERS.keys.join(', ')})"
61
+ return 2
62
+ end
63
+ ranker = factory.call
64
+ end
65
+
66
+ analysis = ReadySet.analyze(intent_dir, ranker: ranker)
67
+ unless analysis[:ok]
68
+ warn "ready-set: #{analysis[:errors].join('; ')}"
69
+ return 1
70
+ end
71
+
72
+ if json
73
+ puts JSON.pretty_generate(json_payload(analysis))
74
+ else
75
+ print_report(analysis)
76
+ end
77
+ 0
78
+ end
79
+
80
+ def json_payload(analysis)
81
+ unready = analysis[:nodes].reject { |_, v| v[:ready] }
82
+ {
83
+ "ready" => analysis[:ranked_ready].map { |r| r[:id] },
84
+ "ranker" => analysis[:ranker_name],
85
+ "batches" => analysis[:batches],
86
+ "critical_path" => analysis[:critical_path],
87
+ "paths" => analysis[:paths],
88
+ "hops" => analysis[:hops],
89
+ "max_paths_total" => analysis[:max_paths_total],
90
+ "blockers" => unready.transform_values { |v| v[:blockers] },
91
+ "errors" => analysis[:errors],
92
+ }
93
+ end
94
+
95
+ def print_report(analysis)
96
+ unready = analysis[:nodes].reject { |_, v| v[:ready] }
97
+
98
+ puts "Ranker: #{analysis[:ranker_name]}"
99
+ puts
100
+ puts "Ready (#{analysis[:ranked_ready].length}):"
101
+ if analysis[:ranked_ready].empty?
102
+ puts " (none)"
103
+ else
104
+ analysis[:ranked_ready].each { |r| puts " #{r[:id]}" }
105
+ end
106
+ puts
107
+ puts "Blockers:"
108
+ if unready.empty?
109
+ puts " (none)"
110
+ else
111
+ unready.each do |id, view|
112
+ puts " #{id}:"
113
+ view[:blockers].each { |b| puts " - #{b}" }
114
+ end
115
+ end
116
+ puts
117
+ puts "Batches:"
118
+ analysis[:batches].each_with_index { |batch, i| puts " #{i + 1}: #{batch.join(', ')}" }
119
+ puts
120
+ puts "Critical path (#{analysis[:hops]} hops): #{(analysis[:critical_path] || []).join(' > ')}"
121
+ puts "Every maximal-length path (#{analysis[:max_paths_total]} total, #{analysis[:paths].length} shown):"
122
+ analysis[:paths].each { |p| puts " #{p.join(' > ')}" }
123
+ end
124
+ end
125
+
126
+ exit(ReadySetCli.main(ARGV)) if $PROGRAM_NAME == __FILE__
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ # release-check - deterministic CLI over ReleaseGuard (intent 347).
6
+ #
7
+ # Guards a tagged publish before the workflow ever reaches `npm publish`:
8
+ # the pushed tag must equal `v` plus the version in package.json, the three
9
+ # repo version files must agree, and the runner's npm must meet the 11.5.1
10
+ # floor npm requires for OIDC trusted publishing. Writes the derived
11
+ # dist-tag and version to $GITHUB_OUTPUT, so the alpha/beta/latest rule has
12
+ # exactly one implementation, ReleaseGuard.dist_tag, shared with the test
13
+ # suite.
14
+ #
15
+ # Usage:
16
+ # release-check (--tag <tag> | --branch <branch>) --npm-version <version>
17
+ # [--github-output <path>] [--root <dir>]
18
+ #
19
+ # Exit codes: 0 (clean), 1 (violations; reported on stderr, one per line,
20
+ # each naming the specific thing that is wrong), 2 (usage).
21
+
22
+ require "json"
23
+
24
+ require_relative "lib/release_guard"
25
+
26
+ NPM_FLOOR = [11, 5, 1].freeze
27
+ KNOWN_FLAGS = %w[--tag --branch --npm-version --github-output --root].freeze
28
+
29
+ def usage
30
+ warn "usage: release-check (--tag <tag> | --branch <branch>) --npm-version <version> [--github-output <path>] [--root <dir>]"
31
+ exit 2
32
+ end
33
+
34
+ def parse_args(argv)
35
+ opts = {}
36
+ i = 0
37
+ while i < argv.length
38
+ flag = argv[i]
39
+ usage unless KNOWN_FLAGS.include?(flag)
40
+
41
+ value = argv[i + 1]
42
+ usage if value.nil?
43
+
44
+ opts[flag] = value
45
+ i += 2
46
+ end
47
+ opts
48
+ end
49
+
50
+ def read_version(path)
51
+ JSON.parse(File.read(path))["version"]
52
+ rescue Errno::ENOENT, JSON::ParserError
53
+ nil
54
+ end
55
+
56
+ opts = parse_args(ARGV)
57
+ usage unless (opts["--tag"] || opts["--branch"]) && opts["--npm-version"]
58
+
59
+ root = opts["--root"] ? File.expand_path(opts["--root"]) : File.expand_path("..", __dir__)
60
+ package_json_path = File.join(root, "package.json")
61
+ plugin_json_path = File.join(root, ".claude-plugin", "plugin.json")
62
+ marketplace_json_path = File.join(root, ".claude-plugin", "marketplace.json")
63
+
64
+ violations = []
65
+
66
+ canonical_version = read_version(package_json_path)
67
+ violations << "could not read a version from #{package_json_path}" if canonical_version.nil?
68
+
69
+ branch = opts["--branch"]
70
+ dist_tag = nil
71
+
72
+ if canonical_version
73
+ expected_tag = "v#{canonical_version}"
74
+ tag = opts.fetch("--tag", expected_tag).sub(%r{\Arefs/tags/}, "")
75
+ if tag != expected_tag
76
+ violations << "tag #{tag} does not match package.json version #{canonical_version} (expected #{expected_tag})"
77
+ end
78
+
79
+ dist_tag = ReleaseGuard.dist_tag(canonical_version)
80
+ if branch && ReleaseGuard::CHANNELS[branch] != dist_tag
81
+ violations << "branch #{branch} publishes #{ReleaseGuard::CHANNELS[branch] || "nothing"}, and version #{canonical_version} is #{dist_tag}"
82
+ end
83
+ stable = dist_tag == "latest"
84
+
85
+ result = ReleaseGuard.check(
86
+ package_json: package_json_path,
87
+ plugin_json: plugin_json_path,
88
+ marketplace_json: marketplace_json_path,
89
+ stable: stable
90
+ )
91
+
92
+ unless result.ok?
93
+ result.mismatches.each do |file|
94
+ violations << "version file #{file} does not agree with package.json (#{canonical_version})"
95
+ end
96
+ if stable && result.prerelease_suffix
97
+ violations << "stable cut #{canonical_version} carries the pre-release suffix -#{result.prerelease_suffix}"
98
+ end
99
+ end
100
+ end
101
+
102
+ npm_version = opts["--npm-version"].to_s.strip
103
+ if npm_version.empty? || !npm_version.match?(/\A\d+(\.\d+)*\z/)
104
+ violations << "npm --version reported #{opts["--npm-version"].inspect}, which is empty or not numeric"
105
+ elsif (npm_version.split(".").map(&:to_i) <=> NPM_FLOOR) < 0
106
+ violations << "npm #{npm_version} is below the 11.5.1 floor OIDC trusted publishing requires"
107
+ end
108
+
109
+ if violations.empty?
110
+ if opts["--github-output"]
111
+ File.open(opts["--github-output"], "a") do |f|
112
+ f.puts "version=#{canonical_version}"
113
+ f.puts "dist_tag=#{dist_tag}"
114
+ f.puts "tag=#{tag}"
115
+ end
116
+ end
117
+ puts "OK: tag #{tag}, version #{canonical_version}, dist-tag #{dist_tag}"
118
+ exit 0
119
+ end
120
+
121
+ warn "VIOLATIONS:"
122
+ violations.each { |v| warn "- #{v}" }
123
+ exit 1
@@ -2,42 +2,121 @@
2
2
  # encoding: UTF-8
3
3
  # frozen_string_literal: true
4
4
 
5
- # report-screen - the three delivery-report screens (intent 317): mid-delivery
6
- # state, post-delivery delivered, and delay. Each fills from the record via
5
+ # report-screen - the pre-delivery plan (intent 331b), the four delivery
6
+ # reports: mid-delivery state, post-delivery delivered, and delay (intent
7
+ # 317), plus session, the whole of one session's delivered work followed by
8
+ # the roster (intent 330). Each fills from the record via
7
9
  # scripts/lib/report_screen.rb; no number here is written by eye.
8
10
  #
9
11
  # Usage:
12
+ # report-screen plan <intent_dir> [--ansi]
10
13
  # report-screen state <intent_dir> [--changed "<text>"] [--ansi]
11
14
  # report-screen state --all <store_root> [--changed "<text>"] [--ansi]
12
- # report-screen delivered <intent_dir> [--ansi]
15
+ # report-screen delivered <intent_dir> [--ansi] [--repo <dir>]
13
16
  # report-screen delay <intent_dir> [--ansi]
17
+ # report-screen session <tier_root> [--session <id>] [--since <iso>]
18
+ # [--ledger-root <dir>] [--ansi]
19
+ # report-screen roadmap <roadmap.md> plan|state|delivered [--ansi] [--store-root <dir>]
20
+ # report-screen archive <store_root> [--ansi]
14
21
  #
15
- # --ansi delegates to 316a's renderer conventions when that file is present
16
- # (D2) and prints plain otherwise, so this never blocks on 316a and never
17
- # breaks when it lands; NO_COLOR forces plain regardless of --ansi.
22
+ # --ansi delegates to ScreenPaint (intent 317a, D1), the parser/re-layouter
23
+ # in the shared TUI core. Selection is by capability, never by harness:
24
+ # NO_COLOR forces plain, a non-TTY stdout stays plain unless
25
+ # PLASTIC_FORCE_COLOR=1 (the test seam), and an unparseable screen falls
26
+ # open to the plain text untouched.
18
27
  #
19
28
  # Exit codes:
20
29
  # 0 - the screen is on stdout
21
30
  # 2 - usage error, or the path/store is not what the verb expects (one line
22
31
  # on stderr, stdout empty)
23
32
 
33
+ require "yaml"
34
+ require "shellwords"
24
35
  require_relative "lib/report_screen"
25
36
  require_relative "lib/intent_screen"
37
+ require_relative "lib/screen_paint"
38
+ require_relative "lib/session_ledger"
39
+
40
+ # Intent 331a (D6/R8): every caller-added screen kind file registers itself
41
+ # on load (ScreenPaint.register), so this glob is the ONLY wiring a new kind
42
+ # needs - no edit here, no edit to screen_paint.rb. Sorted for a
43
+ # deterministic load order; tolerates the directory being absent or empty
44
+ # (Dir.glob answers [] either way), which is this repo's own state today.
45
+ Dir.glob(File.join(__dir__, "lib", "screens", "*.rb")).sort.each { |f| require_relative f }
26
46
 
27
47
  def usage_abort(message)
28
48
  warn "report-screen: #{message}"
29
49
  exit 2
30
50
  end
31
51
 
32
- # The one place allowed to shell out (D2/D20): resolves the nearest git tag
33
- # reachable from the repo this script lives in, so `render_delivered`'s
34
- # version field and the `ship` evidence row are never invented. The pure
35
- # module never reads git itself; this is injected as `tag_reader:`.
36
- def git_tag_reader(repo_root)
37
- lambda do |_intent_dir|
38
- return nil unless File.exist?(File.join(repo_root, ".git"))
39
- tag = `git -C #{repo_root} describe --tags --abbrev=0 2>/dev/null`.strip
40
- tag.empty? ? nil : tag
52
+ # The one place allowed to shell out (D2/D20): resolves the tag that CONTAINS
53
+ # the intent's merge commit, in the project's own repository, so the version
54
+ # on the delivered screen is the release that merge shipped in. The
55
+ # repository comes from --repo, else from projects.yml beside the store (the
56
+ # installed layout, <home>/projects/<slug>/store/<id>), else the repository
57
+ # this script lives in (the in-repo layout, `include_self: true`) - unless
58
+ # `include_self: false` (the branch reader, D23), in which case a global
59
+ # intent's repo never falls back to the script's own repository, since that
60
+ # is not the intent's repository at all. No merge sha, no repository, or no
61
+ # containing tag all answer nil, and the screen says "not recorded" rather
62
+ # than guessing from HEAD (D14). The pure module never reads git.
63
+ def resolve_repo(intent_dir, explicit_repo, script_dir, include_self: true)
64
+ candidates = []
65
+ candidates << File.expand_path(explicit_repo) if explicit_repo
66
+ if (m = intent_dir.match(%r{\A(.*)/(?:projects|stores)/([^/]+)/store/[^/]+\z}))
67
+ home, slug = m[1], m[2]
68
+ projects_yml = File.join(home, "projects.yml")
69
+ if File.exist?(projects_yml)
70
+ data = begin
71
+ YAML.safe_load(File.read(projects_yml))
72
+ rescue StandardError
73
+ nil
74
+ end
75
+ entry = data.is_a?(Hash) && data["projects"].is_a?(Hash) ? data["projects"][slug] : nil
76
+ path = entry.is_a?(Hash) ? entry["path"].to_s : ""
77
+ candidates << File.expand_path(path) unless path.empty?
78
+ end
79
+ end
80
+ candidates << File.expand_path("..", script_dir) if include_self
81
+ candidates.find { |c| File.exist?(File.join(c, ".git")) }
82
+ end
83
+
84
+ def git_tag_reader(explicit_repo:, script_dir:)
85
+ lambda do |intent_dir|
86
+ sha = ReportScreen.merge_sha(intent_dir)
87
+ return nil unless sha
88
+ repo = resolve_repo(intent_dir, explicit_repo, script_dir)
89
+ return nil unless repo
90
+ tags = `git -C #{repo.shellescape} tag --contains #{sha.shellescape} 2>/dev/null`
91
+ versions = tags.lines.map(&:strip).reject(&:empty?).filter_map do |tag|
92
+ v = tag.sub(/\Av/, "")
93
+ Gem::Version.correct?(v) ? [Gem::Version.new(v), tag] : nil
94
+ end
95
+ versions.min_by(&:first)&.last
96
+ end
97
+ end
98
+
99
+ # D9/D23: the injected branch reader. flow_base (a pure YAML read, never
100
+ # git) wins when the intent's project names a flow base; otherwise the
101
+ # repository's remote HEAD, else whichever of main/master exists. Resolves
102
+ # the repo with `include_self: false` (D23): a global-store intent must
103
+ # never fall back to the script's OWN repository (~/.plastic itself, on
104
+ # "main" in the installed layout) and print a confident, wrong branch.
105
+ def git_branch_reader(explicit_repo:, script_dir:)
106
+ lambda do |intent_dir|
107
+ flow = ReportScreen.flow_base(intent_dir)
108
+ next flow if flow && !flow.to_s.empty?
109
+
110
+ repo = resolve_repo(intent_dir, explicit_repo, script_dir, include_self: false)
111
+ next nil unless repo
112
+
113
+ branch = `git -C #{repo.shellescape} symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null`.strip
114
+ branch = branch.sub(%r{\Aorigin/}, "")
115
+ next branch unless branch.empty?
116
+
117
+ %w[main master].find do |b|
118
+ system("git", "-C", repo, "rev-parse", "--verify", "--quiet", b, out: File::NULL, err: File::NULL)
119
+ end
41
120
  end
42
121
  end
43
122
 
@@ -46,7 +125,11 @@ verb = args.shift
46
125
  changed = nil
47
126
  ansi = false
48
127
  template_path = nil
49
- renderer_path = nil
128
+ repo_flag = nil
129
+ session_flag = nil
130
+ since_flag = nil
131
+ ledger_root_flag = nil
132
+ store_root_flag = nil
50
133
  positional = []
51
134
 
52
135
  while (arg = args.shift)
@@ -58,36 +141,55 @@ while (arg = args.shift)
58
141
  ansi = true
59
142
  when "--all"
60
143
  positional << "--all"
144
+ when "--repo"
145
+ repo_flag = args.shift or usage_abort("--repo needs a path")
61
146
  when "--template"
62
147
  template_path = args.shift or usage_abort("--template needs a path")
63
- when "--renderer-path"
64
- renderer_path = args.shift or usage_abort("--renderer-path needs a path")
148
+ when "--session"
149
+ session_flag = args.shift or usage_abort("--session needs a value")
150
+ when "--since"
151
+ since_flag = args.shift or usage_abort("--since needs a value")
152
+ when "--ledger-root"
153
+ ledger_root_flag = args.shift or usage_abort("--ledger-root needs a path")
154
+ when "--store-root"
155
+ store_root_flag = args.shift or usage_abort("--store-root needs a path")
65
156
  else
66
157
  usage_abort("unknown flag #{arg.inspect}") if arg.start_with?("--") && arg != "--all"
67
158
  positional << arg
68
159
  end
69
160
  end
70
161
 
71
- usage_abort("usage: report-screen state|delivered|delay <intent_dir> [--changed \"<text>\"] [--ansi]") unless verb
162
+ usage_abort("usage: report-screen plan|state|delivered|delay|session <intent_dir> [--changed \"<text>\"] [--ansi]") unless verb
72
163
 
73
164
  all_mode = positional.delete("--all") ? true : false
74
165
  target = positional.first
75
166
 
76
- renderer_path ||= File.expand_path("../lib/intent_screen_ansi.rb", __dir__)
77
- ansi_enabled = ansi && ENV["NO_COLOR"].to_s.empty?
167
+ ansi_enabled = ansi && ENV["NO_COLOR"].to_s.empty? &&
168
+ ($stdout.tty? || ENV["PLASTIC_FORCE_COLOR"] == "1")
78
169
 
79
- def paint(text, ansi_enabled, renderer_path)
80
- ReportScreen.maybe_paint(text, renderer_path: renderer_path, enabled: ansi_enabled)
170
+ def paint(text, ansi_enabled, _renderer_path = nil)
171
+ return text unless ansi_enabled
172
+ ScreenPaint.paint(text, color: true) || text
81
173
  end
82
174
 
83
175
  case verb
176
+ when "plan"
177
+ usage_abort("usage: report-screen plan <intent_dir> [--ansi]") unless target
178
+ intent_dir = File.expand_path(target)
179
+ usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
180
+ store_root = File.expand_path("../..", intent_dir)
181
+ template_path ||= File.expand_path("../templates/report-plan.md", __dir__)
182
+ usage_abort("template not found at #{template_path}") unless File.exist?(template_path)
183
+ out = ReportScreen.render_plan(intent_dir: intent_dir, store_root: store_root,
184
+ template: File.read(template_path))
185
+ $stdout.write paint(out, ansi_enabled)
84
186
  when "state"
85
187
  if all_mode
86
188
  usage_abort("usage: report-screen state --all <store_root>") unless target
87
189
  store_root = File.expand_path(target)
88
190
  usage_abort("#{store_root} is not a store (no INDEX.md)") unless File.exist?(File.join(store_root, "INDEX.md"))
89
191
  out = ReportScreen.render_roster(store_root, changed: changed)
90
- $stdout.write paint(out, ansi_enabled, renderer_path)
192
+ $stdout.write paint(out, ansi_enabled)
91
193
  else
92
194
  usage_abort("usage: report-screen state <intent_dir> [--changed \"<text>\"]") unless target
93
195
  intent_dir = File.expand_path(target)
@@ -97,23 +199,83 @@ when "state"
97
199
  usage_abort("template not found at #{template_path}") unless File.exist?(template_path)
98
200
  out = ReportScreen.render_state(intent_dir: intent_dir, store_root: store_root, changed: changed,
99
201
  template: File.read(template_path))
100
- $stdout.write paint(out, ansi_enabled, renderer_path)
202
+ $stdout.write paint(out, ansi_enabled)
101
203
  end
102
204
  when "delivered"
103
205
  usage_abort("usage: report-screen delivered <intent_dir>") unless target
104
206
  intent_dir = File.expand_path(target)
105
207
  usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
106
- repo_root = File.expand_path("..", __dir__)
107
- out = ReportScreen.render_delivered(intent_dir: intent_dir, tag_reader: git_tag_reader(repo_root))
108
- $stdout.write paint(out, ansi_enabled, renderer_path)
208
+ out = ReportScreen.render_delivered(intent_dir: intent_dir,
209
+ tag_reader: git_tag_reader(explicit_repo: repo_flag, script_dir: __dir__),
210
+ branch_reader: git_branch_reader(explicit_repo: repo_flag, script_dir: __dir__))
211
+ $stdout.write paint(out, ansi_enabled)
109
212
  when "delay"
110
213
  usage_abort("usage: report-screen delay <intent_dir>") unless target
111
214
  intent_dir = File.expand_path(target)
112
215
  usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
113
216
  out = ReportScreen.render_delay(intent_dir: intent_dir)
114
- $stdout.write paint(out, ansi_enabled, renderer_path)
217
+ $stdout.write paint(out, ansi_enabled)
218
+ when "session"
219
+ usage_abort("usage: report-screen session <tier_root> [--session <id>] [--since <iso>] [--ledger-root <dir>] [--ansi]") unless target
220
+ store_root = File.expand_path(target)
221
+ usage_abort("#{store_root} is not a store (no INDEX.md)") unless File.exist?(File.join(store_root, "INDEX.md"))
222
+
223
+ now = Time.now
224
+ session_id = session_flag || ENV[SessionLedger::SESSION_ID_ENV_KEY]
225
+ ledger_root = ledger_root_flag ? File.expand_path(ledger_root_flag) : ReportScreen.default_ledger_root(store_root)
226
+
227
+ if since_flag
228
+ begin
229
+ Time.parse(since_flag)
230
+ rescue ArgumentError
231
+ usage_abort("--since needs an ISO timestamp, got #{since_flag.inspect}")
232
+ end
233
+ end
234
+
235
+ dirs, skipped = ReportScreen.session_delivered_dirs(ledger_root: ledger_root, tier_root: store_root,
236
+ session: session_id, since: since_flag, now: now)
237
+ # D17: the widened window is announced whether the id was missing or simply
238
+ # matched nothing. A resumed background job carries a session id that is not
239
+ # the one on the ledger lines, and that case must not answer silently. An
240
+ # explicit --since is the caller naming the window, so it needs no note.
241
+ note =
242
+ if since_flag
243
+ nil
244
+ elsif session_id.nil? || session_id.to_s.strip.empty?
245
+ ReportScreen.window_note(ReportScreen.fallback_day(ledger_root, SessionLedger.day_id(now)),
246
+ "no session id given")
247
+ elsif !ReportScreen.session_tagged?(ledger_root: ledger_root, session: session_id, now: now)
248
+ ReportScreen.window_note(ReportScreen.fallback_day(ledger_root, SessionLedger.day_id(now)),
249
+ "this session has no line in the day ledger")
250
+ end
251
+
252
+ out = ReportScreen.render_session(
253
+ dirs: dirs, skipped: skipped, store_root: store_root,
254
+ tag_reader: git_tag_reader(explicit_repo: nil, script_dir: __dir__),
255
+ branch_reader: git_branch_reader(explicit_repo: nil, script_dir: __dir__),
256
+ note: note, now: now, painter: ->(text) { paint(text, ansi_enabled) }
257
+ )
258
+ $stdout.write out
259
+ when "roadmap"
260
+ usage_abort("usage: report-screen roadmap <roadmap.md> plan|state|delivered [--ansi] [--store-root <dir>]") if positional.empty?
261
+ roadmap_path = File.expand_path(positional[0])
262
+ usage_abort("#{roadmap_path} does not exist") unless File.exist?(roadmap_path)
263
+ sub_verb = positional[1]
264
+ usage_abort("usage: report-screen roadmap <roadmap.md> plan|state|delivered") unless sub_verb
265
+ unless %w[plan state delivered].include?(sub_verb)
266
+ usage_abort("unknown sub-verb #{sub_verb.inspect} (use plan|state|delivered)")
267
+ end
268
+ roadmap_store_root = store_root_flag ? File.expand_path(store_root_flag) : nil
269
+ out = ReportScreen.render_roadmap(path: roadmap_path, verb: sub_verb, store_root: roadmap_store_root)
270
+ $stdout.write paint(out, ansi_enabled)
271
+ when "archive"
272
+ usage_abort("usage: report-screen archive <store_root>") unless target
273
+ store_root = File.expand_path(target)
274
+ usage_abort("#{store_root} is not a store (no INDEX.md)") unless File.exist?(File.join(store_root, "INDEX.md"))
275
+ out = ReportScreen.render_archive(store_root)
276
+ $stdout.write paint(out, ansi_enabled)
115
277
  else
116
- usage_abort("unknown verb #{verb.inspect} (use state|delivered|delay)")
278
+ usage_abort("unknown verb #{verb.inspect} (use plan|state|delivered|delay|session|roadmap|archive)")
117
279
  end
118
280
 
119
281
  exit 0
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ # roadmap-graph - the CLI over the roadmap graph model (intent 337, G4):
6
+ # `check`, `render`, `migrate`, every verb honouring --dry-run.
7
+ #
8
+ # Usage:
9
+ # roadmap-graph check <roadmap.md> [--dry-run]
10
+ # roadmap-graph render <roadmap.md> [--dry-run]
11
+ # roadmap-graph migrate <roadmap.md> [--dry-run]
12
+ #
13
+ # Exit codes:
14
+ # 0 - printed / rendered / migrated cleanly (or a migrate skip: the
15
+ # roadmap already carries a graph)
16
+ # 1 - a real finding: a cyclic graph, a dangling id, or a render/migrate
17
+ # refusal (no ## Graph, no grouping heading)
18
+ # 2 - usage: no argument, an unknown verb, or a missing/non-file path
19
+
20
+ require_relative "lib/roadmap_graph"
21
+ require_relative "lib/roadmap_render"
22
+ require_relative "lib/roadmap_migration"
23
+
24
+ module RoadmapGraphCli
25
+ module_function
26
+
27
+ VERBS = %w[check render migrate].freeze
28
+
29
+ def usage
30
+ warn "Usage: roadmap-graph <#{VERBS.join('|')}> <roadmap.md> [--dry-run]"
31
+ end
32
+
33
+ def main(argv)
34
+ args = argv.dup
35
+ dry_run = !!args.delete("--dry-run")
36
+
37
+ verb = args.shift
38
+ unless verb && VERBS.include?(verb)
39
+ warn "roadmap-graph: unknown verb #{verb.inspect} (use #{VERBS.join(', ')})" if verb
40
+ usage
41
+ return 2
42
+ end
43
+
44
+ path = args.shift
45
+ unless path && File.file?(path)
46
+ warn "roadmap-graph: #{path.inspect} is not a file"
47
+ return 2
48
+ end
49
+
50
+ case verb
51
+ when "check" then check(path)
52
+ when "render" then render(path, dry_run)
53
+ when "migrate" then migrate(path, dry_run)
54
+ end
55
+ end
56
+
57
+ def check(path)
58
+ result = RoadmapGraph.analyze(path)
59
+ unless result[:has_graph]
60
+ warn "roadmap-graph check: #{result[:reason]}"
61
+ return 1
62
+ end
63
+ if result[:cycle]
64
+ warn "roadmap-graph check: cyclic graph: #{result[:cycle].join(' > ')}"
65
+ return 1
66
+ end
67
+ if result[:reason]
68
+ warn "roadmap-graph check: #{result[:reason]}"
69
+ return 1
70
+ end
71
+
72
+ puts "Batches: #{result[:batches].map { |b| b.join(', ') }.join(' | ')}"
73
+ puts "Ready: #{result[:ready].join(', ')}"
74
+ puts "Dead ends: #{result[:dead_ends].join(', ')}" unless result[:dead_ends].empty?
75
+
76
+ unless result[:dangling].empty?
77
+ warn "roadmap-graph check: graph names #{result[:dangling].map(&:inspect).join(', ')}, no batch entry"
78
+ return 1
79
+ end
80
+
81
+ 0
82
+ end
83
+
84
+ def render(path, dry_run)
85
+ result = RoadmapRender.write(path, dry_run: dry_run)
86
+ unless result[:ok]
87
+ warn "roadmap-graph render: #{result[:error]}"
88
+ return 1
89
+ end
90
+
91
+ if dry_run
92
+ puts result[:content]
93
+ else
94
+ puts "roadmap-graph render: wrote #{path}"
95
+ end
96
+ 0
97
+ end
98
+
99
+ def migrate(path, dry_run)
100
+ result = RoadmapMigration.write(path, dry_run: dry_run)
101
+ unless result[:ok]
102
+ if result[:skipped]
103
+ puts "roadmap-graph migrate: #{result[:reason]}"
104
+ return 0
105
+ end
106
+ warn "roadmap-graph migrate: #{result[:reason]}"
107
+ return 1
108
+ end
109
+
110
+ if dry_run
111
+ puts result[:content]
112
+ else
113
+ puts "roadmap-graph migrate: wrote #{path}"
114
+ end
115
+ 0
116
+ end
117
+ end
118
+
119
+ exit(RoadmapGraphCli.main(ARGV)) if $PROGRAM_NAME == __FILE__