@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
@@ -21,12 +21,26 @@
21
21
  #
22
22
  # Read-only. Never modifies files.
23
23
 
24
+ require_relative "lib/store_layout"
24
25
  require "json"
25
26
  require "yaml"
26
27
  require "date"
27
28
  require_relative "doctor"
28
29
  require_relative "lib/savepoint"
29
30
  require_relative "lib/lock"
31
+ require_relative "lib/dashboard_screen"
32
+ require_relative "lib/intent_screen"
33
+ require_relative "lib/report_screen"
34
+ require_relative "lib/roadmap_queue"
35
+ require_relative "lib/day_summary"
36
+ require_relative "lib/screen_paint"
37
+ require_relative "lib/ready_set"
38
+
39
+ # Intent 331a (D6/R8): every caller-added screen kind file registers itself on
40
+ # load, so this glob is the only wiring a new kind needs (scripts/report-screen:42
41
+ # does the identical glob for the same reason). Sorted for a deterministic load
42
+ # order; tolerates the directory being absent or empty.
43
+ Dir.glob(File.join(__dir__, "lib", "screens", "*.rb")).sort.each { |f| require_relative f }
30
44
 
31
45
  PLASTIC_HOME = ENV.fetch("PLASTIC_HOME") { File.join(Dir.home, ".plastic") }
32
46
 
@@ -87,7 +101,7 @@ end
87
101
  # instead, with no end-of-line requirement, so trailing note prose is irrelevant. The
88
102
  # separator there may be a real em dash (U+2014, INDEX.md's normal on-write convention,
89
103
  # built from the codepoint so this source line stays em-dash free) or a plain hyphen
90
- # (end-intent's own Bridge.index_entry_match accepts either on read). Because regex
104
+ # (end-intent's own IndexEntry.match accepts either on read). Because regex
91
105
  # alternation is leftmost-first, this only ever matches the date immediately after the
92
106
  # link. It never continues scanning into the note prose, so a second date mentioned
93
107
  # later in a note's free text cannot be mistaken for the completion date. The id class
@@ -130,13 +144,13 @@ def completion_dates(index_path)
130
144
  end
131
145
 
132
146
  # All stores: global + every registered project. -> [{scope, store, index}]
133
- def stores
147
+ def stores(home = PLASTIC_HOME)
134
148
  list = []
135
- global = File.join(PLASTIC_HOME, "store")
136
- list << { scope: "global", store: global, index: File.join(PLASTIC_HOME, "INDEX.md") } if File.directory?(global)
137
- projects_root = File.join(PLASTIC_HOME, "projects")
149
+ global = Plastic::StoreLayout.global_store(home)
150
+ list << { scope: "global", store: global, index: File.join(Plastic::StoreLayout.global_root(home), "INDEX.md") } if File.directory?(global)
151
+ projects_root = Plastic::StoreLayout.projects_root(home)
138
152
  if File.directory?(projects_root)
139
- Dir.children(projects_root).sort.each do |proj|
153
+ Plastic::StoreLayout.project_slugs(home).each do |proj|
140
154
  store = File.join(projects_root, proj, "store")
141
155
  next unless File.directory?(store)
142
156
  list << { scope: "project:#{proj}", store: store, index: File.join(projects_root, proj, "INDEX.md") }
@@ -256,9 +270,27 @@ def parse_intent(store_info, dir_name, status_index)
256
270
  # durable lock beside the intent. intent_line deliberately does not expose
257
271
  # this filesystem path in --data or any rendered surface.
258
272
  intent_dir: File.expand_path(dir),
273
+ # Intent 336 (G3, D14): the ready node count for a graph-shaped intent,
274
+ # read through ReadySet, nil for every other intent. Explicitly advisory,
275
+ # entirely separate from the sources-derived "unblocked" flag below,
276
+ # which is the cross-intent knowledge graph and stays exactly as it was
277
+ # (327 D40 forbids that field from gating work). ReadySet.analyze is
278
+ # called only when real.("graph.md") is true, so the cost of this read
279
+ # is zero for every intent that has no graph.
280
+ ready_node_count: real.("graph.md") ? ready_node_count_for(dir) : nil,
259
281
  }
260
282
  end
261
283
 
