@zalom/plastic 2.0.0-alpha.8 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/PLASTIC.md +13 -136
  2. package/README.md +357 -133
  3. package/agents/plastic-enforcer.md +20 -15
  4. package/agents/plastic-executor.md +15 -4
  5. package/agents/plastic-node-research.md +30 -0
  6. package/agents/plastic-node-verify.md +28 -0
  7. package/agents/plastic-node-work.md +33 -0
  8. package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +7 -9
  9. package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
  10. package/assets/plastic-logo.svg +1 -0
  11. package/bin/crap +4 -0
  12. package/bin/lib/context_budget.rb +43 -6
  13. package/bin/lib/skill_census.rb +839 -0
  14. package/bin/plastic +6 -0
  15. package/bin/plastic-skill-census +114 -0
  16. package/bin/test +24 -4
  17. package/bin/verify-change +345 -0
  18. package/config_asks.yml +4 -4
  19. package/deprecations.yml +1 -1
  20. package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
  21. package/{skills/auto/references → docs/help}/agent-architecture.md +16 -14
  22. package/{skills/conventions/references → docs/help}/completion-and-done.md +15 -16
  23. package/docs/help/human-report-contract.md +152 -0
  24. package/{skills/conventions/references → docs/help}/knowledge-graph.md +9 -0
  25. package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +23 -13
  26. package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +1 -1
  27. package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
  28. package/{skills/tutorial/references → docs/help}/track-1-guided.md +28 -47
  29. package/{skills/tutorial/references → docs/help}/track-2-auto.md +8 -8
  30. package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
  31. package/hooks/call-budget +4 -0
  32. package/hooks/hooks.json +24 -0
  33. package/hooks/message-display +62 -1
  34. package/hooks/session-start +5 -1
  35. package/hooks/statusline +32 -27
  36. package/hooks/stop +5 -0
  37. package/package.json +5 -3
  38. package/scripts/append-ledger +2 -1
  39. package/scripts/dashboard.rb +267 -16
  40. package/scripts/day-summary +2 -1
  41. package/scripts/doctor.rb +680 -39
  42. package/scripts/end-intent +238 -22
  43. package/scripts/exec-worktree +5 -5
  44. package/scripts/file-session-intent +2 -1
  45. package/scripts/graph-measure +249 -0
  46. package/scripts/hook-call-budget +222 -0
  47. package/scripts/hook-capture +24 -123
  48. package/scripts/hook-close +2 -1
  49. package/scripts/hook-message-display +19 -5
  50. package/scripts/hook-record +24 -16
  51. package/scripts/hook-savepoint +27 -3
  52. package/scripts/hook-session-start +359 -321
  53. package/scripts/hook-stop +58 -0
  54. package/scripts/index-projection +74 -0
  55. package/scripts/insight-append +17 -4
  56. package/scripts/install.rb +9 -7
  57. package/scripts/lib/action_graph_shim.rb +279 -0
  58. package/scripts/lib/active_delivery.rb +105 -0
  59. package/scripts/lib/agent_models.rb +63 -25
  60. package/scripts/lib/arm.rb +48 -83
  61. package/scripts/lib/atomic_write.rb +31 -0
  62. package/scripts/lib/backup.rb +65 -0
  63. package/scripts/lib/cli/command.rb +92 -0
  64. package/scripts/lib/cli/commands/auto.rb +18 -0
  65. package/scripts/lib/cli/commands/auto_brief.rb +44 -0
  66. package/scripts/lib/cli/commands/auto_lock.rb +99 -0
  67. package/scripts/lib/cli/commands/auto_report.rb +50 -0
  68. package/scripts/lib/cli/commands/auto_take.rb +31 -0
  69. package/scripts/lib/cli/commands/backup.rb +43 -0
  70. package/scripts/lib/cli/commands/checkout.rb +25 -0
  71. package/scripts/lib/cli/commands/continue.rb +66 -0
  72. package/scripts/lib/cli/commands/doctor.rb +81 -0
  73. package/scripts/lib/cli/commands/feedback.rb +40 -0
  74. package/scripts/lib/cli/commands/help.rb +69 -0
  75. package/scripts/lib/cli/commands/hook.rb +32 -0
  76. package/scripts/lib/cli/commands/index.rb +23 -0
  77. package/scripts/lib/cli/commands/install.rb +21 -0
  78. package/scripts/lib/cli/commands/installer_verb.rb +37 -0
  79. package/scripts/lib/cli/commands/intent.rb +19 -0
  80. package/scripts/lib/cli/commands/intent_answer.rb +37 -0
  81. package/scripts/lib/cli/commands/intent_command.rb +53 -0
  82. package/scripts/lib/cli/commands/intent_end.rb +62 -0
  83. package/scripts/lib/cli/commands/intent_new.rb +62 -0
  84. package/scripts/lib/cli/commands/intent_note.rb +43 -0
  85. package/scripts/lib/cli/commands/intent_rule.rb +36 -0
  86. package/scripts/lib/cli/commands/intent_show.rb +32 -0
  87. package/scripts/lib/cli/commands/intent_spec.rb +44 -0
  88. package/scripts/lib/cli/commands/intent_step.rb +71 -0
  89. package/scripts/lib/cli/commands/intent_verify.rb +26 -0
  90. package/scripts/lib/cli/commands/migrate.rb +16 -0
  91. package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
  92. package/scripts/lib/cli/commands/next.rb +54 -0
  93. package/scripts/lib/cli/commands/project.rb +19 -0
  94. package/scripts/lib/cli/commands/project_links.rb +42 -0
  95. package/scripts/lib/cli/commands/project_list.rb +20 -0
  96. package/scripts/lib/cli/commands/project_new.rb +72 -0
  97. package/scripts/lib/cli/commands/query.rb +31 -0
  98. package/scripts/lib/cli/commands/render.rb +33 -0
  99. package/scripts/lib/cli/commands/roadmap.rb +19 -0
  100. package/scripts/lib/cli/commands/roadmap_check.rb +54 -0
  101. package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
  102. package/scripts/lib/cli/commands/roadmap_migrate.rb +53 -0
  103. package/scripts/lib/cli/commands/roadmap_next.rb +70 -0
  104. package/scripts/lib/cli/commands/roadmap_show.rb +45 -0
  105. package/scripts/lib/cli/commands/rollback.rb +20 -0
  106. package/scripts/lib/cli/commands/search.rb +60 -0
  107. package/scripts/lib/cli/commands/session.rb +18 -0
  108. package/scripts/lib/cli/commands/session_commit.rb +42 -0
  109. package/scripts/lib/cli/commands/session_handoff.rb +34 -0
  110. package/scripts/lib/cli/commands/session_summary.rb +35 -0
  111. package/scripts/lib/cli/commands/status.rb +68 -0
  112. package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
  113. package/scripts/lib/cli/commands/sync.rb +46 -0
  114. package/scripts/lib/cli/commands/uninstall.rb +20 -0
  115. package/scripts/lib/cli/commands/update.rb +20 -0
  116. package/scripts/lib/cli/commands/version.rb +53 -0
  117. package/scripts/lib/cli/frontier.rb +86 -0
  118. package/scripts/lib/cli/intent_progress.rb +36 -0
  119. package/scripts/lib/cli/legacy.rb +80 -0
  120. package/scripts/lib/cli/output.rb +137 -0
  121. package/scripts/lib/cli/scope.rb +134 -0
  122. package/scripts/lib/cli/table.rb +65 -0
  123. package/scripts/lib/cli.rb +94 -0
  124. package/scripts/lib/codex_adapter.rb +198 -0
  125. package/scripts/lib/compact_instructions.rb +13 -5
  126. package/scripts/lib/core_integrity.rb +71 -0
  127. package/scripts/lib/dashboard_screen.rb +40 -0
  128. package/scripts/lib/data_boundary.rb +132 -0
  129. package/scripts/lib/day_summary.rb +30 -16
  130. package/scripts/lib/doctor_core.rb +114 -38
  131. package/scripts/lib/doctor_session_ledger.rb +5 -53
  132. package/scripts/lib/engine_permissions.rb +88 -0
  133. package/scripts/lib/exec_worktree.rb +24 -21
  134. package/scripts/lib/feedback_report.rb +1 -1
  135. package/scripts/lib/graph_edges.rb +137 -0
  136. package/scripts/lib/graph_file.rb +246 -0
  137. package/scripts/lib/graph_measure.rb +645 -0
  138. package/scripts/lib/graph_measure_budget.rb +409 -0
  139. package/scripts/lib/graph_measure_cohorts.rb +487 -0
  140. package/scripts/lib/graph_measure_models.rb +413 -0
  141. package/scripts/lib/graph_measure_report.rb +532 -0
  142. package/scripts/lib/graph_tree.rb +98 -0
  143. package/scripts/lib/guarded_append.rb +155 -0
  144. package/scripts/lib/handoff.rb +40 -13
  145. package/scripts/lib/harness_adapter.rb +184 -0
  146. package/scripts/lib/hook_registry.rb +27 -3
  147. package/scripts/lib/hook_replay.rb +229 -0
  148. package/scripts/lib/index_entry.rb +62 -0
  149. package/scripts/lib/index_projection.rb +201 -0
  150. package/scripts/lib/insights.rb +1 -1
  151. package/scripts/lib/installer_core.rb +532 -71
  152. package/scripts/lib/intent_screen.rb +9 -6
  153. package/scripts/lib/intent_screen_ansi.rb +73 -12
  154. package/scripts/lib/intent_validator.rb +2 -2
  155. package/scripts/lib/lock.rb +10 -11
  156. package/scripts/lib/message_display.rb +377 -173
  157. package/scripts/lib/meter_watch.rb +185 -0
  158. package/scripts/lib/node_file.rb +234 -0
  159. package/scripts/lib/node_ids.rb +99 -0
  160. package/scripts/lib/node_input.rb +913 -0
  161. package/scripts/lib/node_input_compatibility.rb +62 -0
  162. package/scripts/lib/node_ledger.rb +386 -0
  163. package/scripts/lib/node_progress.rb +153 -0
  164. package/scripts/lib/node_return.rb +204 -0
  165. package/scripts/lib/node_worktree.rb +337 -0
  166. package/scripts/lib/outcome_report.rb +440 -0
  167. package/scripts/lib/preflight.rb +4 -6
  168. package/scripts/lib/project_config.rb +46 -0
  169. package/scripts/lib/project_validator.rb +3 -2
  170. package/scripts/lib/qmd_sync.rb +8 -7
  171. package/scripts/lib/ready_set.rb +462 -0
  172. package/scripts/lib/reference_archive.rb +45 -0
  173. package/scripts/lib/release_guard.rb +18 -0
  174. package/scripts/lib/report_screen.rb +1460 -74
  175. package/scripts/lib/rlm/corpus.rb +13 -0
  176. package/scripts/lib/rlm/probe.rb +29 -0
  177. package/scripts/lib/rlm/query.rb +22 -0
  178. package/scripts/lib/roadmap_graph.rb +210 -0
  179. package/scripts/lib/roadmap_migration.rb +95 -0
  180. package/scripts/lib/roadmap_queue.rb +158 -8
  181. package/scripts/lib/roadmap_render.rb +150 -0
  182. package/scripts/lib/roadmap_savepoint.rb +64 -14
  183. package/scripts/lib/runner_absorb.rb +703 -0
  184. package/scripts/lib/runner_answer.rb +206 -0
  185. package/scripts/lib/runner_core.rb +194 -0
  186. package/scripts/lib/runner_dispatch.rb +525 -0
  187. package/scripts/lib/runner_policy.rb +191 -0
  188. package/scripts/lib/runner_proposals.rb +275 -0
  189. package/scripts/lib/runner_rewind.rb +201 -0
  190. package/scripts/lib/runner_sweep.rb +231 -0
  191. package/scripts/lib/runner_until_empty.rb +252 -0
  192. package/scripts/lib/runner_watch.rb +389 -0
  193. package/scripts/lib/savepoint.rb +141 -19
  194. package/scripts/lib/scaffold_intent.rb +6 -3
  195. package/scripts/lib/screen_paint.rb +555 -0
  196. package/scripts/lib/screens/dashboard.rb +20 -0
  197. package/scripts/lib/screens/plan.rb +18 -0
  198. package/scripts/lib/screens/roadmap.rb +15 -0
  199. package/scripts/lib/search_index.rb +55 -0
  200. package/scripts/lib/session_close.rb +30 -28
  201. package/scripts/lib/session_git.rb +25 -18
  202. package/scripts/lib/session_ledger.rb +48 -4
  203. package/scripts/lib/session_usage.rb +190 -0
  204. package/scripts/lib/sqlite.rb +22 -0
  205. package/scripts/lib/stop_gate.rb +95 -0
  206. package/scripts/lib/store_discovery.rb +7 -6
  207. package/scripts/lib/store_layout.rb +54 -0
  208. package/scripts/lib/store_provisioning.rb +2 -1
  209. package/scripts/lib/store_sync.rb +85 -0
  210. package/scripts/lib/stores_move.rb +93 -0
  211. package/scripts/lib/verify_intent.rb +36 -8
  212. package/scripts/lib/version_number.rb +48 -0
  213. package/scripts/lib/work_graph.rb +59 -0
  214. package/scripts/lib/work_graph_validator.rb +201 -0
  215. package/scripts/lib/worktree.rb +27 -32
  216. package/scripts/lib/worktree_sweep.rb +6 -5
  217. package/scripts/link-suggest +2 -1
  218. package/scripts/meter-watch +57 -0
  219. package/scripts/migrate-to-global +1 -1
  220. package/scripts/new-intent +4 -13
  221. package/scripts/node-input +92 -0
  222. package/scripts/node-run +225 -0
  223. package/scripts/node-transition +291 -0
  224. package/scripts/outcome-report +74 -0
  225. package/scripts/plastic-lock +42 -33
  226. package/scripts/promote-session-item +3 -2
  227. package/scripts/read-config +53 -9
  228. package/scripts/ready-set +126 -0
  229. package/scripts/release-check +123 -0
  230. package/scripts/report-screen +192 -30
  231. package/scripts/roadmap-graph +119 -0
  232. package/scripts/roadmap-savepoint +7 -0
  233. package/scripts/runner +581 -0
  234. package/scripts/savepoint-note +11 -9
  235. package/scripts/session-commit +2 -1
  236. package/scripts/session-usage +56 -0
  237. package/scripts/skill-lint +115 -6
  238. package/scripts/spawn-preamble +2 -2
  239. package/scripts/update.rb +25 -4
  240. package/scripts/validate-work-graph +39 -0
  241. package/scripts/verify-intent +3 -2
  242. package/scripts/write-handoff +2 -1
  243. package/templates/agents.md +7 -7
  244. package/templates/config.yml +16 -9
  245. package/templates/dashboard-screen.md +22 -0
  246. package/templates/display-fixture.md +21 -0
  247. package/templates/graph.md +16 -0
  248. package/templates/index.md +1 -1
  249. package/templates/intent-screen.md +1 -1
  250. package/templates/node-decision.md +11 -0
  251. package/templates/node-research.md +13 -0
  252. package/templates/node-verify.md +13 -0
  253. package/templates/node-work.md +22 -0
  254. package/templates/outcome.md +15 -2
  255. package/templates/project.yml +1 -1
  256. package/templates/render.css +10 -0
  257. package/templates/report-plan.md +15 -0
  258. package/templates/report-roadmap-delivered.md +10 -0
  259. package/templates/report-roadmap-plan.md +9 -0
  260. package/templates/report-roadmap-state.md +9 -0
  261. package/templates/report-state.md +1 -1
  262. package/templates/roadmap.md +13 -0
  263. package/bin/plastic.js +0 -70
  264. package/scripts/lib/bridge.rb +0 -116
  265. package/skills/agent-advisor/SKILL.md +0 -92
  266. package/skills/auto/SKILL.md +0 -295
  267. package/skills/auto/evals/evals.json +0 -255
  268. package/skills/auto/references/end-tail.md +0 -66
  269. package/skills/auto/references/human-report-contract.md +0 -78
  270. package/skills/conventions/SKILL.md +0 -29
  271. package/skills/dashboard/SKILL.md +0 -169
  272. package/skills/dashboard/evals/evals.json +0 -38
  273. package/skills/dashboard/references/classification.md +0 -22
  274. package/skills/dashboard/templates/dashboard-global.md +0 -20
  275. package/skills/dashboard/templates/dashboard-project.md +0 -19
  276. package/skills/direct/SKILL.md +0 -66
  277. package/skills/direct/references/request-signals.md +0 -59
  278. package/skills/doctor/SKILL.md +0 -299
  279. package/skills/doctor/report.md +0 -102
  280. package/skills/feedback/SKILL.md +0 -98
  281. package/skills/feedback/references/transport-and-privacy.md +0 -65
  282. package/skills/feedback/report.md +0 -36
  283. package/skills/install/SKILL.md +0 -217
  284. package/skills/intent-continuing/SKILL.md +0 -154
  285. package/skills/intent-continuing/references/board-fill.md +0 -43
  286. package/skills/intent-continuing/references/boarding-matrix.md +0 -34
  287. package/skills/intent-continuing/references/context-management.md +0 -28
  288. package/skills/intent-continuing/references/liveness-ranking.md +0 -57
  289. package/skills/intent-creating/SKILL.md +0 -164
  290. package/skills/intent-creating/evals/evals.json +0 -72
  291. package/skills/intent-creating/references/lifecycle.md +0 -81
  292. package/skills/intent-creating/references/wikilinks.md +0 -8
  293. package/skills/intent-ending/SKILL.md +0 -173
  294. package/skills/intent-ending/evals/evals.json +0 -74
  295. package/skills/intent-executing/SKILL.md +0 -170
  296. package/skills/intent-executing/evals/evals.json +0 -66
  297. package/skills/intent-executing/implementer-prompt.md +0 -42
  298. package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
  299. package/skills/intent-speccing/SKILL.md +0 -130
  300. package/skills/intent-speccing/evals/evals.json +0 -126
  301. package/skills/intent-speccing/references/design-principles.md +0 -44
  302. package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
  303. package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
  304. package/skills/project-creating/SKILL.md +0 -162
  305. package/skills/project-creating/references/hubs-projects.md +0 -55
  306. package/skills/project-creating/references/project-scaffolding.md +0 -97
  307. package/skills/releasing/SKILL.md +0 -337
  308. package/skills/releasing/references/deprecations.md +0 -60
  309. package/skills/releasing/references/promotion-and-tagging.md +0 -66
  310. package/skills/releasing/references/release-lines.md +0 -105
  311. package/skills/roadmap/SKILL.md +0 -64
  312. package/skills/roadmap/references/file-format.md +0 -124
  313. package/skills/roadmap/references/operations.md +0 -112
  314. package/skills/rollback/SKILL.md +0 -91
  315. package/skills/tutorial/SKILL.md +0 -65
  316. package/skills/tutorial/evals/evals.json +0 -186
  317. package/skills/uninstall/SKILL.md +0 -75
  318. package/skills/update/SKILL.md +0 -126
  319. /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
  320. /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
  321. /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
  322. /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ require "json"
