@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
@@ -3,17 +3,21 @@
3
3
  # Usage: hook-session-start <index_path> <plastic_home> <mode> [plugin_root]
4
4
  # Outputs hook JSON for the session-start hook. Exits silently if nothing to show.
5
5
 
6
+ require_relative "lib/store_layout"
6
7
  require "json"
7
8
  require "date"
8
9
  require "yaml"
9
10
  require "fileutils"
10
- require_relative "lib/bridge"
11
- require_relative "lib/savepoint"
12
11
  require_relative "lib/boot_banner"
13
12
  require_relative "lib/qmd_sync"
14
13
  require_relative "lib/doctor_core"
15
14
  require_relative "lib/session_ledger"
16
15
  require_relative "lib/day_summary"
16
+ require_relative "lib/data_boundary"
17
+ require_relative "lib/active_delivery"
18
+ require_relative "lib/runner_core"
19
+ require_relative "lib/runner_watch"
20
+ require_relative "lib/version_number"
17
21
 
18
22
  index_path, plastic_home, mode, plugin_root = ARGV
19
23
  exit 0 unless index_path && plastic_home && mode
@@ -36,147 +40,32 @@ rescue StandardError
36
40
  end
37
41
  payload_session_id = stdin_payload.is_a?(Hash) ? stdin_payload["session_id"].to_s : ""
38
42
 
43
+ # --- subagent marker (intent 355 spec D9, node n7; review fix n8, B6): the
44
+ # stdin payload carries agent_id only when this SessionStart call runs
45
+ # inside a spawned agent. agent_type alone is not enough: a live
46
+ # `claude --agent` session carries agent_type on every turn but never
47
+ # agent_id, so keying on agent_type would boot a live agent session with the
48
+ # core banner only. Read from that payload only, never from an environment
49
+ # variable, so a helper needs the core banner alone. An absent agent_id (the
50
+ # common case) is a live session. Any exception here still boots the banner,
51
+ # never nothing: the rescue falls back to a live session, whose own content
52
+ # already comes from paths this file already guards independently.
53
+ subagent_session = begin
54
+ stdin_payload.is_a?(Hash) && !!stdin_payload["agent_id"]
55
+ rescue StandardError
56
+ false
57
+ end
58
+
39
59
  # Plastic home and the store are two different paths (intent 231). The shim passes
40
60
  # home (~/.plastic) as argument 2; the store lives one level below it. Compose the
41
61
  # store exactly once here, so no later line re-derives it and no path can gain a
42
62
  # doubled store/ segment.