284
+ # The count of currently-ready nodes in a graph-shaped intent's own graph.md,
285
+ # or nil when ReadySet cannot analyze it (a malformed or cyclic graph never
286
+ # raises out of the dashboard).
287
+ def ready_node_count_for(dir)
288
+ analysis = ReadySet.analyze(dir)
289
+ return nil unless analysis[:ok]
290
+
291
+ analysis[:nodes].count { |_, view| view[:ready] }
292
+ end
293
+
262
294
  def checklist_partially_done?(path)
263
295
  txt = File.read(path)
264
296
  checked = txt.scan(/^\s*- \[x\]/i).size
@@ -561,7 +593,7 @@ def render_continue(records)
561
593
  out.concat(matrix(open, scope_tag: true))
562
594
  out << ""
563
595
  out << LEGEND
564
- out << "ask for the <slug> project board · plastic-auto (works the dispatchable queue)"
596
+ out << "ask for the <slug> project board · plastic auto take ID (works the dispatchable queue)"
565
597
  out.join("\n") + "\n"
566
598
  end
567
599
 
@@ -670,15 +702,10 @@ end
670
702
  # recent_delivery_summary (D1 fix) uses this twice: once per intent label, so a
671
703
  # paragraph-long `intent` field collapses to a short name, and once on the fully assembled
672
704
  # summary string, the hard budget cap that holds no matter how the per-label math adds up.
705
+ # Intent 331f: the one implementation now lives on ReportScreen; this delegates so dashboard.rb
706
+ # and every ReportScreen render entry point share it.
673
707
  def truncate_on_word_boundary(text, max_chars)
674
- t = text.to_s
675
- return t if t.length <= max_chars
676
- ellipsis = "…"
677
- limit = [max_chars - ellipsis.length, 0].max
678
- slice = t[0, limit]
679
- cut = slice.rindex(/\s/)
680
- slice = slice[0, cut] if cut && cut.positive?
681
- "#{slice.rstrip}#{ellipsis}"
708
+ ReportScreen.truncate_on_word_boundary(text, max_chars)
682
709
  end
683
710
 
684
711
  # D3 fix (intent 202 gate review): completion dates only carry day granularity, and many
@@ -821,6 +848,7 @@ def next_work(records, cap: NEXT_WORK_CAP)
821
848
  { id: r[:id], intent: r[:intent], scope: r[:scope], lifecycle: r[:lifecycle],
822
849
  value: r[:value].to_s, disposition: r[:disposition], flags: r[:flags],
823
850
  what: cell(text), flags_label: cell(Array(r[:flags]).join(", ")),
851
+ ready_node_count: r[:ready_node_count],
824
852
  line: "#{r[:id]} #{text}" }
825
853
  end
826
854
  end
@@ -828,7 +856,7 @@ end
828
856
  def short_description(scope)
829
857
  return "" unless scope.start_with?("project:")
830
858
  slug = scope.sub("project:", "")
831
- agents = File.join(PLASTIC_HOME, "projects", slug, "AGENTS.md")
859
+ agents = File.join(Plastic::StoreLayout.project_root(PLASTIC_HOME, slug), "AGENTS.md")
832
860
  if File.exist?(agents)
833
861
  File.readlines(agents).each do |l|
834
862
  t = l.strip
@@ -969,6 +997,211 @@ def render_json(records, scope_label)
969
997
  }
970
998
  end
971
999
 