6
+ require "yaml"
7
+ require "date"
8
+ require_relative "lib/savepoint"
9
+
10
+ # graph-measure - the one read-only executable over the node ledger (intent
11
+ # 343, G10, n2). A subcommand table over one library, following
12
+ # scripts/runner's shape (327 spec): the public verbs are named up front even
13
+ # before every module lands, a flag outside a verb's own vocabulary is
14
+ # refused by name, and each verb's module is required LAZILY inside its own
15
+ # branch so a verb whose module has not landed yet reports plainly instead of
16
+ # taking `intent`, the verb an operator runs constantly, down with it.
17
+ #
18
+ # Usage:
19
+ # graph-measure <intent|budget|cohorts> <dir> [--format text|json]
20
+ #
21
+ # `intent` (n2) and `budget` (n4) take an INTENT directory. `cohorts` (n5,
22
+ # its model section; n6 adds the rates, the latency and the bar to the same
23
+ # verb) takes a STORE directory instead - the directory holding `id--slug/`
24
+ # intent folders for one project - and is validated against that shape, not
25
+ # `intent_directory?`.
26
+ #
27
+ # Exit codes:
28
+ # 0 - a report rendered (anomalies never change this: a ledger with torn
29
+ # lines still renders, lists them, and exits 0)
30
+ # 1 - the directory has no savepoint.md to measure at all, named
31
+ # 2 - usage: no args, an unknown subcommand, an unknown flag, or the path
32
+ # is not the right shape for its verb (an intent directory for
33
+ # `intent`/`budget`, a store directory for `cohorts`)
34
+ # 3 - the verb's module has not been delivered yet
35
+ module GraphMeasureCLI
36
+ module_function
37
+
38
+ VERBS = %w[intent budget cohorts].freeze
39
+ SAVEPOINT_FILE = "savepoint.md"
40
+
41
+ # verb -> [[ModuleName, lib_file], ...], required lazily inside the verb's
42
+ # own branch (row 2.6). "budget" and "cohorts" name libraries n4 and n5
43
+ # ship; requiring them today raises LoadError, caught below.
44
+ LAZY_MODULES = {
45
+ "intent" => [["GraphMeasure", "graph_measure"], ["GraphMeasureReport", "graph_measure_report"]],
46
+ "budget" => [["GraphMeasureBudget", "graph_measure_budget"]],
47
+ "cohorts" => [["GraphMeasureModels", "graph_measure_models"], ["GraphMeasureCohorts", "graph_measure_cohorts"]],
48
+ }.freeze
49
+
50
+ KNOWN_FLAGS = {
51
+ "intent" => %w[--format],
52
+ "budget" => %w[--format],
53
+ "cohorts" => %w[--format],
54
+ }.freeze
55
+
56
+ # "intent" and "budget" take one intent directory; "cohorts" takes the
57
+ # store directory that holds many of them (row 5.15).
58
+ DIR_KIND = { "intent" => :intent, "budget" => :intent, "cohorts" => :store }.freeze
59
+
60
+ def usage
61
+ warn "Usage: graph-measure <#{VERBS.join('|')}> <dir> [--format text|json]"
62
+ end
63
+
64
+ def intent_directory?(dir)
65
+ dir && File.directory?(dir) && File.exist?(Savepoint.intent_file(dir))
66
+ end
67
+
68
+ def store_directory?(dir)
69
+ dir && File.directory?(dir)
70
+ end
71
+
72
+ def valid_dir?(verb, dir)
73
+ DIR_KIND.fetch(verb, :intent) == :store ? store_directory?(dir) : intent_directory?(dir)
74
+ end
75
+
76
+ def opt(args, name)
77
+ (i = args.index(name)) && args[i + 1]
78
+ end
79
+
80
+ def unrecognized_flag(verb, args)
81
+ allowed = KNOWN_FLAGS.fetch(verb, [])
82
+ args.find { |a| a.is_a?(String) && a.start_with?("--") && !allowed.include?(a) }
83
+ end
84
+
85
+ def main(argv)
86
+ args = argv.dup
87
+ verb = args.shift
88
+
89
+ unless verb
90
+ usage
91
+ return 2
92
+ end
93
+
94
+ unless VERBS.include?(verb)
95
+ warn "graph-measure: unknown subcommand #{verb.inspect}"
96
+ usage
97
+ return 2
98
+ end
99
+
100
+ dir_arg = args.shift
101
+ unless valid_dir?(verb, dir_arg && File.expand_path(dir_arg))
102
+ label = DIR_KIND.fetch(verb, :intent) == :store ? "not a store directory" : "not an intent directory"
103
+ warn "graph-measure: #{dir_arg.inspect} is #{label}"
104
+ usage
105
+ return 2
106
+ end
107
+ dir = File.expand_path(dir_arg)
108
+
109
+ bad_flag = unrecognized_flag(verb, args)
110
+ if bad_flag
111
+ warn "graph-measure: unknown flag #{bad_flag.inspect} for #{verb}"
112
+ usage
113
+ return 2
114
+ end
115
+
116
+ format = opt(args, "--format") || "text"
117
+ unless %w[text json].include?(format)
118
+ warn "graph-measure: unknown --format #{format.inspect} (use text or json)"
119
+ usage
120
+ return 2
121
+ end
122
+
123
+ run_lazy(verb, dir, format)
124
+ rescue StandardError => e
125
+ # Row 2.22: a malformed graph.md, a torn ledger, or invalid UTF-8 never
126
+ # raises out of GraphMeasure or GraphMeasureReport (both are built to
127
+ # resolve broken data to an anomaly, never an exception) - this is
128
+ # defence in depth so a defect in either never reaches the owner as a
129
+ # bare Ruby backtrace.
130
+ warn "graph-measure: internal error: #{e.message}"
131
+ 1
132
+ end
133
+
134
+ def run_lazy(verb, dir, format)
135
+ LAZY_MODULES.fetch(verb, []).each { |(_name, file)| require_relative(File.join("lib", file)) }
136
+ rescue LoadError
137
+ warn "graph-measure: #{verb} is not yet delivered (its module has not landed)"
138
+ 3
139
+ else
140
+ dispatch_lazy(verb, dir, format)
141
+ end
142
+
143
+ def dispatch_lazy(verb, dir, format)
144
+ case verb
145
+ when "intent"
146
+ run_intent(dir, format)
147
+ when "budget"
148
+ run_budget(dir, format)
149
+ when "cohorts"
150
+ run_cohorts(dir, format)
151
+ else
152
+ warn "graph-measure: #{verb}'s module loaded but no dispatcher is wired up yet"
153
+ 3
154
+ end
155
+ end
156
+
157
+ # M1: the boundary's own config resolution, never GraphMeasureModels' or
158
+ # GraphMeasureCohorts' (spec D2, both modules' own DI convention). Mirrors
159
+ # scripts/doctor.rb's own `PLASTIC_HOME` fallback and `load_yaml_safe`
160
+ # shape exactly, so a missing or malformed config.yml/project.yml resolves
161
+ # to `{}` (the shipped-default chain), never an exception.
162
+ def plastic_home
163
+ ENV.fetch("PLASTIC_HOME") { File.join(Dir.home, ".plastic") }
164
+ end
165
+
166
+ def load_yaml_safe(path)
167
+ return {} unless path && File.exist?(path)
168
+
169
+ YAML.safe_load(File.read(path), permitted_classes: [Date, Time]) || {}
170
+ rescue StandardError
171
+ {}
172
+ end
173
+
174
+ def missing_savepoint(dir)
175
+ savepoint_path = File.join(dir, SAVEPOINT_FILE)
176
+ return nil if File.exist?(savepoint_path)
177
+
178
+ warn "graph-measure: #{savepoint_path} does not exist, nothing to measure"
179
+ savepoint_path
180
+ end
181
+
182
+ def run_intent(dir, format)
183
+ # Row 2.5: checked directly, ahead of GraphMeasure.read, which resolves a
184
+ # missing file to an empty (not absent) record by design (D3) - the CLI
185
+ # boundary is the only place that can still tell "nothing to measure"
186
+ # apart from "measured, and it is empty".
187
+ return 1 if missing_savepoint(dir)
188
+
189
+ record = GraphMeasure.read(dir)
190
+ puts format == "json" ? GraphMeasureReport.render_json(record) : GraphMeasureReport.render_text(record)
191
+ 0
192
+ end
193
+
194
+ def run_budget(dir, format)
195
+ return 1 if missing_savepoint(dir)
196
+
197
+ record = GraphMeasureBudget.read(dir)
198
+ puts format == "json" ? GraphMeasureBudget.render_json(record) : GraphMeasureBudget.render_text(record)
199
+ 0
200
+ end
201
+
202
+ # Row 2.5's "missing savepoint.md" check does not apply here: `dir` is a
203
+ # STORE directory (spec D1's "the directory holding id--slug/ intent
204
+ # folders"), which never carries a savepoint.md of its own.
205
+ #
206
+ # n6 adds the rates, the latency and the bar to this same verb, sourced
207
+ # from `GraphMeasureCohorts`. Its bar section reuses the one
208
+ # `GraphMeasureModels` walk's `qualified` counts (passed in here) rather
209
+ # than triggering a second store walk for the same numbers.
210
+ #
211
+ # M1 (v1 review): this call used to pass neither `project_config:` nor
212
+ # `global_config:`, so the model comparison always resolved against
213
+ # RunnerPolicy's shipped defaults only, never the real project-then-
214
+ # global-then-default chain the CHANGELOG describes and
215
+ # `scripts/doctor.rb`'s own `model_requalification_checks` actually reads.
216
+ # Reproduced: with a `plastic-executor:` override present in global
217
+ # config, `GraphMeasureModels.read` reports real drift rows while this
218
+ # verb (unpatched) always reported zero. `GraphMeasure`'s own modules stay
219
+ # config-free by design (D2's DI convention: they never read disk or ENV
220
+ # themselves) - this executable is the boundary that is allowed to, the
221
+ # same role `scripts/doctor.rb` already plays for the same two config
222
+ # files.
223
+ #
224
+ # NEW-4 (v2 review, D22): the project-scope half named `<store>/../project.yml`,
225
+ # a file whose template carries no `agents:` key at all, so a real
226
+ # operator override at the file that DOES exist in the wild - the store's
227
+ # sibling `config.yml` - was silently ignored. The path now comes from
228
+ # `GraphMeasureModels.project_config_path`, the one function
229
+ # `scripts/doctor.rb`'s own rule calls too, so the two commands cannot
230
+ # resolve a different file for the same store.
231
+ def run_cohorts(dir, format)
232
+ global_config = load_yaml_safe(File.join(plastic_home, "config.yml"))
233
+ project_config = load_yaml_safe(GraphMeasureModels.project_config_path(dir))
234
+
235
+ models_record = GraphMeasureModels.read(dir, project_config: project_config, global_config: global_config)
236
+ cohorts_record = GraphMeasureCohorts.read(dir, qualified: models_record[:qualified],
237
+ project_config: project_config, global_config: global_config)
238
+ if format == "json"
239
+ combined = GraphMeasureModels.model(models_record).merge(GraphMeasureCohorts.model(cohorts_record))
240
+ puts JSON.generate(combined)
241
+ else
242
+ puts GraphMeasureModels.render_text(models_record)
243
+ puts GraphMeasureCohorts.render_text(cohorts_record)
244
+ end
245
+ 0
246
+ end
247
+ end
248
+
249
+ exit GraphMeasureCLI.main(ARGV) if $PROGRAM_NAME == __FILE__
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ # Usage: hook-call-budget (reads the PreToolUse stdin JSON payload)
6
+ #
7
+ # Intent 355, n2 (review fix n8). A node gets a call budget by kind
8
+ # (RunnerPolicy.call_cap): a cap on tool calls per attempt. RunnerDispatch
9
+ # writes the cap onto the running line as `calls=` and states it as one
10
+ # sentence in the node's own input, so the executor learns the number before
11
+ # it ever gets denied.
12
+ #
13
+ # Inside a subagent the hook input's session_id and transcript_path are the
14
+ # MAIN session's, never the subagent's own (v1 finding). So this hook caps
15
+ # ONLY a subagent call: agent_id present in the payload. A call with no
16
+ # agent_id (the main thread, a plain conversation, a live --agent session's
17
+ # own turns) is never capped and the decision returns before any file is
18
+ # touched.
19
+ #
20
+ # Which node a subagent is working is read from the subagent's OWN
21
+ # transcript, at <dirname(transcript_path)>/<session_id>/subagents/
22
+ # agent-<agent_id>.jsonl (the main transcript_path plus the main session_id
23
+ # plus the subagent's own agent_id): its first user record carries the
24
+ # dispatch prompt, and the lead/spawn block always pastes the node's input
25
+ # path into that prompt (INPUT_PATH_RE). The node input path names the intent
26
+ # directory directly (its grandparent), so this hook never scans the store to
27
+ # find it. No node input path in that first prompt means allow.
28
+ #
29
+ # The cap applies only when that node's LATEST transition line in
30
+ # <intent>/savepoint.md is `running` and carries `calls=`; a later `done` or
31
+ # `failed_verification` line (or a sibling node's own running line) means no
32
+ # cap. Past the cap, a Bash call that runs only a safe git verb (add, commit,
33
+ # status, rev-parse, log) is still allowed, so the denial the hook itself
34
+ # orders (commit what is green) is never the thing the hook blocks.
35
+ #
36
+ # Pure and dependency-injected down to the paths it reads; the trailing block
37
+ # guarded by `$PROGRAM_NAME == __FILE__` is this file's only I/O (stdin,
38
+ # stdout), so a test can `load` this file and call HookCallBudget's module
39
+ # functions directly without spawning a process or touching stdin.
40
+
41
+ require "json"
42
+ require_relative "lib/node_ledger"
43
+ require_relative "lib/savepoint"
44
+
45
+ module HookCallBudget
46
+ module_function
47
+
48
+ RETURN_INSTRUCTION = "commit what is green and return failed_verification reason=call_budget"
49
+
50
+ # A node id, then "--a", then an attempt number, inside a /attempts/
51
+ # directory: the exact shape RunnerDispatch's spawn block pastes into a
52
+ # subagent's own first prompt. Captures [full_path, node].
53
+ INPUT_PATH_RE = %r{(\S+/attempts/([a-z]{1,2}\d+)--a\d+\.input)}.freeze
54
+
55
+ FIRST_RECORD_SCAN_LIMIT = 20
56
+
57
+ GIT_ALLOWED_VERBS = %w[add commit status rev-parse log].freeze
58
+ GIT_UNSAFE_RE = /[;&|\n]|\$\(|`/.freeze
59
+
60
+ # The one decision: nil (allow, no output) or a deny payload Hash. Never
61
+ # raises across its own boundary.
62
+ def decide(payload)
63
+ return nil unless payload.is_a?(Hash)
64
+
65
+ agent_id = payload["agent_id"]
66
+ return nil if agent_id.to_s.strip.empty?
67
+
68
+ session_id = payload["session_id"].to_s
69
+ transcript_path = payload["transcript_path"].to_s
70
+ return nil if session_id.empty? || transcript_path.empty?
71
+
72
+ sub_path = subagent_transcript_path(transcript_path, session_id, agent_id)
73
+
74
+ prompt = begin
75
+ first_user_prompt_text(sub_path)
76
+ rescue StandardError
77
+ nil
78
+ end
79
+ return nil unless prompt
80
+
81
+ match = INPUT_PATH_RE.match(prompt)
82
+ return nil unless match
83
+
84
+ node = match[2]
85
+ intent_dir = File.dirname(File.dirname(match[1]))
86
+
87
+ cap = node_cap(intent_dir, node)
88
+ return nil unless cap
89
+
90
+ count = begin
91
+ count_tool_use_blocks(sub_path)
92
+ rescue StandardError => e
93
+ note_unreadable(intent_dir, session_id, e.message)
94
+ return nil
95
+ end
96
+
97
+ return nil if count <= cap
98
+ return nil if git_only_bash_call?(payload)
99
+
100
+ deny_payload("#{node} is past its call budget (#{count}/#{cap} tool calls); #{RETURN_INSTRUCTION}")
101
+ end
102
+
103
+ def subagent_transcript_path(main_transcript_path, session_id, agent_id)
104
+ File.join(File.dirname(main_transcript_path), session_id, "subagents", "agent-#{agent_id}.jsonl")
105
+ end
106
+
107
+ # The first `"type":"user"` record in `path`'s own transcript, rendered to
108
+ # plain text (message.content as a string, or the joined text blocks of a
109
+ # content array). Scans at most FIRST_RECORD_SCAN_LIMIT lines: the dispatch
110
+ # prompt is always the transcript's opening record, so this never touches
111
+ # the bulk of a long-running subagent's history. Raises on a genuinely
112
+ # unreadable path (a directory, a permission error); the caller decides
113
+ # what that means.
114
+ def first_user_prompt_text(path)
115
+ File.open(path) do |f|
116
+ f.each_line.first(FIRST_RECORD_SCAN_LIMIT).each do |raw|
117
+ record = begin
118
+ JSON.parse(raw)
119
+ rescue JSON::ParserError
120
+ next
121
+ end
122
+ next unless record.is_a?(Hash) && record["type"] == "user"
123
+
124
+ return message_text(record)
125
+ end
126
+ end
127
+ nil
128
+ end
129
+
130
+ def message_text(record)
131
+ content = record.dig("message", "content")
132
+ case content
133
+ when String
134
+ content
135
+ when Array
136
+ content.select { |b| b.is_a?(Hash) && b["type"] == "text" }.map { |b| b["text"].to_s }.join("\n")
137
+ else
138
+ ""
139
+ end
140
+ end
141
+
142
+ # The cap named on `node`'s LATEST (file-order) non-torn transition line in
143
+ # `intent_dir`/savepoint.md, or nil when that line is not `running`, carries
144
+ # no `calls=`, or does not exist at all (matrix 2.6/2.6b: a later done/
145
+ # failed_verification line, or a sibling node's own running line, never
146
+ # caps this node).
147
+ def node_cap(intent_dir, node)
148
+ path = File.join(intent_dir, "savepoint.md")
149
+ return nil unless File.exist?(path)
150
+
151
+ latest = NodeLedger.entries(path).select { |e| !e[:torn] && e[:subject] == node }.last
152
+ return nil unless latest && latest[:state] == "running"
153
+
154
+ cap = (latest[:fields] || {})["calls"]
155
+ cap ? cap.to_i : nil
156
+ end
157
+
158
+ # Streams `path` a line at a time, counting `"type":"tool_use"` content
159
+ # blocks with a plain regex rather than a full JSON parse per line (matrix
160
+ # 2.5/2.8): a 3 MB transcript counts in well under 50 ms this way.
161
+ def count_tool_use_blocks(path)
162
+ count = 0
163
+ File.foreach(path) { |ln| count += ln.scan(/"type"\s*:\s*"tool_use"/).length }
164
+ count
165
+ end
166
+
167
+ # Past the cap, a Bash call is still allowed when its command runs only one
168
+ # safe git verb (matrix 2.7/D3): the git add/commit the denial itself
169
+ # orders must never be the call the hook blocks. Any chaining token
170
+ # (`;`, `&`, `|`, a newline, a subshell or a backtick) refuses the whole
171
+ # command, so a compound command can never smuggle an unrelated call past
172
+ # the cap riding on a leading `git`.
173
+ def git_only_bash_call?(payload)
174
+ return false unless payload["tool_name"].to_s == "Bash"
175
+
176
+ command = payload.dig("tool_input", "command").to_s.strip
177
+ return false if command.empty? || GIT_UNSAFE_RE.match?(command)
178
+
179
+ tokens = command.split(/\s+/)
180
+ return false unless tokens.shift == "git"
181
+
182
+ tokens.shift(2) if tokens.first == "-C"
183
+ GIT_ALLOWED_VERBS.include?(tokens.first)
184
+ end
185
+
186
+ def deny_payload(reason)
187
+ { "hookSpecificOutput" => { "hookEventName" => "PreToolUse", "permissionDecision" => "deny",
188
+ "permissionDecisionReason" => reason } }
189
+ end
190
+
191
+ # Best-effort note that the transcript could not be counted (matrix 2.9): a
192
+ # plain milestone line, the same shared primitive Commit/Review/Report
193
+ # lines already use, dedup'd on (stage, milestone) so a repeat failure in
194
+ # the same session with the same message is not written twice.
195
+ def note_unreadable(intent_dir, session_id, message, now: Time.now)
196
+ Savepoint.append_savepoint_line(intent_dir, "CallBudget", "transcript unreadable for #{session_id}: #{message}",
197
+ now)
198
+ rescue StandardError
199
+ nil
200
+ end
201
+ end
202
+
203
+ if $PROGRAM_NAME == __FILE__
204
+ raw = begin
205
+ $stdin.read
206
+ rescue StandardError
207
+ nil
208
+ end
209
+
210
+ if raw && !raw.strip.empty?
211
+ payload = begin
212
+ JSON.parse(raw)
213
+ rescue StandardError
214
+ nil
215
+ end
216
+
217
+ result = HookCallBudget.decide(payload)
218
+ puts JSON.generate(result) if result
219
+ end
220
+
221
+ exit 0
222
+ end
@@ -5,16 +5,18 @@
5
5
  # Usage: hook-capture (reads the UserPromptSubmit stdin JSON payload)
6
6
  # Intent 298. One UserPromptSubmit process replacing hook-continue,
7
7
  # hook-future-intent-check, and hook-auto-arm: it appends a pending line to
8
- # the session day ledger, detects "continue" and "auto" prompts, and hints at
9
- # matching Future intents. Every job runs in its own rescue and the hook
10
- # always exits 0 (spec D2).
8
+ # the session day ledger and detects "continue" and "auto" prompts. Every job
9
+ # runs in its own rescue and the hook always exits 0 (spec D2). Intent 345
10
+ # (D7, 323) removed the per-prompt Future-intent hint step entirely.
11
11
 
12
+ require_relative "lib/store_layout"
12
13
  require "json"
13
- require "yaml"
14
14
  require "open3"
15
+ require "time"
15
16
  require "fileutils"
16
17
  require_relative "lib/session_ledger"
17
18
  require_relative "lib/dashboard_banner"
19
+ require_relative "lib/active_delivery"
18
20
 
19
21
  raw = begin
20
22
  $stdin.read
@@ -37,124 +39,42 @@ cwd = payload["cwd"].to_s
37
39
  cwd = Dir.pwd if cwd.empty?
38
40
 
39
41
  plastic_home = File.expand_path(ENV["PLASTIC_HOME"] || "~/.plastic")
40
- store = File.join(plastic_home, "store")
42
+ store = Plastic::StoreLayout.global_store(plastic_home)
41
43
  templates = File.expand_path("../templates", __dir__)
42
44
  sid = SessionLedger.short_session_id(nil, session_id)
43
45
  today = SessionLedger.day_id
44
46
 
45
- # --- Extracted matching logic, shared by both the global and project store
46
- # passes of job (f) below (mirrors hook-future-intent-check verbatim). ---
47
- def future_intent_matches(store_root, message)
48
- index_path = File.join(store_root, "INDEX.md")
49
- return [] unless File.exist?(index_path)
50
-
51
- lines = File.readlines(index_path)
52
- future_dirs = []
53
- section = nil
54
-
55
- lines.each do |line|
56
- section = :future if line.start_with?("## Future")
57
- section = nil if line.start_with?("## ") && !line.start_with?("## Future")
58
- next unless section == :future && line.strip.start_with?("- [")
59
-
60
- future_dirs << $1 if line =~ /store\/([\w-]+)\//
61
- end
62
- return [] if future_dirs.empty?
63
-
64
- matches = []
65
- message_words = message.split(/\W+/).reject { |w| w.length < 4 }
66
-
67
- future_dirs.each do |dir|
68
- intent_path = File.join(store_root, "store", dir, "#{dir}.md")
69
- next unless File.exist?(intent_path)
70
-
71
- content = File.read(intent_path)
72
-
73
- tags = []
74
- tags = $1.split(",").map(&:strip).map(&:downcase) if content =~ /^tags:\s*\[([^\]]+)\]/
75
-
76
- intent_name = ""
77
- intent_name = $1.downcase if content =~ /^intent:\s*["']?(.+?)["']?\s*$/
78
-
79
- keywords = (tags + intent_name.split(/\W+/).reject { |w| w.length < 4 }).map(&:downcase).uniq
80
-
81
- matched_keywords = keywords.select { |kw| message.include?(kw) }
82
- name_matches = message_words.select { |w| intent_name.include?(w) }
83
- matched_keywords = (matched_keywords + name_matches).uniq
84
-
85
- next if matched_keywords.empty?
86
-
87
- index_line = lines.find { |l| l.include?(dir) }&.strip || "#{dir} - #{intent_name}"
88
- matches << { "index_line" => index_line, "keywords" => matched_keywords }
89
- end
90
- matches
91
- end
92
-
93
- # The store_root that carries the current project's own store, resolved from
94
- # cwd through projects.yml, mirroring SessionLedger.project_slug's matching
95
- # but returning nil (not "global") when nothing matches, since there is then
96
- # no distinct second store to hint against.
97
- def resolve_project_store_root(cwd, plastic_home)
98
- projects_path = File.join(plastic_home, "projects.yml")
99
- return nil unless File.exist?(projects_path)
100
-
101
- data = begin
102
- YAML.safe_load(File.read(projects_path))
103
- rescue StandardError
104
- nil
105
- end
106
- data = {} unless data.is_a?(Hash)
107
- projects = data["projects"].is_a?(Hash) ? data["projects"] : {}
108
- expanded_cwd = File.expand_path(cwd)
109
-
110
- matches = projects.filter_map do |slug, info|
111
- next unless info.is_a?(Hash) && slug.is_a?(String)
112
-
113
- path = info["path"]
114
- next unless path
115
-
116
- root = File.expand_path(path)
117
- next unless expanded_cwd == root || expanded_cwd.start_with?("#{root}#{File::SEPARATOR}")
118
-
119
- [root.length, slug]
120
- end
121
-
122
- best = matches.max_by { |(length, _slug)| length }
123
- best ? File.join(plastic_home, "projects", best[1]) : nil
124
- end
125
-
126
47
  def truncate(text, max)
127
48
  return text if text.length <= max
128
49
 
129
50
  "#{text[0, max - 3]}..."
130
51
  end
131
52
 
132
- # --- (a) tmp root + heartbeat, guarded on the pointer (spec D9) ------------
133
- # Session start writes the `current` pointer; a capture with no pointer yet
53
+ # --- (a) heartbeat, guarded on the session tmp dir already existing --------
54
+ # Session start creates `.tmp/<sid>/`; a capture with no such directory yet
134
55
  # means session start never ran for this session at all (a `-p` print
135
56
  # session, a resumed background job, an unregistered SessionStart hook), so
136
- # neither the per-session dir nor its heartbeat is created. UserPromptSubmit
137
- # fires before PostToolUse, so capture is the FIRST creator of every orphan
138
- # `.tmp/<sid>/` this guard prevents; hook-record carries the same guard for
139
- # its own (later) heartbeat write.
57
+ # neither the directory nor its heartbeat is created HERE either.
58
+ # UserPromptSubmit fires before PostToolUse, so capture is the FIRST place
59
+ # this guard matters; hook-record carries the same guard for its own
60
+ # (later) heartbeat write. (344 n2, D4-D5)
140
61
  begin
141
- if File.exist?(SessionLedger.pointer_path(store, sid))
142
- SessionLedger.ensure_tmp_root(store)
143
- FileUtils.mkdir_p(SessionLedger.session_tmp_dir(store, sid))
62
+ if Dir.exist?(SessionLedger.session_tmp_dir(store, sid))
144
63
  File.write(SessionLedger.heartbeat_path(store, sid), "#{Time.now.utc.iso8601}\n")
145
64
  end
146
65
  rescue StandardError
147
66
  nil
148
67
  end
149
68
 
150
- # --- (b) read the pointer -----------------------------------------------
69
+ # --- (b) a live delivery lock names this session (344 n2, D5, D12) ----------
151
70
  skip_day_ledger = false
152
71
  begin
153
- pointer = SessionLedger.pointer_path(store, sid)
154
- if File.exist?(pointer)
155
- current = File.read(pointer).strip
156
- skip_day_ledger = !current.empty? && current != today
157
- end
72
+ full_session = session_id.empty? ? ENV["CLAUDE_CODE_SESSION_ID"].to_s : session_id
73
+ skip_day_ledger = !ActiveDelivery.resolve(
74
+ global_store: store,
75
+ project_roots: ActiveDelivery.project_roots(plastic_home),
76
+ session: full_session
77
+ ).nil?
158
78
  rescue StandardError
159
79
  nil
160
80
  end
@@ -183,13 +103,13 @@ system_message = nil
183
103
 
184
104
  # --- (d) "continue" cockpit -------------------------------------------------
185
105
  begin
186
- if prompt.match?(/\bcontinue\b/i)
106
+ if prompt.to_s.strip.downcase == "continue"
187
107
  dashboard = File.expand_path("dashboard.rb", __dir__)
188
108
  if File.exist?(dashboard)
189
109
  cockpit, _err, status = Open3.capture3({ "RUBYOPT" => nil }, "ruby", dashboard, "continue")
190
110
  if status.success? && !cockpit.strip.empty?
191
111
  context_parts << (cockpit.rstrip +
192
- "\n\nFollow the plastic-intent-continuing skill workflow to resume: " \
112
+ "\n\nRun `plastic continue` to resume: " \
193
113
  "if active intents exist, read their state and resume; otherwise surface the " \
194
114
  "most-valuable next work and any stale or triage intents from the board above.")
195
115
 
@@ -216,7 +136,7 @@ begin
216
136
  auto_triggers = ["take it from here", "deliver this", "/plastic-auto", "auto mode"].freeze
217
137
  triggered = auto_triggers.any? { |p| message_lower.include?(p) } || message_lower.match?(/\bauto\b/)
218
138
  if triggered
219
- context_parts << "Auto-mode request detected. Invoke the plastic-auto skill to deliver the " \
139
+ context_parts << "Auto-mode request detected. Run `plastic auto take ID` to deliver the " \
220
140
  "active intent autonomously - it takes the intent under a lock and a worktree " \
221
141
  "and points this session at it."
222
142
  end
@@ -224,25 +144,6 @@ rescue StandardError
224
144
  nil
225
145
  end
226
146
 
227
- # --- (f) Future-intent hint, global store then the project's own store -----
228
- begin
229
- message = prompt.to_s.downcase
230
- if message.strip.length >= 10 && message.strip != "continue"
231
- matches = future_intent_matches(plastic_home, message)
232
- project_root = resolve_project_store_root(cwd, plastic_home)
233
- matches += future_intent_matches(project_root, message) if project_root
234
-
235
- if matches.any?
236
- parts = ["PLASTIC - Future intents related to this message:\n"]
237
- matches.each { |m| parts << "#{m["index_line"]} (matched: #{m["keywords"].join(", ")})" }
238
- parts << "\nConsider asking the user if they want to activate any of these, or note the connection."
239
- context_parts << parts.join("\n")
240
- end
241
- end
242
- rescue StandardError
243
- nil
244
- end
245
-
246
147
  exit 0 if context_parts.empty?
247
148
 
248
149
  payload_out = {