43
- store_dir = File.join(plastic_home, "store")
44
-
45
- # --- Parse INDEX.md ---
46
-
47
- lines = File.readlines(index_path)
48
- active = []
49
- future = []
50
- section = nil
51
-
52
- lines.each do |line|
53
- if line.start_with?("## Active")
54
- section = :active
55
- next
56
- elsif line.start_with?("## Future")
57
- section = :future
58
- next
59
- elsif line.start_with?("## ")
60
- section = nil
61
- next
62
- end
63
-
64
- next unless section && line.strip.start_with?("- [")
65
-
66
- if section == :active
67
- active << line.strip
68
- elsif section == :future
69
- future << line.strip
70
- end
71
- end
72
-
73
- # --- Stage of the single active intent (the /tmp bridge this once derived was removed in 2.0, intent 307) ---
74
-
75
- stage_line = nil
76
- if active.length == 1 && active.first =~ /store\/([\w-]+)\//
77
- dir_name = $1
78
- intent_dir = "#{store_dir}/#{dir_name}"
79
- begin
80
- stage = Savepoint.derive_stage(intent_dir)
81
- missing = Savepoint.missing_for_stage(stage, intent_dir)
82
- missing_str = missing.empty? ? "none" : missing.join(", ")
83
- stage_line = "Stage: #{stage} | Next: #{missing_str}"
84
- rescue StandardError
85
- stage_line = nil
86
- end
87
- end
88
-
89
- # --- Detect stale future intents ---
90
-
91
- stale = []
92
- read_config = if plugin_root && !plugin_root.empty?
93
- "#{plugin_root}/scripts/read-config"
94
- else
95
- File.expand_path("~/.plastic/scripts/read-config")
96
- end
97
- stale_days = `"#{read_config}" stale_threshold_days`.strip.to_i
98
- stale_days = 3 if stale_days == 0
99
-
100
- future.each do |f|
101
- if f =~ /store\/([\w-]+)\//
102
- dir_name = $1
103
- intent_file = "#{store_dir}/#{dir_name}/#{dir_name}.md"
104
- next unless File.exist?(intent_file)
105
-
106
- content = File.read(intent_file)
107
- if content =~ /^created:\s*['"]?(\d{4}-\d{2}-\d{2})/
108
- age = (Date.today - Date.parse($1)).to_i
109
- if age >= stale_days
110
- name = f[/\[([^\]]+)\]/, 1] || "unknown"
111
- stale << { name: name, age: age, entry: f }
112
- end
113
- end
114
- end
115
- end
116
-
117
- # --- Detect current project ---
118
-
119
- current_project = nil
120
- project_active = []
121
- project_future = []
122
-
123
- projects_path = "#{plastic_home}/projects.yml"
124
- if File.exist?(projects_path)
125
- projects = YAML.safe_load(File.read(projects_path)) rescue {}
126
- cwd = Dir.pwd
127
- (projects["projects"] || {}).each do |slug, info|
128
- project_path = File.expand_path(info["path"])
129
- if cwd.start_with?(project_path)
130
- current_project = { "slug" => slug, "parent" => info["parent"], "path" => project_path }
131
- project_index = "#{plastic_home}/projects/#{slug}/INDEX.md"
132
- if File.exist?(project_index)
133
- p_section = nil
134
- File.readlines(project_index).each do |pline|
135
- if pline.start_with?("## Active")
136
- p_section = :active
137
- next
138
- elsif pline.start_with?("## Future")
139
- p_section = :future
140
- next
141
- elsif pline.start_with?("## ")
142
- p_section = nil
143
- next
144
- end
145
- next unless p_section && pline.strip.start_with?("- [")
146
- project_active << pline.strip if p_section == :active
147
- project_future << pline.strip if p_section == :future
148
- end
149
- end
150
- break
151
- end
152
- end
153
- end
154
-
155
- # --- Load PLASTIC.md conventions ---
63
+ store_dir = Plastic::StoreLayout.global_store(plastic_home)
156
64
 
157
- plastic_md_path = "#{plastic_home}/PLASTIC.md"
158
- plastic_md = File.exist?(plastic_md_path) ? File.read(plastic_md_path).strip : nil
159
-
160
- # --- Load deprecations ---
161
-
162
- dep_file = if plugin_root && !plugin_root.empty?
163
- "#{plugin_root}/deprecations.yml"
164
- else
165
- "#{plastic_home}/deprecations.yml"
166
- end
167
-
168
- deprecations = []
169
- if File.exist?(dep_file)
170
- dep_data = YAML.safe_load(File.read(dep_file)) rescue {}
171
- deprecations = dep_data["deprecations"] || []
172
- end
173
-
174
- dismissed_json = `"#{read_config}" deprecations_dismissed`.strip
175
- dismissed = begin
176
- JSON.parse(dismissed_json)
177
- rescue
178
- []
179
- end
65
+ # --- Current version ---
66
+ # Read early, before any of the richer INDEX/project parsing below, so the
67
+ # core banner (the one thing every session must get) never depends on
68
+ # anything that can raise.
180
69
 
181
70
  current_version = nil
182
71
  version_file = "#{plastic_home}/VERSION"
@@ -190,23 +79,6 @@ elsif plugin_root && !plugin_root.empty?
190
79
  end
191
80
  end
192
81
 
193
- active_deprecations = deprecations.select do |dep|
194
- next true if dep["severity"] == "critical"
195
- next true if current_version && dep["removal"] == current_version
196
- !dismissed.include?(dep["id"])
197
- end
198
-
199
- # --- Check for available updates (from previous session's check) ---
200
-
201
- update_notice = nil
202
- cache_file = "#{plastic_home}/.cache/update-check.json"
203
- if File.exist?(cache_file)
204
- cache = JSON.parse(File.read(cache_file)) rescue {}
205
- if cache["updateAvailable"]
206
- update_notice = "Plastic update available: #{cache["current"]} -> #{cache["latest"]} — run /plastic-update"
207
- end
208
- end
209
-
210
82
  # --- Run core health in-process (intent 36a) ---
