@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
@@ -21,6 +21,7 @@
21
21
  # Exit codes: 0 promoted (prints the intent dir last), 1 new-intent failed,
22
22
  # 2 usage error or nothing to promote.
23
23
 
24
+ require_relative "lib/store_layout"
24
25
  require "open3"
25
26
  require "yaml"
26
27
  require_relative "lib/session_ledger"
@@ -61,7 +62,7 @@ usage_abort("--day must be a real calendar date, YYYYMMDD") unless SessionLedger
61
62
  usage_abort("--match is required") if opts[:match].nil? || opts[:match].empty?
62
63
 
63
64
  plastic_home = File.expand_path(opts[:plastic_home] || ENV.fetch("PLASTIC_HOME", "~/.plastic"))
64
- store = opts[:store] || File.join(plastic_home, "store")
65
+ store = opts[:store] || Plastic::StoreLayout.global_store(plastic_home)
65
66
  templates = opts[:templates] || File.expand_path("../templates", __dir__)
66
67
  session = opts[:session] ? SessionLedger.short_session_id(opts[:session]) : nil
67
68
 
@@ -69,7 +70,7 @@ target_store = store
69
70
  if opts[:project]
70
71
  projects = StoreProvisioning.load_projects(plastic_home)
71
72
  usage_abort("--project #{opts[:project]} is not a registered project") unless projects.key?(opts[:project])
72
- target_store = File.join(plastic_home, "projects", opts[:project], "store")
73
+ target_store = File.join(Plastic::StoreLayout.project_root(plastic_home, opts[:project]), "store")
73
74
  end
74
75
 
75
76
  # --- pick the target line ---------------------------------------------------
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
- # Usage: read-config <key> [--default VALUE] [--project PATH]
2
+ # Usage: read-config <key> [--default VALUE] [--project PATH] [--harness claude|codex]
3
3
  # Resolves a config value: project -> global -> built-in defaults.
4
4
  # Nested keys use dot notation: agent.type, architect.style
5
5
  # Returns scalar values as strings, hash/array values as JSON.
@@ -14,9 +14,9 @@ require_relative "lib/agent_models"
14
14
  DEFAULTS = {
15
15
  "version" => 3,
16
16
  "stale_threshold_days" => 3,
17
- # Absolute token counts for a 1M window, 35 and 50 percent (intent 312, 296 D38).
18
- "context_offer_tokens" => 350_000,
19
- "context_insist_tokens" => 500_000,
17
+ # Absolute token counts for a 1M window, 15 and 25 percent (intent 355, n5, D7).
18
+ "context_offer_tokens" => 150_000,
19
+ "context_insist_tokens" => 250_000,
20
20
  "execution_mode" => "subagent-driven",
21
21
  "hash_length" => 6,
22
22
  "hash_algorithm" => "sha256-base36",
@@ -33,6 +33,12 @@ DEFAULTS = {
33
33
  },
34
34
  "architect" => {
35
35
  "style" => nil
36
+ },
37
+ # Intent 340b (G7c, n4, D9): the Stop hook's runtime arm. Off until the
38
+ # owner's C32 ruling; read-config emits it as the string "false", which
39
+ # StopGate parses as a boolean itself rather than trusting.
40
+ "runner" => {
41
+ "stop_hook" => false
36
42
  }
37
43
  }.freeze
38
44
 
@@ -40,6 +46,7 @@ def parse_args(argv)
40
46
  key = nil
41
47
  default_val = nil
42
48
  project_dir = nil
49
+ harness = nil
43
50
 
44
51
  i = 0
45
52
  while i < argv.length
@@ -52,13 +59,17 @@ def parse_args(argv)
52
59
  abort "Error: --project requires a value" unless argv[i + 1]
53
60
  project_dir = argv[i + 1]
54
61
  i += 2
62
+ when "--harness"
63
+ abort "Error: --harness requires a value" unless argv[i + 1]
64
+ harness = argv[i + 1]
65
+ i += 2
55
66
  else
56
67
  key = argv[i]
57
68
  i += 1
58
69
  end
59
70
  end
60
71
 
61
- [key, default_val, project_dir]
72
+ [key, default_val, project_dir, harness]
62
73
  end
63
74
 
64
75
  def load_yaml(path)
@@ -113,10 +124,10 @@ if ARGV.include?("--migrate")
113
124
  exit 0
114
125
  end
115
126
 
116
- key, explicit_default, project_dir = parse_args(ARGV)
127
+ key, explicit_default, project_dir, harness = parse_args(ARGV)
117
128
 
118
129
  if key.nil? || key.empty?
119
- $stderr.puts "Usage: read-config <key> [--default VALUE] [--project PATH]"
130
+ $stderr.puts "Usage: read-config <key> [--default VALUE] [--project PATH] [--harness claude|codex]"
120
131
  exit 1
121
132
  end
122
133
 
@@ -129,9 +140,42 @@ if project_dir
129
140
  project_config = load_yaml(project_config_path)
130
141
  end
131
142
 
132
- value = dig_key(project_config, key)
133
- value = dig_key(global_config, key) if value.nil?
143
+ def canonical_harness(value)
144
+ return nil if value.nil?
145
+ return "claude" if %w[claude claude-code].include?(value.to_s)
146
+ return "codex" if value.to_s == "codex"
147
+
148
+ abort "Error: --harness must be claude or codex"
149
+ end
150
+
151
+ def harness_agent_value(config, key, harness)
152
+ match = key.match(/\Aagents\.(models|efforts)\.([^.]+)\z/)
153
+ return nil unless match
154
+
155
+ section, agent = match.captures
156
+ if section == "models"
157
+ AgentModels.models_section(config, harness: harness)[agent]
158
+ else
159
+ AgentModels.efforts_section(config, harness)[agent]
160
+ end
161
+ end
162
+
163
+ harness = canonical_harness(harness)
164
+ if harness
165
+ value = harness_agent_value(project_config, key, harness)
166
+ value = harness_agent_value(global_config, key, harness) if value.nil?
167
+ else
168
+ value = dig_key(project_config, key)
169
+ value = dig_key(global_config, key) if value.nil?
170
+ end
134
171
  value = explicit_default if value.nil? && explicit_default
172
+ if value.nil? && harness && (match = key.match(/\Aagents\.(models|efforts)\.([^.]+)\z/))
173
+ value = if match[1] == "models"
174
+ AgentModels.shipped_model_for(match[2], harness: harness)
175
+ else
176
+ AgentModels::DEFAULT_EFFORT
177
+ end
178
+ end
135
179
  value = dig_key(DEFAULTS, key) if value.nil?
136
180
 
137
181
  puts format_value(value)
@@ -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