@zalom/plastic 2.0.0-alpha.9 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/PLASTIC.md +13 -136
  2. package/README.md +357 -133
  3. package/agents/plastic-enforcer.md +20 -15
  4. package/agents/plastic-executor.md +15 -4
  5. package/agents/plastic-node-research.md +30 -0
  6. package/agents/plastic-node-verify.md +28 -0
  7. package/agents/plastic-node-work.md +33 -0
  8. package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +7 -9
  9. package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
  10. package/assets/plastic-logo.svg +1 -0
  11. package/bin/crap +4 -0
  12. package/bin/lib/context_budget.rb +43 -6
  13. package/bin/lib/skill_census.rb +839 -0
  14. package/bin/plastic +6 -0
  15. package/bin/plastic-skill-census +114 -0
  16. package/bin/test +24 -4
  17. package/bin/verify-change +345 -0
  18. package/config_asks.yml +4 -4
  19. package/deprecations.yml +1 -1
  20. package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
  21. package/{skills/auto/references → docs/help}/agent-architecture.md +16 -14
  22. package/{skills/conventions/references → docs/help}/completion-and-done.md +15 -16
  23. package/docs/help/human-report-contract.md +152 -0
  24. package/{skills/conventions/references → docs/help}/knowledge-graph.md +9 -0
  25. package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +11 -13
  26. package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +1 -1
  27. package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
  28. package/{skills/tutorial/references → docs/help}/track-1-guided.md +28 -47
  29. package/{skills/tutorial/references → docs/help}/track-2-auto.md +8 -8
  30. package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
  31. package/hooks/call-budget +4 -0
  32. package/hooks/hooks.json +24 -0
  33. package/hooks/message-display +55 -2
  34. package/hooks/session-start +5 -1
  35. package/hooks/statusline +32 -27
  36. package/hooks/stop +5 -0
  37. package/package.json +5 -3
  38. package/scripts/append-ledger +2 -1
  39. package/scripts/dashboard.rb +267 -16
  40. package/scripts/day-summary +2 -1
  41. package/scripts/doctor.rb +680 -39
  42. package/scripts/end-intent +153 -30
  43. package/scripts/exec-worktree +5 -5
  44. package/scripts/file-session-intent +2 -1
  45. package/scripts/graph-measure +249 -0
  46. package/scripts/hook-call-budget +222 -0
  47. package/scripts/hook-capture +24 -123
  48. package/scripts/hook-close +2 -1
  49. package/scripts/hook-message-display +7 -0
  50. package/scripts/hook-record +24 -16
  51. package/scripts/hook-savepoint +27 -3
  52. package/scripts/hook-session-start +359 -321
  53. package/scripts/hook-stop +58 -0
  54. package/scripts/index-projection +74 -0
  55. package/scripts/insight-append +17 -4
  56. package/scripts/install.rb +9 -7
  57. package/scripts/lib/action_graph_shim.rb +279 -0
  58. package/scripts/lib/active_delivery.rb +105 -0
  59. package/scripts/lib/agent_models.rb +63 -25
  60. package/scripts/lib/arm.rb +48 -83
  61. package/scripts/lib/atomic_write.rb +31 -0
  62. package/scripts/lib/backup.rb +65 -0
  63. package/scripts/lib/cli/command.rb +92 -0
  64. package/scripts/lib/cli/commands/auto.rb +18 -0
  65. package/scripts/lib/cli/commands/auto_brief.rb +44 -0
  66. package/scripts/lib/cli/commands/auto_lock.rb +99 -0
  67. package/scripts/lib/cli/commands/auto_report.rb +50 -0
  68. package/scripts/lib/cli/commands/auto_take.rb +31 -0
  69. package/scripts/lib/cli/commands/backup.rb +43 -0
  70. package/scripts/lib/cli/commands/checkout.rb +25 -0
  71. package/scripts/lib/cli/commands/continue.rb +66 -0
  72. package/scripts/lib/cli/commands/doctor.rb +81 -0
  73. package/scripts/lib/cli/commands/feedback.rb +40 -0
  74. package/scripts/lib/cli/commands/help.rb +69 -0
  75. package/scripts/lib/cli/commands/hook.rb +32 -0
  76. package/scripts/lib/cli/commands/index.rb +23 -0
  77. package/scripts/lib/cli/commands/install.rb +21 -0
  78. package/scripts/lib/cli/commands/installer_verb.rb +37 -0
  79. package/scripts/lib/cli/commands/intent.rb +19 -0
  80. package/scripts/lib/cli/commands/intent_answer.rb +37 -0
  81. package/scripts/lib/cli/commands/intent_command.rb +53 -0
  82. package/scripts/lib/cli/commands/intent_end.rb +62 -0
  83. package/scripts/lib/cli/commands/intent_new.rb +62 -0
  84. package/scripts/lib/cli/commands/intent_note.rb +43 -0
  85. package/scripts/lib/cli/commands/intent_rule.rb +36 -0
  86. package/scripts/lib/cli/commands/intent_show.rb +32 -0
  87. package/scripts/lib/cli/commands/intent_spec.rb +44 -0
  88. package/scripts/lib/cli/commands/intent_step.rb +71 -0
  89. package/scripts/lib/cli/commands/intent_verify.rb +26 -0
  90. package/scripts/lib/cli/commands/migrate.rb +16 -0
  91. package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
  92. package/scripts/lib/cli/commands/next.rb +54 -0
  93. package/scripts/lib/cli/commands/project.rb +19 -0
  94. package/scripts/lib/cli/commands/project_links.rb +42 -0
  95. package/scripts/lib/cli/commands/project_list.rb +20 -0
  96. package/scripts/lib/cli/commands/project_new.rb +72 -0
  97. package/scripts/lib/cli/commands/query.rb +31 -0
  98. package/scripts/lib/cli/commands/render.rb +33 -0
  99. package/scripts/lib/cli/commands/roadmap.rb +19 -0
  100. package/scripts/lib/cli/commands/roadmap_check.rb +54 -0
  101. package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
  102. package/scripts/lib/cli/commands/roadmap_migrate.rb +53 -0
  103. package/scripts/lib/cli/commands/roadmap_next.rb +70 -0
  104. package/scripts/lib/cli/commands/roadmap_show.rb +45 -0
  105. package/scripts/lib/cli/commands/rollback.rb +20 -0
  106. package/scripts/lib/cli/commands/search.rb +60 -0
  107. package/scripts/lib/cli/commands/session.rb +18 -0
  108. package/scripts/lib/cli/commands/session_commit.rb +42 -0
  109. package/scripts/lib/cli/commands/session_handoff.rb +34 -0
  110. package/scripts/lib/cli/commands/session_summary.rb +35 -0
  111. package/scripts/lib/cli/commands/status.rb +68 -0
  112. package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
  113. package/scripts/lib/cli/commands/sync.rb +46 -0
  114. package/scripts/lib/cli/commands/uninstall.rb +20 -0
  115. package/scripts/lib/cli/commands/update.rb +20 -0
  116. package/scripts/lib/cli/commands/version.rb +53 -0
  117. package/scripts/lib/cli/frontier.rb +86 -0
  118. package/scripts/lib/cli/intent_progress.rb +36 -0
  119. package/scripts/lib/cli/legacy.rb +80 -0
  120. package/scripts/lib/cli/output.rb +137 -0
  121. package/scripts/lib/cli/scope.rb +134 -0
  122. package/scripts/lib/cli/table.rb +65 -0
  123. package/scripts/lib/cli.rb +94 -0
  124. package/scripts/lib/codex_adapter.rb +198 -0
  125. package/scripts/lib/compact_instructions.rb +13 -5
  126. package/scripts/lib/core_integrity.rb +71 -0
  127. package/scripts/lib/dashboard_screen.rb +40 -0
  128. package/scripts/lib/data_boundary.rb +132 -0
  129. package/scripts/lib/day_summary.rb +23 -14
  130. package/scripts/lib/doctor_core.rb +114 -38
  131. package/scripts/lib/doctor_session_ledger.rb +5 -53
  132. package/scripts/lib/engine_permissions.rb +88 -0
  133. package/scripts/lib/exec_worktree.rb +24 -21
  134. package/scripts/lib/feedback_report.rb +1 -1
  135. package/scripts/lib/graph_edges.rb +137 -0
  136. package/scripts/lib/graph_file.rb +246 -0
  137. package/scripts/lib/graph_measure.rb +645 -0
  138. package/scripts/lib/graph_measure_budget.rb +409 -0
  139. package/scripts/lib/graph_measure_cohorts.rb +487 -0
  140. package/scripts/lib/graph_measure_models.rb +413 -0
  141. package/scripts/lib/graph_measure_report.rb +532 -0
  142. package/scripts/lib/graph_tree.rb +98 -0
  143. package/scripts/lib/guarded_append.rb +155 -0
  144. package/scripts/lib/handoff.rb +40 -13
  145. package/scripts/lib/harness_adapter.rb +184 -0
  146. package/scripts/lib/hook_registry.rb +27 -3
  147. package/scripts/lib/hook_replay.rb +229 -0
  148. package/scripts/lib/index_entry.rb +62 -0
  149. package/scripts/lib/index_projection.rb +201 -0
  150. package/scripts/lib/insights.rb +1 -1
  151. package/scripts/lib/installer_core.rb +531 -71
  152. package/scripts/lib/intent_screen.rb +4 -4
  153. package/scripts/lib/intent_screen_ansi.rb +73 -12
  154. package/scripts/lib/intent_validator.rb +2 -2
  155. package/scripts/lib/lock.rb +10 -11
  156. package/scripts/lib/message_display.rb +350 -54
  157. package/scripts/lib/meter_watch.rb +185 -0
  158. package/scripts/lib/node_file.rb +234 -0
  159. package/scripts/lib/node_ids.rb +99 -0
  160. package/scripts/lib/node_input.rb +913 -0
  161. package/scripts/lib/node_input_compatibility.rb +62 -0
  162. package/scripts/lib/node_ledger.rb +386 -0
  163. package/scripts/lib/node_progress.rb +153 -0
  164. package/scripts/lib/node_return.rb +204 -0
  165. package/scripts/lib/node_worktree.rb +337 -0
  166. package/scripts/lib/outcome_report.rb +440 -0
  167. package/scripts/lib/preflight.rb +4 -6
  168. package/scripts/lib/project_config.rb +46 -0
  169. package/scripts/lib/project_validator.rb +3 -2
  170. package/scripts/lib/qmd_sync.rb +8 -7
  171. package/scripts/lib/ready_set.rb +462 -0
  172. package/scripts/lib/reference_archive.rb +45 -0
  173. package/scripts/lib/release_guard.rb +18 -0
  174. package/scripts/lib/report_screen.rb +1371 -47
  175. package/scripts/lib/rlm/corpus.rb +13 -0
  176. package/scripts/lib/rlm/probe.rb +29 -0
  177. package/scripts/lib/rlm/query.rb +22 -0
  178. package/scripts/lib/roadmap_graph.rb +210 -0
  179. package/scripts/lib/roadmap_migration.rb +95 -0
  180. package/scripts/lib/roadmap_queue.rb +158 -8
  181. package/scripts/lib/roadmap_render.rb +150 -0
  182. package/scripts/lib/roadmap_savepoint.rb +64 -14
  183. package/scripts/lib/runner_absorb.rb +703 -0
  184. package/scripts/lib/runner_answer.rb +206 -0
  185. package/scripts/lib/runner_core.rb +194 -0
  186. package/scripts/lib/runner_dispatch.rb +525 -0
  187. package/scripts/lib/runner_policy.rb +191 -0
  188. package/scripts/lib/runner_proposals.rb +275 -0
  189. package/scripts/lib/runner_rewind.rb +201 -0
  190. package/scripts/lib/runner_sweep.rb +231 -0
  191. package/scripts/lib/runner_until_empty.rb +252 -0
  192. package/scripts/lib/runner_watch.rb +389 -0
  193. package/scripts/lib/savepoint.rb +141 -19
  194. package/scripts/lib/scaffold_intent.rb +6 -3
  195. package/scripts/lib/screen_paint.rb +365 -28
  196. package/scripts/lib/screens/dashboard.rb +20 -0
  197. package/scripts/lib/screens/plan.rb +18 -0
  198. package/scripts/lib/screens/roadmap.rb +15 -0
  199. package/scripts/lib/search_index.rb +55 -0
  200. package/scripts/lib/session_close.rb +30 -28
  201. package/scripts/lib/session_git.rb +25 -18
  202. package/scripts/lib/session_ledger.rb +48 -4
  203. package/scripts/lib/session_usage.rb +190 -0
  204. package/scripts/lib/sqlite.rb +22 -0
  205. package/scripts/lib/stop_gate.rb +95 -0
  206. package/scripts/lib/store_discovery.rb +7 -6
  207. package/scripts/lib/store_layout.rb +54 -0
  208. package/scripts/lib/store_provisioning.rb +2 -1
  209. package/scripts/lib/store_sync.rb +85 -0
  210. package/scripts/lib/stores_move.rb +93 -0
  211. package/scripts/lib/verify_intent.rb +36 -8
  212. package/scripts/lib/version_number.rb +48 -0
  213. package/scripts/lib/work_graph.rb +59 -0
  214. package/scripts/lib/work_graph_validator.rb +201 -0
  215. package/scripts/lib/worktree.rb +27 -32
  216. package/scripts/lib/worktree_sweep.rb +6 -5
  217. package/scripts/link-suggest +2 -1
  218. package/scripts/meter-watch +57 -0
  219. package/scripts/migrate-to-global +1 -1
  220. package/scripts/new-intent +4 -13
  221. package/scripts/node-input +92 -0
  222. package/scripts/node-run +225 -0
  223. package/scripts/node-transition +291 -0
  224. package/scripts/outcome-report +74 -0
  225. package/scripts/plastic-lock +42 -33
  226. package/scripts/promote-session-item +3 -2
  227. package/scripts/read-config +53 -9
  228. package/scripts/ready-set +126 -0
  229. package/scripts/release-check +123 -0
  230. package/scripts/report-screen +177 -16
  231. package/scripts/roadmap-graph +119 -0
  232. package/scripts/roadmap-savepoint +7 -0
  233. package/scripts/runner +581 -0
  234. package/scripts/savepoint-note +11 -9
  235. package/scripts/session-commit +2 -1
  236. package/scripts/session-usage +56 -0
  237. package/scripts/skill-lint +115 -6
  238. package/scripts/spawn-preamble +2 -2
  239. package/scripts/update.rb +25 -4
  240. package/scripts/validate-work-graph +39 -0
  241. package/scripts/verify-intent +3 -2
  242. package/scripts/write-handoff +2 -1
  243. package/templates/agents.md +7 -7
  244. package/templates/config.yml +16 -9
  245. package/templates/dashboard-screen.md +22 -0
  246. package/templates/display-fixture.md +21 -0
  247. package/templates/graph.md +16 -0
  248. package/templates/index.md +1 -1
  249. package/templates/intent-screen.md +1 -1
  250. package/templates/node-decision.md +11 -0
  251. package/templates/node-research.md +13 -0
  252. package/templates/node-verify.md +13 -0
  253. package/templates/node-work.md +22 -0
  254. package/templates/outcome.md +8 -3
  255. package/templates/project.yml +1 -1
  256. package/templates/render.css +10 -0
  257. package/templates/report-plan.md +15 -0
  258. package/templates/report-roadmap-delivered.md +10 -0
  259. package/templates/report-roadmap-plan.md +9 -0
  260. package/templates/report-roadmap-state.md +9 -0
  261. package/templates/report-state.md +1 -1
  262. package/templates/roadmap.md +13 -0
  263. package/bin/plastic.js +0 -70
  264. package/scripts/lib/bridge.rb +0 -116
  265. package/skills/agent-advisor/SKILL.md +0 -92
  266. package/skills/auto/SKILL.md +0 -296
  267. package/skills/auto/evals/evals.json +0 -255
  268. package/skills/auto/references/end-tail.md +0 -66
  269. package/skills/auto/references/human-report-contract.md +0 -78
  270. package/skills/conventions/SKILL.md +0 -29
  271. package/skills/dashboard/SKILL.md +0 -169
  272. package/skills/dashboard/evals/evals.json +0 -38
  273. package/skills/dashboard/references/classification.md +0 -22
  274. package/skills/dashboard/templates/dashboard-global.md +0 -20
  275. package/skills/dashboard/templates/dashboard-project.md +0 -19
  276. package/skills/direct/SKILL.md +0 -66
  277. package/skills/direct/references/request-signals.md +0 -59
  278. package/skills/doctor/SKILL.md +0 -299
  279. package/skills/doctor/report.md +0 -102
  280. package/skills/feedback/SKILL.md +0 -98
  281. package/skills/feedback/references/transport-and-privacy.md +0 -65
  282. package/skills/feedback/report.md +0 -36
  283. package/skills/install/SKILL.md +0 -217
  284. package/skills/intent-continuing/SKILL.md +0 -154
  285. package/skills/intent-continuing/references/board-fill.md +0 -43
  286. package/skills/intent-continuing/references/boarding-matrix.md +0 -34
  287. package/skills/intent-continuing/references/context-management.md +0 -28
  288. package/skills/intent-continuing/references/liveness-ranking.md +0 -57
  289. package/skills/intent-creating/SKILL.md +0 -164
  290. package/skills/intent-creating/evals/evals.json +0 -72
  291. package/skills/intent-creating/references/lifecycle.md +0 -81
  292. package/skills/intent-creating/references/wikilinks.md +0 -8
  293. package/skills/intent-ending/SKILL.md +0 -176
  294. package/skills/intent-ending/evals/evals.json +0 -74
  295. package/skills/intent-executing/SKILL.md +0 -170
  296. package/skills/intent-executing/evals/evals.json +0 -66
  297. package/skills/intent-executing/implementer-prompt.md +0 -42
  298. package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
  299. package/skills/intent-speccing/SKILL.md +0 -130
  300. package/skills/intent-speccing/evals/evals.json +0 -126
  301. package/skills/intent-speccing/references/design-principles.md +0 -44
  302. package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
  303. package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
  304. package/skills/project-creating/SKILL.md +0 -162
  305. package/skills/project-creating/references/hubs-projects.md +0 -55
  306. package/skills/project-creating/references/project-scaffolding.md +0 -97
  307. package/skills/releasing/SKILL.md +0 -337
  308. package/skills/releasing/references/deprecations.md +0 -60
  309. package/skills/releasing/references/promotion-and-tagging.md +0 -66
  310. package/skills/releasing/references/release-lines.md +0 -105
  311. package/skills/roadmap/SKILL.md +0 -64
  312. package/skills/roadmap/references/file-format.md +0 -124
  313. package/skills/roadmap/references/operations.md +0 -112
  314. package/skills/rollback/SKILL.md +0 -91
  315. package/skills/tutorial/SKILL.md +0 -65
  316. package/skills/tutorial/evals/evals.json +0 -186
  317. package/skills/uninstall/SKILL.md +0 -75
  318. package/skills/update/SKILL.md +0 -126
  319. /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
  320. /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
  321. /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
  322. /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