211
83
  # Reuse Doctor's own --core checks so there is one source of truth for "core
212
84
  # health" (no second process spawn, no duplicated check list). Never blocks the
@@ -219,206 +91,372 @@ rescue
219
91
  end
220
92
  core_banner = BootBanner.render(health: core_health, version: current_version)
221
93
 
222
- # --- Assemble session context ---
223
-
224
- all_active = active + project_active
225
- all_future = future + project_future
226
-
227
94
  # --- Build context ---
228
- # PLASTIC.md conventions and intent context render only when conventions are
229
- # installed. Deprecation warnings and update notices render regardless, so a
230
- # partial install (or a missing PLASTIC.md) still surfaces critical warnings.
231
-
232
- parts = []
233
-
234
- # Boot banner first: every session start surfaces that Plastic loaded, with the
235
- # version (clean) or the first failing core check (degraded). Owned by the hook
236
- # so it runs by construction — the plastic-intent-continuing skill no longer does this.
237
- parts << core_banner
238
- parts << ""
239
-
240
- # --- QMD search status (intent 45a, READ-ONLY) ---
241
- # Report-only line for the model (additionalContext), never systemMessage and
242
- # never the exit code. QmdSync.status shells out to `qmd collection list`, so the
243
- # whole block is guarded: a 2s timeout caps any hang, and rescue-all guarantees a
244
- # slow/broken/missing qmd appends nothing and the hook continues cleanly.
95
+ # The core banner is the one thing a session always gets. Everything below it
96
+ # (the project/global banner with its active intent, the QMD line,
97
+ # deprecations, the update notice, the first-boot sweep, the day ledger) is
98
+ # best-effort: intent 341, G8 wraps all of it in one rescue so any exception
99
+ # anywhere in that assembly degrades to a banner-only boot, never nothing.
100
+ # That is the same "as today" contract every individual sub-block already
101
+ # kept on its own, made a whole-block guarantee now that the doctrine dump
102
+ # this used to gate everything on is cut (PLASTIC.md's conventions prose, the
103
+ # active-intents listing, the stage line, the stale-future list), since a
104
+ # skill or the conventions chapter already carries that text.
105
+
106
+ parts = [core_banner, ""]
107
+
245
108
  begin
246
- require "timeout"
247
- qmd_status = Timeout.timeout(2) { QmdSync.status(plastic_home: plastic_home) }
248
- if qmd_status[:present]
249
- if qmd_status[:all_registered]
250
- parts << "QMD: #{qmd_status[:registered].size} Plastic collections indexed (search with the qmd skill)."
251
- else
252
- parts << "QMD detected — run `qmd-sync register --all` to index your Plastic stores for search."
109
+ # --- Parse INDEX.md ---
110
+
111
+ lines = File.readlines(index_path)
112
+ active = []
113
+ future = []
114
+ section = nil
115
+
116
+ lines.each do |line|
117
+ if line.start_with?("## Active")
118
+ section = :active
119
+ next
120
+ elsif line.start_with?("## Future")
121
+ section = :future
122
+ next
123
+ elsif line.start_with?("## ")
124
+ section = nil
125
+ next
253
126
  end
254
- end
255
- rescue Exception
256
- # Any failure (timeout, missing binary, parse error) — stay silent, never crash.
257
- end
258
127
 
259
- if plastic_md
260
- # Conventions always loaded first
261
- parts << plastic_md
262
- parts << "\n---\n"
263
-
264
- if current_project
265
- slug = current_project["slug"]
266
- banner = "Project: #{slug} | Store: ~/.plastic/projects/#{slug}/store/"
267
- if project_active.any? && project_active.first =~ /\[([^\]]+)\].*store\/([\w-]+)\//
268
- intent_name, dir_name = $1, $2
269
- intent_id = dir_name.split("--").first
270
- banner += "\nActive: [#{intent_id} — #{intent_name}] | Artifacts → store/#{dir_name}/"
128
+ next unless section && line.strip.start_with?("- [")
129
+
130
+ if section == :active
131
+ active << line.strip
132
+ elsif section == :future
133
+ future << line.strip
271
134
  end
