@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
@@ -18,6 +18,7 @@
18
18
  # rebuilds an empty ledger.
19
19
 
20
20
  require_relative "lib/roadmap_savepoint"
21
+ require_relative "lib/guarded_append"
21
22
 
22
23
  def opt(args, name)
23
24
  (i = args.index(name)) && args[i + 1]
@@ -47,6 +48,12 @@ when "append"
47
48
  rescue ArgumentError => e
48
49
  warn "append: #{e.message}"
49
50
  exit 2
51
+ rescue GuardedAppend::Unavailable => e
52
+ # Intent 335 (spec D14): the write guard could not be taken; nothing was written. Exit 4
53
+ # rather than let a backtrace reach the six skill recipes that drive this CLI - it rescues
54
+ # ArgumentError alone today and would otherwise print one straight to the terminal.
55
+ warn "append: #{e.message}"
56
+ exit 4
50
57
  end
51
58
 
52
59
  when "rebuild"
package/scripts/runner ADDED
@@ -0,0 +1,581 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ require "yaml"
6
+ require "stringio"
7
+ require_relative "lib/savepoint"
8
+ require_relative "lib/ready_set"
9
+ require_relative "lib/runner_core"
10
+ require_relative "lib/harness_adapter"
11
+ require_relative "lib/runner_proposals"
12
+
13
+ # runner - the one executable over the graph-ready loop's declared node graph
14
+ # (intent 340, G7, n1). A subcommand table: the public verbs (step, status,
15
+ # answer) are the only ones the skill body ever names; the internal verbs
16
+ # (ready, sweep, rewind, until-empty) route and work so `step` can compose
17
+ # them and so each can be tested alone, but stay out of the usage text (327
18
+ # spec, 340b n7).
19
+ #
20
+ # `step`, `sweep`, `answer`, `rewind` and `until-empty` route to modules
21
+ # later nodes deliver (RunnerDispatch and RunnerAbsorb for `step`;
22
+ # RunnerSweep, RunnerAnswer, RunnerRewind, RunnerUntilEmpty for the rest).
23
+ # Each is required LAZILY, inside its own verb branch: a verb whose module
24
+ # has not landed yet reports plainly and exits nonzero, and every other verb
25
+ # - most importantly `status`, which an operator polls constantly across all
26
+ # five of this intent's dispatches - keeps working.
27
+ #
28
+ # Usage:
29
+ # runner <step|status|answer> <intent_dir> [--node ID] [--answer TEXT]
30
+ #
31
+ # Exit codes (matching the family scripts/node-transition and
32
+ # scripts/ready-set already use):
33
+ # 0 - success
34
+ # 1 - the verb ran but reports failure (an invalid graph for `ready`)
35
+ # 2 - usage: unknown verb, or not an intent directory
36
+ # 3 - a routed verb's module has not been delivered yet
37
+ module Runner
38
+ module_function
39
+
40
+ PUBLIC_VERBS = %w[step status answer].freeze
41
+ INTERNAL_VERBS = %w[ready sweep rewind until-empty watch].freeze
42
+ VERBS = (PUBLIC_VERBS + INTERNAL_VERBS).freeze
43
+
44
+ # verb -> [[ModuleName, lib_file], ...], required lazily inside the verb's
45
+ # own branch (row 1.6). `step` composes two modules (dispatching new work,
46
+ # then absorbing what a sub-agent reported back); every other routed verb
47
+ # names exactly one.
48
+ LAZY_MODULES = {
49
+ "step" => [["RunnerSweep", "runner_sweep"], ["RunnerAbsorb", "runner_absorb"],
50
+ ["RunnerDispatch", "runner_dispatch"]],
51
+ "sweep" => [["RunnerSweep", "runner_sweep"]],
52
+ "answer" => [["RunnerAnswer", "runner_answer"]],
53
+ "rewind" => [["RunnerRewind", "runner_rewind"]],
54
+ "until-empty" => [["RunnerUntilEmpty", "runner_until_empty"]],
55
+ "watch" => [["RunnerWatch", "runner_watch"]],
56
+ }.freeze
57
+
58
+ def usage
59
+ warn "Usage: runner <#{PUBLIC_VERBS.join('|')}> <intent_dir> [--node ID] [--answer TEXT]"
60
+ end
61
+
62
+ def intent_directory?(dir)
63
+ dir && File.directory?(dir) && File.exist?(Savepoint.intent_file(dir))
64
+ end
65
+
66
+ def opt(args, name)
67
+ (i = args.index(name)) && args[i + 1]
68
+ end
69
+
70
+ # opt_all(args, name) -> every value given for a repeated flag, in the order
71
+ # they appear, or [] when the flag never occurs (intent 340, G7, n8, row
72
+ # 8.1-8.3). `run_step` needs this, not `opt`, for `--return`: one step may
73
+ # carry more than one `--return NODE=PATH` pair (one per node the session
74
+ # is reporting back on), and `opt`'s single-value semantics would silently
75
+ # keep only the first, absorbing one node while stranding the other's
76
+ # lease until it expires.
77
+ def opt_all(args, name)
78
+ args.each_index.select { |i| args[i] == name }.map { |i| args[i + 1] }.compact
79
+ end
80
+
81
+ # KNOWN_FLAGS (minor 2): the exact flag vocabulary each verb accepts. A
82
+ # flag not in a verb's own list is refused rather than silently ignored -
83
+ # a mistyped `--session` used to vanish with no effect and the runner
84
+ # simply acted as whichever session `CLAUDE_CODE_SESSION_ID` names.
85
+ KNOWN_FLAGS = {
86
+ "step" => %w[--return --allow-core-drift --harness],
87
+ "answer" => %w[--node --answer],
88
+ "rewind" => %w[--node --confirm],
89
+ "until-empty" => %w[--harness],
90
+ "watch" => %w[--dispatch --harness --install-timer --home],
91
+ }.freeze
92
+
93
+ # The explicit YAML document-end marker (intent 340b, G7c, n1, row 1.28)
94
+ # separating the dispatch plan `step` has always printed from the
95
+ # harness-rendered block underneath it: stdout has never been one YAML
96
+ # document (absorb/reclaim/extend/reap lines print before the plan,
97
+ # decision/park lines after it), so a reader splits on this marker rather
98
+ # than guessing where the plan ends.
99
+ DOCUMENT_BOUNDARY = "...".freeze
100
+
101
+ # unrecognized_flag(verb, args) -> the first token in `args` that looks
102
+ # like a flag (`--...`) and is not in `verb`'s own KNOWN_FLAGS, or nil. A
103
+ # verb absent from KNOWN_FLAGS (status, ready, sweep) accepts none.
104
+ def unrecognized_flag(verb, args)
105
+ allowed = KNOWN_FLAGS.fetch(verb, [])
106
+ args.find { |a| a.is_a?(String) && a.start_with?("--") && !allowed.include?(a) }
107
+ end
108
+
109
+ def main(argv)
110
+ args = argv.dup
111
+ verb = args.shift
112
+
113
+ unless VERBS.include?(verb)
114
+ warn "runner: unknown verb #{verb.inspect}"
115
+ usage
116
+ exit 2
117
+ end
118
+
119
+ intent_dir_arg = args.shift
120
+ unless intent_directory?(intent_dir_arg && File.expand_path(intent_dir_arg))
121
+ warn "runner: #{intent_dir_arg.inspect} is not an intent directory"
122
+ usage
123
+ exit 2
124
+ end
125
+ intent_dir = File.expand_path(intent_dir_arg)
126
+
127
+ bad_flag = unrecognized_flag(verb, args)
128
+ if bad_flag
129
+ warn "runner: unknown flag #{bad_flag.inspect} for #{verb}"
130
+ usage
131
+ exit 2
132
+ end
133
+
134
+ case verb
135
+ when "status"
136
+ run_status(intent_dir)
137
+ when "ready"
138
+ run_ready(intent_dir)
139
+ else
140
+ run_lazy(verb, intent_dir, args)
141
+ end
142
+ end
143
+
144
+ def run_status(intent_dir)
145
+ context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
146
+ rows = RunnerCore.status(context)
147
+
148
+ if rows.empty?
149
+ puts "No declared nodes."
150
+ else
151
+ rows.each do |id, view|
152
+ puts "#{id} kind=#{view[:kind]} state=#{view[:state]} ready=#{view[:ready]}"
153
+ last = view[:last_transition]
154
+ puts " last: #{last[:raw]}" if last
155
+ end
156
+ end
157
+
158
+ graph = context.graph || {}
159
+ fixes = RunnerProposals.review_fix_count(graph[:edges] || {}, graph[:nodes] || {})
160
+ puts "review fixes: #{fixes} of #{RunnerProposals::REVIEW_FIX_CAP}"
161
+
162
+ complete = RunnerCore.complete?(context)
163
+ puts
164
+ puts complete ? "complete" : "stalled"
165
+ unless complete
166
+ rows.each do |id, view|
167
+ next if ReadySet::TERMINAL_STATES.include?(view[:state])
168
+
169
+ view[:blockers].each { |b| puts "#{id} blocked: #{b}" }
170
+ end
171
+ end
172
+
173
+ exit 0
174
+ end
175
+
176
+ def run_ready(intent_dir)
177
+ analysis = ReadySet.analyze(intent_dir)
178
+ unless analysis[:ok]
179
+ warn "runner: #{analysis[:errors].join('; ')}"
180
+ exit 1
181
+ end
182
+
183
+ if analysis[:ranked_ready].empty?
184
+ puts "Ready: (none)"
185
+ else
186
+ puts "Ready: #{analysis[:ranked_ready].map { |r| r[:id] }.join(', ')}"
187
+ end
188
+ exit 0
189
+ end
190
+
191
+ # Requires this verb's module(s) lazily, then hands off. Nothing in n1
192
+ # delivers runner_dispatch.rb, runner_absorb.rb, runner_sweep.rb,
193
+ # runner_answer.rb, or runner_rewind.rb, so every LAZY_MODULES require
194
+ # raises LoadError today and every routed verb reports "not yet
195
+ # delivered" - the only thing this node needs to prove (row 1.6). Once a
196
+ # later node ships its file, the require succeeds and falls through to
197
+ # dispatch_lazy, which that node replaces with the real call.
198
+ def run_lazy(verb, intent_dir, args)
199
+ # The require target below is a runtime value, never an inline quoted
200
+ # literal, so it falls outside test/install_packaging_test.rb's
201
+ # require-closure guard on purpose: every file LAZY_MODULES names is a
202
+ # genuine later-node deliverable, none of which core_files should track
203
+ # until that node actually lands it.
204
+ LAZY_MODULES.fetch(verb, []).each { |(_name, file)| require_relative(File.join("lib", file)) }
205
+ rescue LoadError
206
+ warn "runner: #{verb} is not yet delivered (its module has not landed)"
207
+ exit 3
208
+ else
209
+ dispatch_lazy(verb, intent_dir, args)
210
+ end
211
+
212
+ def dispatch_lazy(verb, intent_dir, args)
213
+ case verb
214
+ when "step"
215
+ run_step(intent_dir, args)
216
+ when "sweep"
217
+ run_sweep(intent_dir, args)
218
+ when "answer"
219
+ run_answer(intent_dir, args)
220
+ when "rewind"
221
+ run_rewind(intent_dir, args)
222
+ when "until-empty"
223
+ run_until_empty(intent_dir, args)
224
+ when "watch"
225
+ run_watch(intent_dir, args)
226
+ else
227
+ warn "runner: #{verb}'s module loaded but no dispatcher is wired up yet"
228
+ exit 3
229
+ end
230
+ end
231
+
232
+ # run_sweep (intent 340, G7, n8): the standalone `sweep` verb, unreachable
233
+ # from the CLI until this node - `dispatch_lazy` had no `when "sweep"` arm
234
+ # even though `RunnerSweep.run` (n2) is a complete, documented standalone
235
+ # entry point, exactly the shape a bare `runner sweep` call needs (abort
236
+ # check, THEN the lease heartbeat, THEN reclaim - RunnerSweep's own fixed
237
+ # order). `step` never calls this method: it composes `abort_if_merging`
238
+ # and `reclaim` itself, interleaved around absorb.
239
+ def run_sweep(intent_dir, args)
240
+ context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
241
+ result = RunnerSweep.run(context)
242
+
243
+ unless result[:ok]
244
+ warn "runner: #{result[:error]}"
245
+ warn "runner: run `#{result[:recovery_command]}` before the next sweep can run" if result[:recovery_command]
246
+ exit 1
247
+ end
248
+
249
+ result[:reclaimed].each { |r| puts "reclaimed #{r[:node]} (holder=#{r[:holder]})" }
250
+ result[:extended].each { |e| puts "extended #{e[:node]} (head=#{e[:head]})" }
251
+ puts "nothing to sweep" if result[:reclaimed].empty? && result[:extended].empty?
252
+ exit 0
253
+ end
254
+
255
+ # run_answer (intent 340, G7, n6): closes a decision node or unparks a
256
+ # work node the runner parked at `needs_decision` (327 D22, C26), then
257
+ # prints what became ready so the owner knows the loop can continue.
258
+ def run_answer(intent_dir, args)
259
+ node = opt(args, "--node")
260
+ text = opt(args, "--answer")
261
+
262
+ unless node
263
+ warn "runner: answer requires --node ID"
264
+ exit 2
265
+ end
266
+
267
+ context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
268
+ result = RunnerAnswer.answer(context, node: node, text: text.to_s)
269
+
270
+ unless result[:ok]
271
+ warn "runner: answer refused (#{result[:reason]})"
272
+ exit 1
273
+ end
274
+
275
+ puts "answered #{node}"
276
+ puts "respun to #{result[:respun_to]}" if result[:respun_to]
277
+ puts "newly ready: #{result[:newly_ready].join(', ')}" if result[:newly_ready].any?
278
+ exit 0
279
+ end
280
+
281
+ # run_rewind (intent 340, G7, n6): confirm-gated reset of the intent
282
+ # branch to a node's own recorded commit, superseding every downstream
283
+ # node and respinning the rewound node itself (327 D15's rewind clause).
284
+ def run_rewind(intent_dir, args)
285
+ node = opt(args, "--node")
286
+ confirm = args.include?("--confirm")
287
+
288
+ unless node
289
+ warn "runner: rewind requires --node ID"
290
+ exit 2
291
+ end
292
+
293
+ context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
294
+ result = RunnerRewind.rewind(context, node: node, confirm: confirm)
295
+
296
+ unless result[:ok]
297
+ warn "runner: rewind refused (#{result[:reason]})"
298
+ exit 1
299
+ end
300
+
301
+ puts "reset #{node} to #{result[:reset_to]}"
302
+ puts "superseded: #{result[:superseded].join(', ')}" if result[:superseded].any?
303
+ puts "respun to #{result[:respun_to]}"
304
+ exit 0
305
+ end
306
+
307
+ # run_until_empty (intent 340b, G7c, n7): the Codex loop. Internal, like
308
+ # `rewind` (row 7.13) - `RunnerUntilEmpty.run` composes `step` and
309
+ # `node-run` itself, since Codex has no session on the other end to make
310
+ # the subagent calls `step` only ever prints a plan for. `--harness`
311
+ # threads into every turn the loop runs (row 7.5), never re-resolved per
312
+ # iteration from a literal. The lock check here is the same defence in
313
+ # depth `run_step_body` carries (B4): `RunnerUntilEmpty.step_once` refuses
314
+ # `lock_not_held` on its own first call regardless, so this exits before
315
+ # even constructing the loop's own state.
316
+ def run_until_empty(intent_dir, args)
317
+ harness_override = opt(args, "--harness")
318
+ context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
319
+
320
+ unless context.session
321
+ warn "runner: until-empty refused (lock_not_held)"
322
+ exit 1
323
+ end
324
+
325
+ result = RunnerUntilEmpty.run(context, harness: harness_override)
326
+
327
+ case result[:status]
328
+ when "complete", "stalled", "needs_decision", "iteration_cap"
329
+ exit 0
330
+ else
331
+ exit 1
332
+ end
333
+ end
334
+
335
+ # run_watch (intent 340a, G7b, n2): one tick over disk truth. Internal,
336
+ # like until-empty and rewind - the timer that carries it lives outside
337
+ # this process (a local `/loop` on Claude Code, a launchd job on Codex,
338
+ # n3/n4). `--dispatch` is unattended start (graph.md D7): refused here,
339
+ # before RunnerWatch.tick ever takes the tick lock or writes anything,
340
+ # when the resolved harness's `HarnessAdapter.unattended_start?` is
341
+ # false, so a refusal never leaves a `watch.record` line behind that
342
+ # looks like a completed tick.
343
+ # --install-timer (intent 340a, G7b, n3, graph.md D9): the Codex carrier.
344
+ # Writes the plist and prints the path plus the `launchctl load` command;
345
+ # never runs `launchctl` itself, so the owner stays in control of when the
346
+ # job actually loads. `--home` defaults to the OS home, as
347
+ # `scripts/meter-watch --install-timer` already does.
348
+ def run_watch(intent_dir, args)
349
+ dispatch = args.include?("--dispatch")
350
+ harness_override = opt(args, "--harness")
351
+ context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
352
+ agent_config = load_agent_config(context.plastic_home)
353
+ harness = HarnessAdapter.resolve_key(config: agent_config, override: harness_override)
354
+
355
+ if args.include?("--install-timer")
356
+ home = opt(args, "--home") || Dir.home
357
+ plist_path = RunnerWatch.install_timer(context, home: home, harness_key: harness)
358
+ puts "plist: #{plist_path}"
359
+ puts "launchctl load #{plist_path}"
360
+ exit 0
361
+ end
362
+
363
+ if dispatch && !HarnessAdapter.unattended_start?(harness)
364
+ warn HarnessAdapter::UNATTENDED_START_SENTENCE
365
+ exit 1
366
+ end
367
+
368
+ result = RunnerWatch.tick(context, dispatch: dispatch, harness: harness)
369
+
370
+ puts "class: #{result[:class]}"
371
+ result[:blockers].each { |b| puts "blocked: #{b}" }
372
+ puts "ready: #{result[:ready].empty? ? '(none)' : result[:ready].join(', ')}"
373
+ puts "dispatched: #{result[:dispatched].empty? ? '(none)' : result[:dispatched].join(', ')}"
374
+ exit 0
375
+ end
376
+
377
+ # run_step (intent 340, G7, n5): one turn of the loop. The real work lives
378
+ # in #run_step_body below; this wrapper (intent 340b, G7c, n1, rows
379
+ # 1.21-1.25 and 1.29-1.31) exists ONLY to capture every byte `step` prints
380
+ # - stdout and stderr both, refusals included - and persist it to
381
+ # `<intent_dir>/runner-step.last` before the process actually exits, so a
382
+ # PreCompact hand-off (n3) always has something to render, even after one
383
+ # of the three pre-plan refusals that used to warn to stderr alone.
384
+ #
385
+ # `run_step_body` still calls Kernel#exit(N) at every one of its own exit
386
+ # points, exactly as before this node; wrapping it in a rescue for
387
+ # SystemExit lets this method capture the intended status, persist the
388
+ # file, replay the captured output onto the REAL stdout/stderr (so a
389
+ # subprocess caller like Open3.capture3 sees byte-identical output to
390
+ # before this node), and only then perform the one real process exit.
391
+ def run_step(intent_dir, args)
392
+ out_buf = StringIO.new
393
+ err_buf = StringIO.new
394
+ real_stdout = $stdout
395
+ real_stderr = $stderr
396
+ exit_code = 0
397
+ begin
398
+ $stdout = out_buf
399
+ $stderr = err_buf
400
+ run_step_body(intent_dir, args)
401
+ rescue SystemExit => e
402
+ exit_code = e.status || 1
403
+ ensure
404
+ $stdout = real_stdout
405
+ $stderr = real_stderr
406
+ end
407
+
408
+ real_stdout.print(out_buf.string)
409
+ real_stderr.print(err_buf.string)
410
+ persist_runner_step_last(intent_dir, out_buf.string, err_buf.string)
411
+
412
+ exit(exit_code)
413
+ end
414
+
415
+ # Row 1.24: lives beside savepoint.md, never inside attempts/ (which the
416
+ # reaper treats as attempt evidence). Row 1.32: registers itself in the
417
+ # store's own top-level `.gitignore` (Worktree.ensure_gitignored's own
418
+ # idempotent, non-raising append) the same way `*.lock` already is, so a
419
+ # file rewritten on every step never dirties the store tree. Row 1.25:
420
+ # wrapped in its own rescue - an unwritable intent directory must never
421
+ # crash a step that has already written its transitions.
422
+ def persist_runner_step_last(intent_dir, stdout_text, stderr_text)
423
+ home_root = Arm.home_for(intent_dir)
424
+ Worktree.ensure_gitignored(File.join(home_root, ".plastic"), "runner-step.last")
425
+
426
+ path = File.join(intent_dir, "runner-step.last")
427
+ File.write(path, "#{stdout_text}#{stderr_text}")
428
+ rescue StandardError => e
429
+ warn "runner: could not persist runner-step.last: #{e.message}"
430
+ end
431
+
432
+ # Intent 340b, G7c, n1, row 1.1: the config `HarnessAdapter.resolve_key`
433
+ # reads `agent.type` from. `context.plastic_home` is derived from the
434
+ # intent_dir's OWN ancestry (Arm.home_for -> Worktree.home_from_store), so
435
+ # a sandboxed test fixture never reaches the real ~/.plastic/config.yml
436
+ # (Arm.home_for's own docstring: "a sandboxed store never resolves to the
437
+ # real Dir.home") - this stays hermetic for free, the same way every other
438
+ # `context.plastic_home` read in this file already does. A missing or
439
+ # unparseable config.yml reads as {}, never raises.
440
+ def load_agent_config(plastic_home)
441
+ path = File.join(plastic_home.to_s, "config.yml")
442
+ return {} unless File.exist?(path)
443
+
444
+ YAML.safe_load(File.read(path)) || {}
445
+ rescue StandardError
446
+ {}
447
+ end
448
+
449
+ # run_step_body (intent 340, G7, n5): one turn of the loop, in the fixed
450
+ # order 327 D14 and RunnerSweep's own docstring name - abort on a stuck
451
+ # merge, the delivery-lease heartbeat, absorb every `--return NODE=PATH`
452
+ # this call carries, THEN reclaim (so a node this very call just absorbed
453
+ # is never reclaimed out from under its own return, matrix row 2.19/2.20) -
454
+ # and only then dispatch whatever is left ready. Never spawns an agent (327
455
+ # D42): the session makes every subagent call from the plan this prints.
456
+ def run_step_body(intent_dir, args)
457
+ context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
458
+
459
+ # v1 minor 3/row 11.18: RunnerSweep.abort_if_merging already prints the
460
+ # whole sentence itself (its own error PLUS the recovery command) - this
461
+ # used to print both halves again, so the operator read the same merge
462
+ # warning twice for one refusal.
463
+ abort_result = RunnerSweep.abort_if_merging(context)
464
+ exit 1 unless abort_result[:ok]
465
+
466
+ Lock.heartbeat(intent_dir, session: context.session) unless context.session.to_s.strip.empty?
467
+
468
+ # B4: this is the runner's ACTUAL lock check now - the absorb loop below
469
+ # writes node transitions, and RunnerDispatch's own check (row 5.31/5.32)
470
+ # runs too late to stop that: a session holding no lock would otherwise
471
+ # have every `--return` it carries written anyway, attributed to
472
+ # whichever session's holder= happens to be on the node's `running`
473
+ # line. RunnerDispatch keeps its own check too, as defence in depth for
474
+ # a caller that reaches it some other way.
475
+ unless context.session
476
+ warn "runner: step refused (lock_not_held)"
477
+ warn RunnerDispatch.rearm_command(intent_dir)
478
+ exit 1
479
+ end
480
+
481
+ # M3/row 10.1: `--allow-core-drift` (D11) - the one escape hatch letting
482
+ # Plastic's own repository, which legitimately drifts from the installed
483
+ # manifest, dogfood its own runner. Threaded straight into the absorb,
484
+ # never defaulted true.
485
+ allow_core_drift = args.include?("--allow-core-drift")
486
+
487
+ # Intent 340b, G7c, n1, row 1.2/1.4: `--harness KEY` overrides config
488
+ # `agent.type` for this one call - how one machine drives both adapters
489
+ # without editing config. `nil` when absent, so HarnessAdapter falls
490
+ # through to config's own value untouched.
491
+ harness_override = opt(args, "--harness")
492
+
493
+ returns = {}
494
+ opt_all(args, "--return").each do |pair|
495
+ node, path = pair.to_s.split("=", 2)
496
+ if node.to_s.empty? || path.to_s.empty?
497
+ warn "runner: malformed --return #{pair.inspect}, expected NODE=PATH"
498
+ exit 2
499
+ end
500
+ returns[node] = path
501
+ end
502
+
503
+ returns.each do |node, path|
504
+ absorbed = RunnerAbsorb.absorb(context, node: node, return_path: path, allow_core_drift: allow_core_drift)
505
+ puts "absorbed #{node}: #{absorbed[:state]}"
506
+ # M4/row 10.3: a refused proposal is surfaced here, not left to a
507
+ # savepoint comment the operator never sees - the exact reason the
508
+ # proposer's next attempt otherwise proposes the same thing again.
509
+ proposal = absorbed[:proposal]
510
+ puts " proposal refused: #{Array(proposal[:errors]).join('; ')}" if proposal && !proposal[:ok]
511
+ end
512
+
513
+ swept = RunnerSweep.reclaim(context, skip: returns.keys)
514
+ swept[:reclaimed].each { |r| puts "reclaimed #{r[:node]} (holder=#{r[:holder]})" }
515
+ swept[:extended].each { |e| puts "extended #{e[:node]} (head=#{e[:head]})" }
516
+
517
+ # M5/row 10.4: NodeWorktree.reap runs once per step, right after the
518
+ # reclaim pass - D26's reaper, unreachable from anywhere until now.
519
+ reaped = NodeWorktree.reap(context)
520
+ reaped[:removed].each { |r| puts "reaped #{r[:node]} worktree" }
521
+
522
+ agent_config = load_agent_config(context.plastic_home)
523
+ result = RunnerDispatch.dispatch(context, config: agent_config, harness: harness_override)
524
+ unless result[:ok]
525
+ warn "runner: step refused (#{result[:reason]}): #{Array(result[:errors]).join('; ')}"
526
+ warn result[:rearm_command] if result[:rearm_command]
527
+ exit 1
528
+ end
529
+
530
+ case result[:status]
531
+ when "dispatched"
532
+ puts result[:plan]
533
+ when "queued"
534
+ puts "queued (concurrency ceiling reached)"
535
+ when "complete"
536
+ puts "complete"
537
+ when "needs_decision"
538
+ # printed below, alongside a dispatch plan when the same step also
539
+ # dispatched (M11/row 10.14) - `result[:stop]` is set whenever
540
+ # status is needs_decision.
541
+ else
542
+ puts "stalled"
543
+ result[:blockers].each { |b| puts "blocked: #{b}" }
544
+ end
545
+
546
+ # Intent 340b, G7c, n1, rows 1.6/1.13/1.14/1.27/1.28: the YAML plan keeps
547
+ # printing exactly as it did before this node (327's contract) - this is
548
+ # the rendered block UNDERNEATH it, for the harness `result[:harness]`
549
+ # resolved (config's `agent.type`, or this call's own `--harness`
550
+ # override), separated by an explicit YAML document-end marker so a
551
+ # reader splits on that marker rather than a heuristic. `render` returns
552
+ # nil for an empty dispatch list (row 1.14), so nothing extra ever
553
+ # prints when this step dispatched nothing.
554
+ block = HarnessAdapter.render(result[:dispatched], harness: result[:harness],
555
+ return_contract: RunnerDispatch::RETURN_CONTRACT)
556
+ if block
557
+ puts DOCUMENT_BOUNDARY
558
+ puts block
559
+ end
560
+
561
+ # M11/row 10.14: a `needs_decision` stop must print even when THIS SAME
562
+ # step also dispatched other nodes ahead of the decision node in ranked
563
+ # order - `result[:status]` reads "dispatched" in that case (dispatched
564
+ # takes priority in RunnerDispatch's own report), which used to swallow
565
+ # the stop and its `runner answer` command entirely.
566
+ if result[:stop]
567
+ stop = result[:stop]
568
+ puts "needs_decision: #{stop[:node]} - #{stop[:question]}"
569
+ puts stop[:answer_command]
570
+ end
571
+
572
+ result[:parked].each do |p|
573
+ puts "parked #{p[:node]} (#{p[:reason]}): #{p[:question]}"
574
+ puts p[:answer_command]
575
+ end
576
+
577
+ exit 0
578
+ end
579
+ end
580
+
581
+ Runner.main(ARGV) if $PROGRAM_NAME == __FILE__
@@ -2,15 +2,16 @@
2
2
  # encoding: UTF-8