@@ -2,7 +2,6 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "open3"
5
- require_relative "bridge"
6
5
  require_relative "arm"
7
6
  require_relative "lock"
8
7
  require_relative "worktree"
@@ -29,8 +28,8 @@ require_relative "scaffold_intent"
29
28
  # fail-open, see the merge-verification note below), `finisher:` (defaults to
30
29
  # `Worktree.method(:finish)`), `status_checker:` (defaults to a `git status --porcelain`
31
30
  # lambda). Every seam exists so
32
- # tests run with no real git, no real worktree removal, and no real bridge file outside a
33
- # tmpdir.
31
+ # tests run with no real git, no real worktree removal, and no real delivery-lock file
32
+ # outside a tmpdir.
34
33
  #
35
34
  # MERGE VERIFICATION (delivered disposition only). `Worktree.merge_branch` is deliberately
36
35
  # fail-open: on a conflict it runs `git merge --abort`, warns, and returns false, and
@@ -39,7 +38,7 @@ require_relative "scaffold_intent"
39
38
  # module never trusts the discarded return value; it asks git directly, AFTER finish, whether
40
39
  # the intent's code branch is now an ancestor of the repo's current HEAD
41
40
  # (`merged_into_current_branch?` below). `repo` and `branch` are captured BEFORE the finisher