272
- parts << banner + "\n"
135
+ end
136
+
137
+ # --- read-config path (used below for author and dismissed deprecations) ---
138
+
139
+ read_config = if plugin_root && !plugin_root.empty?
140
+ "#{plugin_root}/scripts/read-config"
273
141
  else
274
- parts << "PLASTIC — Global store loaded from ~/.plastic/\n"
142
+ File.expand_path("~/.plastic/scripts/read-config")
275
143
  end
276
144
 
277
- if all_active.any?
278
- parts << "Active intents:\n"
279
- all_active.each { |a| parts << a }
280
- parts << stage_line if stage_line
281
- parts << ""
145
+ # --- Detect current project ---
146
+
147
+ current_project = nil
148
+ project_active = []
149
+ project_future = []
150
+
151
+ projects_path = "#{plastic_home}/projects.yml"
152
+ if File.exist?(projects_path)
153
+ projects = YAML.safe_load(File.read(projects_path)) rescue {}
154
+ cwd = Dir.pwd
155
+ (projects["projects"] || {}).each do |slug, info|
156
+ project_path = File.expand_path(info["path"])
157
+ if cwd.start_with?(project_path)
158
+ current_project = { "slug" => slug, "parent" => info["parent"], "path" => project_path }
159
+ project_index = File.join(Plastic::StoreLayout.project_root(plastic_home, slug), "INDEX.md")
160
+ if File.exist?(project_index)
161
+ p_section = nil
162
+ File.readlines(project_index).each do |pline|
163
+ if pline.start_with?("## Active")
164
+ p_section = :active
165
+ next
166
+ elsif pline.start_with?("## Future")
167
+ p_section = :future
168
+ next
169
+ elsif pline.start_with?("## ")
170
+ p_section = nil
171
+ next
172
+ end
173
+ next unless p_section && pline.strip.start_with?("- [")
174
+ project_active << pline.strip if p_section == :active
175
+ project_future << pline.strip if p_section == :future
176
+ end
177
+ end
178
+ break
179
+ end
180
+ end
181
+ end
182
+
183
+ # --- Load PLASTIC.md conventions ---
184
+ # Its content no longer renders at boot (intent 341, G8): the gate stays,
185
+ # so a partial install without PLASTIC.md still renders no project banner,
186
+ # but the text itself is never read into `parts`.
187
+
188
+ plastic_md_path = "#{plastic_home}/PLASTIC.md"
189
+ plastic_md = File.exist?(plastic_md_path) ? File.read(plastic_md_path).strip : nil
190
+
191
+ # --- Load deprecations ---
192
+
193
+ dep_file = if plugin_root && !plugin_root.empty?
194
+ "#{plugin_root}/deprecations.yml"
282
195
  else
283
- parts << "No active intents. #{future.length} future intents available.\n"
196
+ "#{plastic_home}/deprecations.yml"
197
+ end
198
+
199
+ deprecations = []
200
+ if File.exist?(dep_file)
201
+ dep_data = YAML.safe_load(File.read(dep_file)) rescue {}
202
+ deprecations = dep_data["deprecations"] || []
203
+ end
204
+
205
+ dismissed_json = `"#{read_config}" deprecations_dismissed`.strip
206
+ dismissed = begin
207
+ JSON.parse(dismissed_json)
208
+ rescue
209
+ []
210
+ end
211
+
212
+ # A notice whose removal version is already behind the installed one has
213
+ # nothing left to warn about, and kept firing at every boot until someone
214
+ # dismissed it by hand. Only a critical notice outlives its own removal.
215
+ installed = VersionNumber.parse(current_version)
216
+
217
+ active_deprecations = deprecations.select do |dep|
218
+ next true if dep["severity"] == "critical"
219
+
220
+ removal = VersionNumber.parse(dep["removal"])
221
+ next false if installed && removal && removal < installed
222
+ next true if current_version && dep["removal"] == current_version
223
+
224
+ !dismissed.include?(dep["id"])
284
225
  end
285
226
 
286
- if stale.any?
287
- parts << "\nStale future intents (untouched for days):\n"
288
- stale.each do |s|
289
- parts << "- #{s[:name]} (#{s[:age]} days) — consider: activate, abandon, or defer to agent"
227
+ # --- Check for available updates (from previous session's check) ---
228
+
229
+ update_notice = nil
230
+ cache_file = "#{plastic_home}/.cache/update-check.json"
231
+ if File.exist?(cache_file)
232
+ cache = JSON.parse(File.read(cache_file)) rescue {}
233
+ if cache["updateAvailable"]
234
+ update_notice = "Plastic update available: #{cache["current"]} -> #{cache["latest"]} — run `plastic update`"
290
235
  end