3
3
  # frozen_string_literal: true
4
4
 
5
- # savepoint-note - the writer CLI for the two savepoint kinds intent 317 adds:
6
- # Review (one line per plan-review or post-execution-review verdict) and
7
- # Commit (one line per commit landing during Exec). D17: there is no automatic
8
- # seam for either (session-commit writes the day ledger, not the intent
9
- # ledger; the executor's own commits are plain git), so this is an explicit
10
- # thin wrapper on Savepoint.append_review_savepoint / append_commit_savepoint.
5
+ # savepoint-note - the writer CLI for three savepoint kinds: Review (one line per
6
+ # plan-review or post-execution-review verdict) and Commit (one line per commit landing
7
+ # during Exec), both from intent 317, D17; and Report (one line per report screen printed,
8
+ # intent 331f). None has an automatic seam (session-commit writes the day ledger, not the
9
+ # intent ledger; the executor's own commits are plain git; a printed screen is a skill's own
10
+ # reply, not a file write), so this is an explicit thin wrapper on
11
+ # Savepoint.append_review_savepoint / append_commit_savepoint / append_report_savepoint.
11
12
  #
12
13
  # Usage:
13
- # savepoint-note <intent_dir> --kind Review|Commit --text "<text>"
14
+ # savepoint-note <intent_dir> --kind Review|Commit|Report --text "<text>"
14
15
  #