42
- # runs, because finish deletes the bridge's `worktree` block.
41
+ # runs, because finish deletes the delivery's `worktree` block.
43
42
  #
44
43
  # DOES NOT RUN A TEST SUITE, in any form. `verify-intent` owns test execution; one concern
45
44
  # per script (spec D8).
@@ -60,6 +59,10 @@ module ExecWorktree
60
59
  # [stdout, stderr, status] triple Open3.capture3 returns (status responds to
61
60
  # success?). A raised error (git missing, corrupt repo) is caught and reported as a
62
61
  # nil status, so the caller can fail closed the same way a failed exit status would.
62
+ def blank?(value)
63
+ value.nil? || value.to_s.strip.empty?
64
+ end
65
+
63
66
  def default_status_checker
64
67
  lambda do |worktree_code|
65
68
  Open3.capture3("git", "-C", worktree_code, "status", "--porcelain")
@@ -85,7 +88,7 @@ module ExecWorktree
85
88
  # `<repo>/.claude/worktrees/<name>` (scripts/lib/worktree.rb:75), so walking up three
86
89
  # directories recovers `<repo>` deterministically, with no I/O and no git.
87
90
  def repo_from_worktree_code(worktree_code)
88
- return nil if Bridge.blank?(worktree_code)
91
+ return nil if blank?(worktree_code)
89
92
  File.dirname(File.dirname(File.dirname(worktree_code.to_s)))