291
- parts << "\nWhen appropriate, ask the user what to do with stale intents."
292
236
  end
293
- end
294
237
 
295
- if active_deprecations.any?
296
- parts << ""
297
- active_deprecations.each do |dep|
298
- severity = dep["severity"] || "info"
299
- summary = dep["summary"] || dep["id"]
300
- removal = dep["removal"]
301
- link = dep["link"]
302
- steps = dep["migration_steps"] || []
303
-
304
- if severity == "info"
305
- line = "i Deprecation: #{summary}. Removed in: #{removal}."
306
- line += " See: #{link}" if link
307
- parts << line
238
+ # --- QMD search status (intent 45a, READ-ONLY) ---
239
+ # Report-only line for the model (additionalContext), never systemMessage and
240
+ # never the exit code. QmdSync.status shells out to `qmd collection list`, so the
241
+ # whole block is guarded: a 2s timeout caps any hang, and rescue-all guarantees a
242
+ # slow/broken/missing qmd appends nothing and the hook continues cleanly.
243
+ begin
244
+ require "timeout"
245
+ qmd_status = Timeout.timeout(2) { QmdSync.status(plastic_home: plastic_home) }
246
+ if qmd_status[:present]
247
+ # C23 (intent 341, G8): a QMD hit is untrusted text, so it never sits
248
+ # loose in additionalContext alongside the owner's own banners; it
249
+ # rides inside DataBoundary's data boundary.
250
+ qmd_line = if qmd_status[:all_registered]
251
+ "QMD: #{qmd_status[:registered].size} Plastic collections indexed (search with the qmd skill)."
252
+ else
253
+ "QMD detected — run `qmd-sync register --all` to index your Plastic stores for search."
254
+ end
255
+ token = DataBoundary.boundary_token([qmd_line])
256
+ parts << DataBoundary.wrap(qmd_line, label: "qmd-hit", source: "qmd-sync", token: token).rstrip
257
+ end
258
+ rescue Exception
259
+ # Any failure (timeout, missing binary, parse error) — stay silent, never crash.
260
+ end unless subagent_session
261
+
262
+ # --- Delivery watch (intent 340a, G7b, n3, graph.md D10) ---
263
+ # One unrecorded tick (record: false: reclaim and classify, no snapshot,
264
+ # no record line, so boot never advances a quiet count) per intent
265
+ # ActiveDelivery.candidate_intent_dirs finds, naming every stalled or
266
+ # done_unreported one in a single line, nothing when there are none. The
267
+ # whole block is one guarded unit, like the QMD block above it: a raise or
268
+ # a hang on any one candidate must add nothing rather than name only the
269
+ # candidates seen before the failure, so the line a boot does print is
270
+ # never a partial one.
271
+ begin
272
+ Timeout.timeout(2) do
273
+ roots_raw = IO.popen({ "PLASTIC_HOME" => plastic_home }, [read_config, "project_roots"],
274
+ err: File::NULL, &:read).to_s.strip
275
+ project_roots = roots_raw.empty? ? [] : Array(JSON.parse(roots_raw))
276
+ project_roots = project_roots.map { |root| File.expand_path(root.to_s) }
277
+
278
+ attention = []
279
+ ActiveDelivery.candidate_intent_dirs(global_store: store_dir, project_roots: project_roots).uniq.each do |dir|
280
+ watch_context = RunnerCore.context(intent_dir: dir)
281
+ result = RunnerWatch.tick(watch_context, record: false)
282
+ next unless %w[stalled done_unreported].include?(result[:class])
283
+
284
+ watch_intent_id = File.basename(dir).split("--").first
285
+ if result[:class] == "stalled"
286
+ blocker = Array(result[:blockers]).first
287
+ attention << (blocker ? "#{watch_intent_id} stalled (#{blocker})" : "#{watch_intent_id} stalled")
288
+ else
289
+ attention << "#{watch_intent_id} done_unreported"
290
+ end
291
+ end
292
+ parts << "PLASTIC watch: #{attention.join(', ')}" if attention.any?
293
+ end
294
+ rescue Exception
295
+ # Any failure or timeout (a malformed intent directory, a hung tick) -
296
+ # stay silent, never crash boot.
297
+ end unless subagent_session
298
+
299
+ # --- Project (or global) banner with its active intent ---
300
+ # The one piece of intent context a live boot still carries: which store
301
+ # is live, and which intent (if any) is active in it (intent 341, G8, D4).
302
+ if plastic_md && !subagent_session
303
+ if current_project
304
+ slug = current_project["slug"]
305
+ banner = "Project: #{slug} | Store: #{File.join(Plastic::StoreLayout.project_root(plastic_home, slug), "store").sub(Dir.home, "~")}/"
306
+ if project_active.any? && project_active.first =~ /\[([^\]]+)\].*store\/([\w-]+)\//
307
+ intent_name, dir_name = $1, $2
308
+ intent_id = dir_name.split("--").first
309
+ banner += "\nActive: [#{intent_id} — #{intent_name}] | Artifacts → store/#{dir_name}/"
310
+ end
311
+ parts << banner
308
312
  else
