@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,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,15 +2,22 @@
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
22
  # --ansi delegates to ScreenPaint (intent 317a, D1), the parser/re-layouter
16
23
  # in the shared TUI core. Selection is by capability, never by harness:
@@ -23,24 +30,93 @@
23
30
  # 2 - usage error, or the path/store is not what the verb expects (one line
24
31
  # on stderr, stdout empty)
25
32
 
33
+ require "yaml"
34
+ require "shellwords"
26
35
  require_relative "lib/report_screen"
27
36
  require_relative "lib/intent_screen"
28
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 }
29
46
 
30
47
  def usage_abort(message)
31
48
  warn "report-screen: #{message}"
32
49
  exit 2
33
50
  end
34
51
 
35
- # The one place allowed to shell out (D2/D20): resolves the nearest git tag
36
- # reachable from the repo this script lives in, so `render_delivered`'s
37
- # version field and the `ship` evidence row are never invented. The pure
38
- # module never reads git itself; this is injected as `tag_reader:`.
39
- def git_tag_reader(repo_root)
40
- lambda do |_intent_dir|
41
- return nil unless File.exist?(File.join(repo_root, ".git"))
42
- tag = `git -C #{repo_root} describe --tags --abbrev=0 2>/dev/null`.strip
43
- 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
44
120
  end
45
121
  end
46
122
 
@@ -49,6 +125,11 @@ verb = args.shift
49
125
  changed = nil
50
126
  ansi = false
51
127
  template_path = nil
128
+ repo_flag = nil
129
+ session_flag = nil
130
+ since_flag = nil
131
+ ledger_root_flag = nil
132
+ store_root_flag = nil
52
133
  positional = []
53
134
 
54
135
  while (arg = args.shift)
@@ -60,15 +141,25 @@ while (arg = args.shift)
60
141
  ansi = true
61
142
  when "--all"
62
143
  positional << "--all"
144
+ when "--repo"
145
+ repo_flag = args.shift or usage_abort("--repo needs a path")
63
146
  when "--template"
64
147
  template_path = args.shift or usage_abort("--template 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
@@ -82,6 +173,16 @@ def paint(text, ansi_enabled, _renderer_path = nil)
82
173
  end
83
174
 
84
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)
85
186
  when "state"
86
187
  if all_mode
87
188
  usage_abort("usage: report-screen state --all <store_root>") unless target
@@ -104,8 +205,9 @@ when "delivered"
104
205
  usage_abort("usage: report-screen delivered <intent_dir>") unless target
105
206
  intent_dir = File.expand_path(target)
106
207
  usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
107
- repo_root = File.expand_path("..", __dir__)
108
- out = ReportScreen.render_delivered(intent_dir: intent_dir, tag_reader: git_tag_reader(repo_root))
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__))
109
211
  $stdout.write paint(out, ansi_enabled)
110
212
  when "delay"
111
213
  usage_abort("usage: report-screen delay <intent_dir>") unless target
@@ -113,8 +215,67 @@ when "delay"
113
215
  usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
114
216
  out = ReportScreen.render_delay(intent_dir: intent_dir)
115
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)
116
277
  else
117
- 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)")
118
279
  end
119
280
 
120
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__
@@ -18,6 +18,7 @@
18
18
  # rebuilds an empty ledger.
19
19
 
20
20
  require_relative "lib/roadmap_savepoint"
21
+ require_relative "lib/guarded_append"
21
22
 
22
23
  def opt(args, name)
23
24
  (i = args.index(name)) && args[i + 1]
@@ -47,6 +48,12 @@ when "append"
47
48
  rescue ArgumentError => e
48
49
  warn "append: #{e.message}"
49
50
  exit 2
51
+ rescue GuardedAppend::Unavailable => e
52
+ # Intent 335 (spec D14): the write guard could not be taken; nothing was written. Exit 4
53
+ # rather than let a backtrace reach the six skill recipes that drive this CLI - it rescues
54
+ # ArgumentError alone today and would otherwise print one straight to the terminal.
55
+ warn "append: #{e.message}"
56
+ exit 4
50
57
  end
51
58
 
52
59
  when "rebuild"