90
93
  end
91
94
 
@@ -94,7 +97,7 @@ module ExecWorktree
94
97
  # i.e. the merge actually landed. Fails CLOSED (not merged) on a blank repo/branch or a
95
98
  # raising runner, so an inconclusive check is never reported as a success.
96
99
  def merged_into_current_branch?(runner, repo:, branch:)
97
- return false if Bridge.blank?(repo) || Bridge.blank?(branch)
100
+ return false if blank?(repo) || blank?(branch)
98
101
  runner.run("-C", repo, "merge-base", "--is-ancestor", branch, "HEAD").success?
99
102
  rescue StandardError
100
103
  false
@@ -106,12 +109,12 @@ module ExecWorktree
106
109
  # The lock-owner fallback is correct here: this is a teardown step on an intent this
107
110
  # session has been delivering.
108
111
  def resolve_session(explicit, env_session, intent_dir)
109
- return explicit.to_s.strip unless Bridge.blank?(explicit)
110
- return env_session.to_s.strip unless Bridge.blank?(env_session)
112
+ return explicit.to_s.strip unless blank?(explicit)
113
+ return env_session.to_s.strip unless blank?(env_session)
111
114
  lock = Lock.read(intent_dir)
112
115
  return nil unless lock
113
116
  owner = lock["owner_session"].to_s