309
- marker = severity == "critical" ? "!! DEPRECATION (critical)" : "! DEPRECATION (warning)"
310
- parts << "#{marker}: #{summary}"
311
- if steps.any?
312
- parts << " Migration steps:"
313
- steps.each_with_index { |s, i| parts << " #{i + 1}. #{s}" }
313
+ banner = "PLASTIC — Global store loaded from ~/.plastic/"
314
+ if active.any? && active.first =~ /\[([^\]]+)\].*store\/([\w-]+)\//
315
+ intent_name, dir_name = $1, $2
316
+ intent_id = dir_name.split("--").first
317
+ banner += "\nActive: [#{intent_id} — #{intent_name}] | Artifacts → store/#{dir_name}/"
314
318
  end
315
- trail = " Removed in: #{removal}"
316
- trail += " | Details: #{link}" if link
317
- parts << trail
319
+ parts << banner
318
320
  end
319
321
  end
320
- end
321
322
 
322
- if update_notice
323
- parts.unshift("! #{update_notice}\n")
324
- end
323
+ if active_deprecations.any? && !subagent_session
324
+ parts << ""
325
+ active_deprecations.each do |dep|
326
+ severity = dep["severity"] || "info"
327
+ summary = dep["summary"] || dep["id"]
328
+ removal = dep["removal"]
329
+ link = dep["link"]
330
+ steps = dep["migration_steps"] || []
331
+
332
+ if severity == "info"
333
+ line = "i Deprecation: #{summary}. Removed in: #{removal}."
334
+ line += " See: #{link}" if link
335
+ parts << line
336
+ else
337
+ marker = severity == "critical" ? "!! DEPRECATION (critical)" : "! DEPRECATION (warning)"
338
+ parts << "#{marker}: #{summary}"
339
+ if steps.any?
340
+ parts << " Migration steps:"
341
+ steps.each_with_index { |s, i| parts << " #{i + 1}. #{s}" }
342
+ end
343
+ trail = " Removed in: #{removal}"
344
+ trail += " | Details: #{link}" if link
345
+ parts << trail
346
+ end
347
+ end
348
+ end
325
349
 
326
- # --- First-boot sweep (intent 301, spec D9): file every unclosed prior day
327
- # ledger into today, oldest first, at most 3 per boot within a 5-second
328
- # budget. Each day runs the sibling file-session-intent in its own rescue;
329
- # the boot never blocks on it.
330
- begin
331
- require "open3"
332
- require "timeout"
333
- require_relative "lib/session_backfill"
334
-
335
- sweep_today = SessionLedger.day_id
336
- sweep_root = SessionLedger.sessions_root(store_dir)
337
- sweep_filer = File.expand_path("file-session-intent", __dir__)
338
- sweep_templates = File.expand_path("../templates", __dir__)
339
- candidates = []
340
- if Dir.exist?(sweep_root) && File.exist?(sweep_filer)
341
- candidates = Dir.children(sweep_root).select do |name|
342
- File.directory?(File.join(sweep_root, name)) && SessionLedger.valid_day_id?(name) &&
343
- name < sweep_today && !SessionBackfill.closed?(store_dir, name)
344
- end.sort
350
+ if update_notice && !subagent_session
351
+ parts.unshift("! #{update_notice}\n")
345
352
  end