1000
+ # ---------------------------------------------------------------------------
1001
+ # Screen renderer (intent 331d) - dashboard.rb continue|project <slug> --screen.
1002
+ #
1003
+ # Sources every fact through the same helpers report-screen and DaySummary
1004
+ # already use (Resolved contract, ACTION_1): a missing source prints "not
1005
+ # recorded" or "none", never a guess or a crash (R1). The classification
1006
+ # pipeline above (classify, rank_key, actionable?, QUADRANTS, disposition_of)
1007
+ # is read here, never edited: the screen is a new renderer over the same
1008
+ # records (D3), so every count and rank matches what --json already reports
1009
+ # for the identical scope.
1010
+ # ---------------------------------------------------------------------------
1011
+
1012
+ SCREEN_ACTIVE_CAP = 8
1013
+ SCREEN_NEXT_CAP = 6
1014
+ SCREEN_NOT_RECORDED = "not recorded"
1015
+
1016
+ def screen_scope_slug(scope)
1017
+ scope.sub(/\Aproject:/, "")
1018
+ end
1019
+
1020
+ # Tier root: PLASTIC_HOME for "global", PLASTIC_HOME/projects/<slug> for
1021
+ # "project:<slug>" (Resolved contract) - the same tier scripts/lib/qmd_sync.rb
1022
+ # already derives from a store path. Global's own roadmaps/INDEX.md sit
1023
+ # directly under PLASTIC_HOME, with no intervening "store" segment.
1024
+ def screen_tier_root(plastic_home, scope)
1025
+ return plastic_home if scope == "global"
1026
+ Plastic::StoreLayout.project_root(plastic_home, screen_scope_slug(scope))
1027
+ end
1028
+
1029
+ # "global" spans every store (the same aggregate render_continue and --json's
1030
+ # continue-mode subset already read); a project scope narrows to its own
1031
+ # records. Never a project-only helper on the unscoped path (D10).
1032
+ def screen_scoped_records(records, scope)
1033
+ return records if scope == "global"
1034
+ records.select { |r| r[:scope] == scope }
1035
+ end
1036
+
1037
+ def screen_active_count(scoped)
1038
+ scoped.count { |r| r[:status] == "active" }
1039
+ end
1040
+
1041
+ # D2: a stale or absent lock never counts as delivering.
1042
+ def screen_in_delivery_count(scoped, now:)
1043
+ scoped.count do |r|
1044
+ r[:status] == "active" && Lock.who(r[:intent_dir], now: now)["state"] == "fresh"
1045
+ end
1046
+ end
1047
+
1048
+ # D3: completed_on (the INDEX.md completion date) is the source of truth,
1049
+ # rec[:done_at] (the savepoint's own Done timestamp) the fallback when a
1050
+ # completed intent has no dated INDEX entry yet. `created` is never read.
1051
+ def screen_completion_date(rec)
1052
+ raw = rec[:completed_on].to_s
1053
+ raw = rec[:done_at].to_s if raw.empty?
1054
+ return nil if raw.empty?
1055
+ begin
1056
+ Date.parse(raw)
1057
+ rescue ArgumentError, TypeError
1058
+ nil
1059
+ end
1060
+ end
1061
+
1062
+ def screen_delivered_count(scoped, now:)
1063
+ today_date = now.to_date
1064
+ scoped.count do |r|
1065
+ next false unless r[:status] == "completed"
1066
+ date = screen_completion_date(r)
1067
+ next false unless date
1068
+ diff = (today_date - date).to_i
1069
+ diff >= 0 && diff <= 7
1070
+ end
1071
+ end
1072
+
1073
+ # "none" on state none/tie/exhausted or a missing roadmaps dir (R1, D4); the
1074
+ # live frontier otherwise (D17). RoadmapQueue itself tolerates an absent
1075
+ # directory (Dir.exist? guard), so this never crashes on a bare tier.
1076
+ def screen_roadmap_field(plastic_home, scope, now:)
1077
+ tier = screen_tier_root(plastic_home, scope)
1078
+ reader = RoadmapQueue.new(roadmaps_dir: File.join(tier, "roadmaps"),
1079
+ index_path: File.join(tier, "INDEX.md"), now: now)
1080
+ payload = reader.which
1081
+ return "none" if %w[none tie exhausted].include?(payload["state"])
1082
+ "#{payload['roadmap']} · #{payload['frontier_wave']}"
1083
+ end
1084
+
1085
+ # Sessions are global, never per-project (Resolved contract): always the
1086
+ # PLASTIC_HOME/store tmp root, and `session: nil` (A6) so the calling
1087
+ # session's own live heartbeat counts rather than being excluded as "self".
1088
+ def screen_sessions_count(plastic_home, now:)
1089
+ store = Plastic::StoreLayout.global_store(plastic_home)
1090
+ DaySummary.active_sessions(store, nil, now: now, ttl: DaySummary::HEARTBEAT_TTL).size
1091
+ end
1092
+
1093
+ def screen_changed_field(scoped)
1094
+ latest = scoped.map { |r| r[:last_accessed_at].to_s }.reject(&:empty?).max
1095
+ return SCREEN_NOT_RECORDED unless latest
1096
+ t = begin
1097
+ Time.parse(latest)
1098
+ rescue ArgumentError, TypeError
1099
+ nil
1100
+ end
1101
+ return SCREEN_NOT_RECORDED unless t
1102
+ t.utc.strftime("%Y-%m-%d %H:%M UTC")
1103
+ end
1104
+
1105
+ # D6, intent 331f: the one Lead freshness rule every screen shares - fresh prints
1106
+ # "agent · key", a stale lock prints "stale · N min" (never "not recorded" or a bare "idle",
1107
+ # which would either hide the staleness or contradict a live In-delivery count with a dead
1108
+ # lead), and no lock (or one that will not read) prints "idle".
1109
+ def screen_lead_field(rec, now:)
1110
+ ReportScreen.lead_cell(rec[:intent_dir], now: now)
1111
+ end
1112
+
1113
+ # D3 (331d1): the owner ruled no rendered row exceeds 115 visible columns.
1114
+ # The bound is on the WHOLE pipe-delimited row, not on one cell, so the
1115
+ # Intent cell gets whatever the other cells leave it. A cell short enough on
1116
+ # its own still drifts the row past the bound once the bar, the lead and the
1117
+ # scaffolding are added, which is the failure this measures away.
1118
+ SCREEN_ROW_MAX_COLUMNS = 115
1119
+
1120
+ # D2 (331d1): the Intent cell carries the intent line up to but not including
1121
+ # its first colon. A Plastic intent line opens with a short name and then
1122
+ # explains itself after a colon, so the lead IS the name; a line with no
1123
+ # colon is already a name and passes through whole. Escaping happens here, so
1124
+ # the budget below measures what actually reaches the row.
1125
+ def screen_intent_title(rec)
1126
+ # D8: one colon rule for every screen, on ReportScreen. `max:` is the whole line here
1127
+ # because screen_fit_intent does this cell's own width budgeting a moment later.
1128
+ cell(ReportScreen.title_before_colon(rec[:intent], max: rec[:intent].to_s.length + 1))
1129
+ end
1130
+
1131
+ # The Intent cell fitted to what the row has left. `others` are the already
1132
+ # rendered sibling cells; the scaffolding is the leading "| ", a " | " between
1133
+ # every pair of cells, and the trailing " |". Intent 331f1: the one implementation now lives
1134
+ # on ReportScreen (fit_row_cell), measured in DISPLAY columns rather than String#length - an
1135
+ # `others` cell carrying a progress bar costs two columns per glyph, not one (RC1) - so
1136
+ # roadmap_state_entries_table's own Intent cell spends the same budget by the same rule.
1137
+ def screen_fit_intent(title, others)
1138
+ ReportScreen.fit_row_cell(title, others, max: SCREEN_ROW_MAX_COLUMNS)
1139
+ end
1140
+
1141
+ # D6: last_accessed_at descending, then id, capped at SCREEN_ACTIVE_CAP.
1142
+ def screen_where_we_are(scoped, now:)
1143
+ active = scoped.select { |r| r[:status] == "active" }
1144
+ ordered = active.sort_by { |r| [invert_ts(r[:last_accessed_at]), r[:id]] }
1145
+ ordered.first(SCREEN_ACTIVE_CAP).map do |r|
1146
+ items = IntentScreen.checklist_items(r[:intent_dir])
1147
+ progress = IntentScreen.progress_fields(items)
1148
+ graph_id = r[:id].to_s
1149
+ stage = r[:lifecycle].to_s.capitalize
1150
+ bar = "#{progress['progress.bar']} #{progress['progress.done']} / #{progress['progress.total']}"
1151
+ lead = screen_lead_field(r, now: now)
1152
+ {
1153
+ graph_id: graph_id,
1154
+ intent: screen_fit_intent(screen_intent_title(r), [graph_id, stage, bar, lead]),
1155
+ stage: stage,
1156
+ progress: bar,
1157
+ lead: lead,
1158
+ }
1159
+ end
1160
+ end
1161
+
1162
+ # A2: the exact pool render_json's dispatchable_queue ranks - actionable?
1163
+ # records, rank_key order, filtered to disposition defer/research (D12
1164
+ # excludes drive/triage). Rank is that queue's own 1-based position, so a
1165
+ # capped display row's rank always agrees with the uncapped --json contract.
1166
+ def screen_dispatchable_pool(scope_records)
1167
+ ranked = scope_records.select { |r| actionable?(r) }.sort_by { |r| rank_key(r) }
1168
+ ranked.select { |r| %w[defer research].include?(r[:disposition]) }
1169
+ end
1170
+
1171
+ def screen_where_we_go_next(scope_records)
1172
+ pool = screen_dispatchable_pool(scope_records)
1173
+ pool.each_with_index.map do |r, i|
1174
+ rank = i + 1
1175
+ graph_id = r[:id].to_s
1176
+ reason = r[:quadrant].to_s
1177
+ {
1178
+ rank: rank,
1179
+ graph_id: graph_id,
1180
+ intent: screen_fit_intent(screen_intent_title(r), [rank.to_s, graph_id, reason]),
1181
+ reason: reason,
1182
+ }
1183
+ end.first(SCREEN_NEXT_CAP)
1184
+ end
1185
+
1186
+ def screen_fields(records, scope, plastic_home:, now: Time.now)
1187
+ scoped = screen_scoped_records(records, scope)
1188
+ {
1189
+ scope: scope,
1190
+ active: screen_active_count(scoped),
1191
+ in_delivery: screen_in_delivery_count(scoped, now: now),
1192
+ delivered: screen_delivered_count(scoped, now: now),
1193
+ roadmap: screen_roadmap_field(plastic_home, scope, now: now),
1194
+ sessions: screen_sessions_count(plastic_home, now: now),
1195
+ changed: screen_changed_field(scoped),
1196
+ where_we_are: screen_where_we_are(scoped, now: now),
1197
+ where_we_go_next: screen_where_we_go_next(scoped),
1198
+ }
1199
+ end
1200
+
1201
+ def render_screen(records, scope, plastic_home: PLASTIC_HOME, now: Time.now)
1202
+ DashboardScreen.render(screen_fields(records, scope, plastic_home: plastic_home, now: now))
1203
+ end
1204
+
972
1205
  # ---------------------------------------------------------------------------