114
- Bridge.blank?(owner) ? nil : owner
117
+ blank?(owner) ? nil : owner
115
118
  end
116
119
 
117
120
  # --- result builders (the exit_code/stdout/stderr shape the thin CLIs share) --------
@@ -181,7 +184,7 @@ module ExecWorktree
181
184
  merge_line =
182
185
  if disposition != "delivered"
183
186
  "not attempted (abandoned)"
184
- elsif Bridge.blank?(target_branch)
187
+ elsif blank?(target_branch)
185
188
  "merged (integration target branch could not be resolved)"
186
189
  else
187
190
  "merged into #{target_branch}"
@@ -202,10 +205,10 @@ module ExecWorktree
202
205
  runner: Worktree::ShellRunner.new,
203
206
  finisher: Worktree.method(:finish),
204
207
  status_checker: default_status_checker)
205
- return usage_result("--store is required") if Bridge.blank?(store)
206
- return usage_result("--id is required") if Bridge.blank?(id)
207
- return usage_result("--home is required") if Bridge.blank?(home)
208
- return usage_result("--disposition is required") if Bridge.blank?(disposition)
208
+ return usage_result("--store is required") if blank?(store)
209
+ return usage_result("--id is required") if blank?(id)
210
+ return usage_result("--home is required") if blank?(home)
211
+ return usage_result("--disposition is required") if blank?(disposition)
209
212
  unless DISPOSITIONS.include?(disposition)
210
213
  return usage_result("--disposition must be one of #{DISPOSITIONS.join('|')} (got #{disposition.inspect})")
211
214
  end
@@ -220,16 +223,16 @@ module ExecWorktree
220
223
 
221
224
  # Step 0: session resolution + pre-flight refusal (D3-mirroring; see resolve_session).
222
225
  key_session = resolve_session(session, env_session, intent_dir)
223
- if Bridge.blank?(key_session) && File.exist?(Lock.path(intent_dir))
226
+ if blank?(key_session) && File.exist?(Lock.path(intent_dir))
224
227
  return deny_result(EXIT_UNRESOLVED, no_session_message(intent_dir))
225
228
  end
226
229
 
227
- bridge_data = Arm.bridge_hash(intent_dir: intent_dir, home: normalized_home)
228
- worktree_code = bridge_data.dig("worktree", "code")
229
- return ok_result(nothing_provisioned_message(intent_dir)) if Bridge.blank?(worktree_code)
230
+ delivery = Arm.delivery(intent_dir: intent_dir, home: normalized_home)
231
+ worktree_code = delivery.dig("worktree", "code")
232
+ return ok_result(nothing_provisioned_message(intent_dir)) if blank?(worktree_code)
230
233
 
231
- branch = bridge_data.dig("worktree", "code_branch")
232
- # Captured now, before `finisher.call` below (Worktree.finish) deletes the bridge's
234
+ branch = delivery.dig("worktree", "code_branch")
235
+ # Captured now, before `finisher.call` below (Worktree.finish) deletes the delivery's
233
236
  # `worktree` block, so the post-finish merge verification (BLOCKING 1/2) still has
234
237
  # both `repo` and `branch` to work with.
235
238
  repo = repo_from_worktree_code(worktree_code)
@@ -254,7 +257,7 @@ module ExecWorktree
254
257
  # disposition only, whether the worktree directory still exists on disk (teardown
255
258
  # incomplete); second, for a delivered disposition only, whether the branch actually
256
259
  # merged (BLOCKING 1/2, see merged_into_current_branch? above).