15
16
  # --text is normalized (D21): runs of two or more spaces collapse to one, so a
16
17
  # free-text kind that happens to contain a double space cannot key the dedup
@@ -46,12 +47,12 @@ while (arg = args.shift)
46
47
  end
47
48
  end
48
49
 
49
- usage_abort("usage: savepoint-note <intent_dir> --kind Review|Commit --text \"<text>\"") unless positional.length == 1
50
+ usage_abort("usage: savepoint-note <intent_dir> --kind Review|Commit|Report --text \"<text>\"") unless positional.length == 1
50
51
 
51
52
  intent_dir = File.expand_path(positional.first)
52
53
  usage_abort("#{intent_dir} is not an intent directory") unless File.exist?(Savepoint.intent_file(intent_dir))
53
54
 
54
- KINDS = %w[Review Commit].freeze
55
+ KINDS = %w[Review Commit Report].freeze
55
56
  usage_abort("--kind must be one of #{KINDS.join(', ')}, got #{kind.inspect}") unless KINDS.include?(kind)
56
57
  usage_abort("--text is required") if text.nil? || text.empty?
57
58
  usage_abort("--text must not contain a newline") if text.include?("\n")
@@ -62,6 +63,7 @@ usage_abort("--text is empty after normalization") if normalized.empty?
62
63
  case kind