973
1206
  # CLI
974
1207
  # ---------------------------------------------------------------------------
@@ -997,6 +1230,8 @@ def main(argv)
997
1230
  json = argv.delete("--json")
998
1231
  data = argv.delete("--data")
999
1232
  plain = argv.delete("--plain")
1233
+ screen = argv.delete("--screen")
1234
+ ansi = argv.delete("--ansi")
1000
1235
  all = argv.delete("--all")
1001
1236
  limit_active = extract_flag_value(argv, "--limit-active")
1002
1237
  limit_next = extract_flag_value(argv, "--limit-next")
@@ -1047,6 +1282,22 @@ def main(argv)
1047
1282
  return 0
1048
1283
  end
1049
1284
 
1285
+ if screen
1286
+ if mode == "project" && (slug.nil? || slug.empty?)
1287
+ warn "usage: dashboard.rb project <slug> --screen"
1288
+ return 2
1289
+ end
1290
+ scope = mode == "project" ? "project:#{slug}" : "global"
1291
+ text = render_screen(records, scope, plastic_home: PLASTIC_HOME, now: Time.now)
1292
+ # A4: the identical capability guard scripts/report-screen:161 applies -
1293
+ # NO_COLOR always wins to plain; a non-tty stdout stays plain unless the
1294
+ # PLASTIC_FORCE_COLOR test seam is set.
1295
+ ansi_enabled = ansi && ENV["NO_COLOR"].to_s.empty? &&
1296
+ ($stdout.tty? || ENV["PLASTIC_FORCE_COLOR"] == "1")
1297
+ print(ansi_enabled ? (ScreenPaint.paint(text, color: true) || text) : text)
1298
+ return 0
1299
+ end
1300
+
1050
1301
  case mode
1051
1302
  when "continue" then print render_continue(records)
1052
1303
  when "project"
@@ -14,6 +14,7 @@
14
14
  #
15
15
  # Exit codes: 0 done; 2 usage error.
16
16
 
17
+ require_relative "lib/store_layout"
17
18
  require_relative "lib/session_ledger"
18
19
  require_relative "lib/day_summary"
19
20
 
@@ -43,7 +44,7 @@ end
43
44
 
44
45
  opts = parse_args(ARGV)
45
46
  home = opts[:home] ? expand(opts[:home]) : expand(ENV.fetch("PLASTIC_HOME", "~/.plastic"))
46
- store = opts[:store] ? expand(opts[:store]) : File.join(home, "store")
47
+ store = opts[:store] ? expand(opts[:store]) : Plastic::StoreLayout.global_store(home)
47
48
  day = opts[:day] || SessionLedger.day_id
48
49
  usage_abort("--day must be eight digits parsing as a real date, got #{day.inspect}") unless SessionLedger.valid_day_id?(day)
49
50
  session = SessionLedger.short_session_id(opts[:session], ENV["CLAUDE_CODE_SESSION_ID"])