257
- finisher.call(bridge_data, home: normalized_home, runner: runner,
260
+ finisher.call(delivery, home: normalized_home, runner: runner,
258
261
  merge: disposition == "delivered")
259
262
 
260
263
  removed = !Dir.exist?(worktree_code)
@@ -18,7 +18,7 @@ require "fileutils"
18
18
  # Trust model: this class never sends anything anywhere. `compose` returns a
19
19
  # Result carrying a local file path and a browser URL; only the human, in
20
20
  # their own authenticated browser, submits it. There is no send method here
21
- # and there must never be one (see skills/feedback/references/transport-and-privacy.md).
21
+ # and there must never be one (see SECURITY.md).
22
22
  class FeedbackReport
23
23
  GITHUB_REPO = "zalom/plastic"
24
24
  CAP_BYTES = 7500
@@ -0,0 +1,137 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # GraphEdges (intent 334, n1): the shared `needs` syntax and cycle-path check
5
+ # (327 D2r-D4r, D12r). Parses a "## Graph" section's edge lines into a node
6
+ # set and an edge map, the one parser an intent's graph.md and a roadmap's
7
+ # own ## Graph section both use unchanged (C1, review A5): the same list-item
8
+ # grammar, the same root keyword, the same cycle walk. Deliberately loose
9
+ # about what an id looks like - a roadmap id is numeric ("334", "340a"), a
10
+ # node id carries a kind prefix ("n1") - the kind-prefix rule belongs to
11
+ # NodeFile and WorkGraphValidator, never here (D12r, review A6).
12
+ #
13
+ # Grammar (D2r/D3r): a list item shaped "- <id> needs <target> [<target>
14
+ # ...]", ending at end of line. The single literal target "nothing" declares
15
+ # a root and must be the ONLY target on the line. Any line that does not
16
+ # start with "- <token> needs" is prose and is skipped in silence. A line
17
+ # that DOES look like an edge is held to the grammar strictly: no target at
18
+ # all, "nothing" alongside other targets, or a target token carrying
19
+ # sentence punctuation (a period, a colon, ...) rather than the loose
20
+ # id-token charset, is an error naming the line, never a silently
21
+ # mis-parsed edge or an invented node (review A7).
22
+ #
23
+ # Pure and side-effect-free; never raises across the boundary, matching the
24
+ # Result-hash convention every library in scripts/lib/ follows.
25
+ module GraphEdges
26
+ module_function
27
+
28
+ EDGE_LEAD_RE = /\A-\s*(\S+)\s+needs\b(.*)\z/.freeze
29
+ TOKEN_RE = /\A[A-Za-z0-9][A-Za-z0-9_-]*\z/.freeze
30
+ ROOT_TARGET = "nothing"
31
+
32
+ # {nodes:, edges:, errors:} - nodes is the declared ids together with every
33
+ # id any edge targets (review A5), in first-seen order; edges maps a declared
34
+ # id to its target ids (empty for a root); errors names each malformed
35
+ # edge-shaped line. Prose lines contribute nothing and raise nothing.
36
+ def parse(section_text)
37
+ declared_order = []
38
+ edges = {}
39
+ targeted = []
40
+ errors = []
41
+
42
+ section_text.to_s.each_line do |raw_line|
43
+ line = raw_line.chomp("\n").chomp("\r")
44
+ stripped = line.strip
45
+ next if stripped.empty?
46
+
47
+ m = stripped.match(EDGE_LEAD_RE)
48
+ next unless m
49
+
50
+ id = m[1]
51
+ raw_targets = m[2].to_s.strip.split(/\s+/)
52
+
53
+ if raw_targets.empty?
54
+ errors << "malformed edge line, no target: #{stripped.inspect}"
55
+ next
56
+ end
57
+
58
+ if raw_targets.include?(ROOT_TARGET) && raw_targets.length > 1
59
+ errors << "root target #{ROOT_TARGET.inspect} mixed with a real target, root must be the only target: #{stripped.inspect}"
60
+ next
61
+ end
62
+
63
+ bad_token = raw_targets.find { |t| t != ROOT_TARGET && !t.match?(TOKEN_RE) }
64
+ if bad_token
65
+ errors << "prose tail after targets: #{stripped.inspect}"
66
+ next
67
+ end
68
+
69
+ if edges.key?(id)
70
+ errors << "duplicate node declaration for #{id.inspect}: #{stripped.inspect}"
71
+ next
72
+ end
73
+
74
+ targets = raw_targets == [ROOT_TARGET] ? [] : raw_targets
75
+ declared_order << id
76
+ edges[id] = targets
77
+ targeted.concat(targets)
78
+ end
79
+
80
+ nodes = declared_order.dup
81
+ targeted.each { |t| nodes << t unless nodes.include?(t) }
82
+
83
+ { nodes: nodes, edges: edges, errors: errors }
84
+ end
85
+
86
+ # The cycle as a path with the entry id repeated (D4r), or nil when the
87
+ # graph is acyclic. A node reachable by two distinct paths (a diamond) is
88
+ # never mistaken for a cycle: once a node's whole subtree is walked clean
89
+ # it is marked visited and never re-examined.
90
+ def cycle(edges)
91
+ visiting = {}
92
+ visited = {}
93
+ path = []
94
+
95
+ edges.each_key do |node|
96
+ next if visited[node]
97
+ found = walk(node, edges, visiting, visited, path)
98
+ return found if found
99
+ end
100
+ nil
101
+ end
102
+
103
+ def walk(node, edges, visiting, visited, path)
104
+ if visiting[node]
105
+ idx = path.index(node)
106
+ return path[idx..] + [node]
107
+ end
108
+ return nil if visited[node]
109
+
110
+ visiting[node] = true
111
+ path.push(node)
112
+ (edges[node] || []).each do |target|
113
+ found = walk(target, edges, visiting, visited, path)
114
+ return found if found
115
+ end
116
+ path.pop
117
+ visiting.delete(node)
118
+ visited[node] = true
119
+ nil
120
+ end
121
+
122
+ # The declared work ids whose `needs` reach a verify node without passing
123
+ # through another verify node (343 D7), in declaration order. `kinds` maps
124
+ # an id to its node kind, so this module stays blind to where a kind lives.
125
+ def review_fixes(edges, kinds)
126
+ edges.keys.select { |id| kinds[id] == "work" && reaches_verify?(id, edges, kinds, {}) }
127
+ end
128
+
129
+ def reaches_verify?(node, edges, kinds, seen)
130
+ (edges[node] || []).any? do |target|
131
+ next false if seen[target]
132
+
133
+ seen[target] = true
134
+ kinds[target] == "verify" || reaches_verify?(target, edges, kinds, seen)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,246 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "graph_edges"
5
+ require_relative "atomic_write"
6
+
7
+ # GraphFile (intent 334, n2): the four graph.md sections (## Goal,
8
+ # ## Decisions, ## Graph, ## Status), the verify:-none directive, and the two
9
+ # writers - write_status and append_decision. Both writers refuse a cyclic
10
+ # graph and both go through AtomicWrite (review D19r). Fence-aware everywhere a
11
+ # heading is located, so a fenced example carrying a fake "## " line never
12
+ # splits or ends a real section (review A8's sibling concern, applied to
13
+ # section boundaries rather than edge lines).
14
+ module GraphFile
15
+ module_function
16
+
17
+ FENCE_LINE_RE = /\A\s{0,3}(`{3,}|~{3,})/.freeze
18
+ DIRECTIVE_RE = /\A-\s*verify:\s*none(?:\s+reason=(.*))?\z/i.freeze
19
+
20
+ # {ok:, goal:, decisions:, graph:, status:, verify:, errors:}. `graph` is
21
+ # GraphEdges.parse's own Result hash, over the Graph section text with any
22
+ # verify:-none directive line stripped first (review A8). A missing ## Graph
23
+ # section, or one that declares no nodes, is an error naming which (review
24
+ # A10).
25
+ def parse(path)
26
+ return failure(["graph file not found: #{path}"]) unless File.exist?(path)
27
+
28
+ content = File.read(path)
29
+ goal = section_body(content, "## Goal")
30
+ decisions = section_body(content, "## Decisions")
31
+ graph_section = section_body(content, "## Graph")
32
+ status = section_body(content, "## Status")
33
+
34
+ errors = []
35
+ if graph_section.nil?
36
+ errors << "missing ## Graph section"
37
+ return { ok: false, goal: goal, decisions: decisions, graph: nil, status: status, verify: nil, errors: errors }
38
+ end
39
+
40
+ directive, directive_errors, remaining = extract_verify_directive(graph_section)
41
+ errors.concat(directive_errors)
42
+
43
+ parsed_graph = GraphEdges.parse(strip_fenced_blocks(remaining))
44
+ errors.concat(parsed_graph[:errors])
45
+ errors << "## Graph declares no nodes" if parsed_graph[:nodes].empty?
46
+
47
+ { ok: errors.empty?, goal: goal, decisions: decisions, graph: parsed_graph, status: status, verify: directive, errors: errors }
48
+ end
49
+
50
+ def failure(errors)
51
+ { ok: false, goal: nil, decisions: nil, graph: nil, status: nil, verify: nil, errors: errors }
52
+ end
53
+
54
+ # Rows shaped {node:, state:, detail:} parsed back from the ## Status
55
+ # table (D8r's round-trip guarantee).
56
+ def status_rows(path)
57
+ parsed = parse(path)
58
+ rows_from_table(parsed[:status])
59
+ end
60
+
61
+ # Replace the whole ## Status section with rows, creating the section if
62
+ # absent. Refuses a cyclic graph (D8r); goes through AtomicWrite (D19r).
63
+ def write_status(path, rows, renamer: File.method(:rename))
64
+ guard = refuse_if_cyclic(path)
65
+ return guard if guard
66
+
67
+ content = File.read(path)
68
+ new_content = replace_or_append_section(content, "## Status", render_status_table(rows))
69
+ AtomicWrite.write(path, new_content, renamer: renamer)
70
+ { ok: true, errors: [] }
71
+ end
72
+
73
+ # Append one answered decision as a list item to ## Decisions (C26),
74
+ # leaving every other section byte-identical. Refuses a cyclic graph.
75
+ def append_decision(path, text, renamer: File.method(:rename))
76
+ guard = refuse_if_cyclic(path)
77
+ return guard if guard
78
+
79
+ content = File.read(path)
80
+ bounds = section_bounds(content, "## Decisions")
81
+ return { ok: false, errors: ["missing ## Decisions section"] } unless bounds
82
+
83
+ start_idx, end_idx = bounds
84
+ heading_line_end = line_end(content, start_idx)
85
+ body = content[heading_line_end...end_idx]
86
+ trimmed = body.rstrip
87
+ trailing = body[trimmed.length..]
88
+ new_body = trimmed.empty? ? "- #{text}#{trailing}" : "#{trimmed}\n- #{text}#{trailing}"
89
+ new_content = content[0...heading_line_end] + new_body + content[end_idx..]
90
+ AtomicWrite.write(path, new_content, renamer: renamer)
91
+ { ok: true, errors: [] }
92
+ end
93
+
94
+ def refuse_if_cyclic(path)
95
+ parsed = parse(path)
96
+ return { ok: false, errors: parsed[:errors] } if parsed[:graph].nil?
97
+
98
+ cyc = GraphEdges.cycle(parsed[:graph][:edges])
99
+ return { ok: false, errors: ["cyclic graph, refusing to write: #{cyc.join(' > ')}"] } if cyc
100
+
101
+ nil
102
+ end
103
+
104
+ # --- verify: none directive --------------------------------------------------
105
+
106
+ def extract_verify_directive(graph_section)
107
+ return [nil, [], graph_section] if graph_section.nil?
108
+
109
+ reason = nil
110
+ present = false
111
+ errors = []
112
+ remaining_lines = []
113
+
114
+ graph_section.each_line do |line|
115
+ stripped = line.strip
116
+ m = stripped.match(DIRECTIVE_RE)
117
+ if m
118
+ present = true
119
+ captured = m[1].to_s.strip
120
+ if captured.empty?
121
+ errors << "verify: none requires reason=<text>: #{stripped.inspect}"
122
+ else
123
+ reason = captured
124
+ end
125
+ else
126
+ remaining_lines << line
127
+ end
128
+ end
129
+
130
+ directive = present ? { reason: reason } : nil
131
+ [directive, errors, remaining_lines.join]
132
+ end
133
+
134
+ # D18: drop every fenced line from `text`, so a fenced example edge inside a
135
+ # hand-written graph.md or roadmap ## Graph section is never handed to
136
+ # GraphEdges as real edge text. Non-fenced lines pass through unchanged.
137
+ def strip_fenced_blocks(text)
138
+ lines = []
139
+ each_fence_line(text) { |line, fenced| lines << line unless fenced }
140
+ lines.join
141
+ end
142
+
143
+ # --- fence-aware section location ---------------------------------------------
144
+
145
+ def each_fence_line(text)
146
+ return enum_for(:each_fence_line, text) unless block_given?
147
+
148
+ marker = nil
149
+ text.to_s.each_line do |line|
150
+ if marker
151
+ yield line, true
152
+ m = line.match(FENCE_LINE_RE)
153
+ next unless m && m[1][0] == marker[0] && m[1].length >= marker[1]
154
+ next unless line.sub(FENCE_LINE_RE, "").strip.empty?
155
+
156
+ marker = nil
157
+ else
158
+ m = line.match(FENCE_LINE_RE)
159
+ if m
160
+ marker = [m[1][0], m[1].length]
161
+ yield line, true
162
+ else
163
+ yield line, false
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ # [start_of_heading_line, start_of_next_top_level_heading_or_EOF] byte
170
+ # offsets for the FIRST line, outside any fence, whose stripped text
171
+ # exactly equals heading_text. nil when no such line exists.
172
+ def section_bounds(content, heading_text)
173
+ offset = 0
174
+ heading_start = nil
175
+
176
+ each_fence_line(content) do |line, fenced|
177
+ if !fenced && heading_start.nil? && line.strip == heading_text
178
+ heading_start = offset
179
+ elsif !fenced && heading_start && offset > heading_start && line.match?(/\A##[^#]/)
180
+ return [heading_start, offset]
181
+ end
182
+ offset += line.length
183
+ end
184
+
185
+ heading_start ? [heading_start, offset] : nil
186
+ end
187
+
188
+ def section_body(content, heading_text)
189
+ bounds = section_bounds(content, heading_text)
190
+ return nil unless bounds
191
+
192
+ start_idx, end_idx = bounds
193
+ content[(line_end(content, start_idx))...end_idx].to_s
194
+ end
195
+
196
+ def line_end(content, start_idx)
197
+ idx = content.index("\n", start_idx)
198
+ idx ? idx + 1 : content.length
199
+ end
200
+
201
+ def replace_or_append_section(content, heading_text, body_text)
202
+ rendered_body = body_text.to_s
203
+ rendered_body += "\n" unless rendered_body.end_with?("\n")
204
+ section = "#{heading_text}\n#{rendered_body}"
205
+
206
+ bounds = section_bounds(content, heading_text)
207
+ if bounds
208
+ start_idx, end_idx = bounds
209
+ tail = content[end_idx..].to_s
210
+ section += "\n" unless tail.empty?
211
+ content[0...start_idx] + section + tail
212
+ else
213
+ head = content.dup
214
+ head += "\n" unless head.end_with?("\n")
215
+ head += "\n" unless head.end_with?("\n\n")
216
+ head + section
217
+ end
218
+ end
219
+
220
+ # --- table rendering and reading -----------------------------------------------
221
+
222
+ def render_status_table(rows)
223
+ lines = ["| Node | State | Detail |", "| --- | --- | --- |"]
224
+ rows.each { |r| lines << "| #{r[:node]} | #{r[:state]} | #{r[:detail]} |" }
225
+ "#{lines.join("\n")}\n"
226
+ end
227
+
228
+ def rows_from_table(text)
229
+ return [] if text.nil?
230
+
231
+ lines = []
232
+ each_fence_line(text) do |line, fenced|
233
+ next if fenced
234
+
235
+ stripped = line.strip
236
+ lines << stripped if stripped.start_with?("|")
237
+ end
238
+ sep_idx = lines.index { |l| l.match?(/\A\|[\s:|-]+\|?\z/) }
239
+ return [] unless sep_idx
240
+
241
+ lines[(sep_idx + 1)..].map do |l|
242
+ cells = l.split("|", -1).map(&:strip)[1..-2].to_a
243
+ { node: cells[0], state: cells[1], detail: cells[2] }
244
+ end
245
+ end
246
+ end