346
- filed = 0
347
- budget_end = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
348
- candidates.first(3).each do |day|
349
- break if Process.clock_gettime(Process::CLOCK_MONOTONIC) > budget_end
350
353
 
354
+ # --- First-boot sweep (intent 301, spec D9): file every unclosed prior day
355
+ # ledger into today, oldest first, at most 3 per boot within a 5-second
356
+ # budget. Each day runs the sibling file-session-intent in its own rescue;
357
+ # the boot never blocks on it.
358
+ unless subagent_session
351
359
  begin
352
- Timeout.timeout(5) do
353
- _out, _err, sweep_status = Open3.capture3({ "RUBYOPT" => nil }, RbConfig.ruby, sweep_filer,
354
- "--day", day, "--carry-to", sweep_today,
355
- "--store", store_dir, "--templates", sweep_templates)
356
- filed += 1 if sweep_status.success? && SessionBackfill.closed?(store_dir, day)
360
+ require "open3"
361
+ require "timeout"
362
+ require_relative "lib/session_backfill"
363
+
364
+ sweep_today = SessionLedger.day_id
365
+ sweep_root = SessionLedger.sessions_root(store_dir)
366
+ sweep_filer = File.expand_path("file-session-intent", __dir__)
367
+ sweep_templates = File.expand_path("../templates", __dir__)
368
+ candidates = []
369
+ if Dir.exist?(sweep_root) && File.exist?(sweep_filer)
370
+ candidates = Dir.children(sweep_root).select do |name|
371
+ File.directory?(File.join(sweep_root, name)) && SessionLedger.valid_day_id?(name) &&
372
+ name < sweep_today && !SessionBackfill.closed?(store_dir, name)
373
+ end.sort
374
+ end
375
+ filed = 0
376
+ budget_end = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
377
+ candidates.first(3).each do |day|
378
+ break if Process.clock_gettime(Process::CLOCK_MONOTONIC) > budget_end
379
+
380
+ begin
381
+ Timeout.timeout(5) do
382
+ _out, _err, sweep_status = Open3.capture3({ "RUBYOPT" => nil }, RbConfig.ruby, sweep_filer,
383
+ "--day", day, "--carry-to", sweep_today,
384
+ "--store", store_dir, "--templates", sweep_templates)
385
+ filed += 1 if sweep_status.success? && SessionBackfill.closed?(store_dir, day)
386
+ end
387
+ rescue StandardError
388
+ nil
389
+ end
390
+ end
391
+ if filed.positive?
392
+ line = "PLASTIC: filed #{filed} prior day ledger(s) into #{sweep_today}"
393
+ remaining = candidates.size - filed
394
+ line += ", #{remaining} more wait for the next boot" if remaining.positive?
395
+ parts << line
357
396
  end
358
397
  rescue StandardError
359
398
  nil
360
399
  end
361
400
  end
362
- if filed.positive?
363
- line = "PLASTIC: filed #{filed} prior day ledger(s) into #{sweep_today}"
364
- remaining = candidates.size - filed
365
- line += ", #{remaining} more wait for the next boot" if remaining.positive?
366
- parts << line
367
- end
368
- rescue StandardError
369
- nil
370
- end
371
401
 