63
64
  when "Review" then Savepoint.append_review_savepoint(intent_dir, normalized)
64
65
  when "Commit" then Savepoint.append_commit_savepoint(intent_dir, normalized)
66
+ when "Report" then Savepoint.append_report_savepoint(intent_dir, normalized)
65
67
  end
66
68
 
67
69
  exit 0
@@ -18,6 +18,7 @@
18
18
  # 0 - always, except a usage error
19
19
  # 2 - usage error (missing --cwd or --summary); nothing is written
20
20
 
21
+ require_relative "lib/store_layout"
21
22
  require "fileutils"
22
23
  require_relative "lib/session_git"
23
24
  require_relative "lib/session_ledger"
@@ -78,7 +79,7 @@ def main(argv)
78
79
  usage_abort("--summary is required") if opts[:summary].nil?
79
80
 
80
81
  plastic_home = expand(opts[:plastic_home] || ENV.fetch("PLASTIC_HOME", "~/.plastic"))
81
- store = opts[:store] ? expand(opts[:store]) : File.join(plastic_home, "store")
82
+ store = opts[:store] ? expand(opts[:store]) : Plastic::StoreLayout.global_store(plastic_home)
82
83
  cwd = expand(opts[:cwd])
83
84
 
84
85
  day = opts[:day] || SessionLedger.day_id