372
- # --- Session day ledger: open or join today, write the per-session pointer
373
- # and heartbeat (intent 298, spec D4). Best-effort: any failure here degrades
374
- # to no ledger context line, never blocks the session.
375
- begin
376
- templates = File.expand_path("../templates", __dir__)
377
- day = SessionLedger.day_id
378
- author = `"#{read_config}" author`.strip
379
- author = "session" if author.empty?
380
- SessionLedger.open_day(store: store_dir, day: day, templates: templates, author: author)
381
-
382
- session = if !payload_session_id.empty?
383
- payload_session_id
384
- else
385
- ENV["CLAUDE_CODE_SESSION_ID"] || Process.pid.to_s
386
- end
387
- sid = SessionLedger.short_session_id(nil, session)
388
- SessionLedger.ensure_tmp_root(store_dir)
389
- FileUtils.mkdir_p(SessionLedger.session_tmp_dir(store_dir, sid))
390
- File.write(SessionLedger.heartbeat_path(store_dir, sid), "#{Time.now.utc.iso8601}\n")
391
- pointer = SessionLedger.pointer_path(store_dir, sid)
392
- File.write(pointer, "#{day}\n") unless File.exist?(pointer)
393
-
394
- checklist = SessionLedger.checklist_path(store_dir, day)
395
- open_count = 0
396
- pending_count = 0
397
- if File.exist?(checklist)
398
- File.readlines(checklist).each do |line|
399
- parsed = SessionLedger.parse_checklist_line(line)
400
- next unless parsed
401
-
402
- open_count += 1 if parsed[:state] == :open
403
- pending_count += 1 if parsed[:state] == :pending
402
+ # --- Session day ledger: open or join today, create the tmp dir and write
403
+ # the heartbeat (344 n2, spec D4). The per-session state retired: hooks
404
+ # now ask ActiveDelivery.resolve for a live delivery lock instead.
405
+ # Best-effort: any failure here degrades to no ledger context line, never
406
+ # blocks the session.
407
+ unless subagent_session
408
+ begin
409
+ templates = File.expand_path("../templates", __dir__)
410
+ day = SessionLedger.day_id
411
+ author = `"#{read_config}" author`.strip
412
+ author = "session" if author.empty?
413
+ SessionLedger.open_day(store: store_dir, day: day, templates: templates, author: author)
414
+
415
+ session = if !payload_session_id.empty?
416
+ payload_session_id
417
+ else
418
+ ENV["CLAUDE_CODE_SESSION_ID"] || Process.pid.to_s
419
+ end
420
+ sid = SessionLedger.short_session_id(nil, session)
421
+ SessionLedger.ensure_tmp_root(store_dir)
422
+ FileUtils.mkdir_p(SessionLedger.session_tmp_dir(store_dir, sid))
423
+ File.write(SessionLedger.heartbeat_path(store_dir, sid), "#{Time.now.utc.iso8601}\n")
424
+
425
+ checklist = SessionLedger.checklist_path(store_dir, day)
426
+ open_count = 0
427
+ pending_count = 0
428
+ if File.exist?(checklist)
429
+ File.readlines(checklist).each do |line|
430
+ parsed = SessionLedger.parse_checklist_line(line)
431
+ next unless parsed
432
+
433
+ open_count += 1 if parsed[:state] == :open
434
+ pending_count += 1 if parsed[:state] == :pending
435
+ end
436
+ end
437
+ parts << "PLASTIC: day ledger #{day} joined (#{open_count} open items, #{pending_count} pending)"
438
+
439
+ # The day summary (intent 311, spec D8): open items, the last five done,
440
+ # live auto intents, other active sessions. Never the raw ledger. A
441
+ # failure here leaves the joined line alone.
442
+ begin
443
+ summary = DaySummary.build(store: store_dir, day: day, session: sid, home: plastic_home, now: Time.now)
444
+ parts << summary unless summary.empty?
445
+ rescue StandardError
446
+ nil
447
+ end
448
+ rescue StandardError
449
+ nil
404
450
  end
405
451
  end
406
- parts << "PLASTIC: day ledger #{day} joined (#{open_count} open items, #{pending_count} pending)"
407
-
408
- # The day summary (intent 311, spec D8): open items, the last five done,
409
- # live auto intents, other active sessions. Never the raw ledger. A
410
- # failure here leaves the joined line alone.
411
- begin
412
- summary = DaySummary.build(store: store_dir, day: day, session: sid, home: plastic_home, now: Time.now)
413
- parts << summary unless summary.empty?
414
- rescue StandardError
415
- nil
416
- end
417
452
  rescue StandardError
418
- nil
453
+ # Anything unhandled above (a malformed INDEX.md, a broken projects.yml, or
454
+ # any other surprise) degrades the whole boot to the core banner alone,
455
+ # never a naked crash with no JSON at all (intent 341, G8, row 2.4).
456
+ parts = [core_banner, ""]
419
457
  end
420
458
 
421
- # Emit nothing when there is genuinely nothing to surface (no conventions,
459
+ # Emit nothing when there is genuinely nothing to surface (no active intent,
422
460
  # no deprecations, no update notice).
423
461
  exit 0 if parts.join.strip.empty?
424
462