@zalom/plastic 2.0.0-alpha.22 → 2.0.0-alpha.24

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 (103) hide show
  1. package/PLASTIC.md +6 -5
  2. package/agents/plastic-enforcer.md +6 -3
  3. package/agents/plastic-executor.md +4 -0
  4. package/agents/plastic-node-research.md +28 -0
  5. package/agents/plastic-node-verify.md +27 -0
  6. package/agents/plastic-node-work.md +32 -0
  7. package/bin/lib/context_budget.rb +1 -1
  8. package/hooks/hooks.json +12 -0
  9. package/hooks/statusline +28 -0
  10. package/hooks/stop +5 -0
  11. package/package.json +1 -1
  12. package/scripts/dashboard.rb +1 -1
  13. package/scripts/doctor.rb +80 -6
  14. package/scripts/end-intent +22 -19
  15. package/scripts/exec-worktree +5 -5
  16. package/scripts/graph-measure +249 -0
  17. package/scripts/hook-call-budget +6 -6
  18. package/scripts/hook-capture +16 -15
  19. package/scripts/hook-record +21 -14
  20. package/scripts/hook-savepoint +26 -3
  21. package/scripts/hook-session-start +49 -10
  22. package/scripts/hook-stop +57 -0
  23. package/scripts/lib/active_delivery.rb +105 -0
  24. package/scripts/lib/agent_models.rb +10 -1
  25. package/scripts/lib/arm.rb +41 -102
  26. package/scripts/lib/codex_adapter.rb +197 -0
  27. package/scripts/lib/{packet_wrapper.rb → data_boundary.rb} +7 -7
  28. package/scripts/lib/day_summary.rb +19 -11
  29. package/scripts/lib/doctor_core.rb +8 -3
  30. package/scripts/lib/doctor_session_ledger.rb +3 -52
  31. package/scripts/lib/engine_permissions.rb +88 -0
  32. package/scripts/lib/exec_worktree.rb +24 -21
  33. package/scripts/lib/graph_edges.rb +4 -4
  34. package/scripts/lib/graph_file.rb +4 -4
  35. package/scripts/lib/graph_measure.rb +645 -0
  36. package/scripts/lib/graph_measure_budget.rb +409 -0
  37. package/scripts/lib/graph_measure_cohorts.rb +487 -0
  38. package/scripts/lib/graph_measure_models.rb +411 -0
  39. package/scripts/lib/graph_measure_report.rb +532 -0
  40. package/scripts/lib/graph_tree.rb +2 -2
  41. package/scripts/lib/handoff.rb +40 -13
  42. package/scripts/lib/harness_adapter.rb +184 -0
  43. package/scripts/lib/hook_registry.rb +13 -1
  44. package/scripts/lib/hook_replay.rb +23 -5
  45. package/scripts/lib/index_entry.rb +53 -0
  46. package/scripts/lib/index_projection.rb +1 -1
  47. package/scripts/lib/insights.rb +1 -1
  48. package/scripts/lib/installer_core.rb +228 -9
  49. package/scripts/lib/intent_screen.rb +1 -1
  50. package/scripts/lib/intent_validator.rb +2 -2
  51. package/scripts/lib/lock.rb +8 -9
  52. package/scripts/lib/meter_watch.rb +15 -9
  53. package/scripts/lib/node_file.rb +3 -3
  54. package/scripts/lib/{node_packet.rb → node_input.rb} +49 -49
  55. package/scripts/lib/node_input_compatibility.rb +62 -0
  56. package/scripts/lib/node_ledger.rb +11 -2
  57. package/scripts/lib/node_progress.rb +153 -0
  58. package/scripts/lib/outcome_report.rb +2 -2
  59. package/scripts/lib/project_config.rb +45 -0
  60. package/scripts/lib/ready_set.rb +1 -1
  61. package/scripts/lib/report_screen.rb +10 -6
  62. package/scripts/lib/roadmap_graph.rb +1 -1
  63. package/scripts/lib/roadmap_queue.rb +1 -1
  64. package/scripts/lib/roadmap_render.rb +1 -1
  65. package/scripts/lib/roadmap_savepoint.rb +1 -1
  66. package/scripts/lib/runner_absorb.rb +36 -10
  67. package/scripts/lib/runner_dispatch.rb +66 -51
  68. package/scripts/lib/runner_sweep.rb +4 -4
  69. package/scripts/lib/runner_until_empty.rb +252 -0
  70. package/scripts/lib/runner_watch.rb +389 -0
  71. package/scripts/lib/savepoint.rb +9 -10
  72. package/scripts/lib/scaffold_intent.rb +6 -3
  73. package/scripts/lib/session_close.rb +30 -28
  74. package/scripts/lib/session_git.rb +22 -16
  75. package/scripts/lib/session_ledger.rb +44 -4
  76. package/scripts/lib/stop_gate.rb +95 -0
  77. package/scripts/lib/verify_intent.rb +2 -2
  78. package/scripts/lib/work_graph_validator.rb +6 -6
  79. package/scripts/lib/worktree.rb +24 -24
  80. package/scripts/lib/worktree_sweep.rb +3 -3
  81. package/scripts/new-intent +1 -1
  82. package/scripts/{node-packet → node-input} +15 -15
  83. package/scripts/node-run +224 -0
  84. package/scripts/plastic-lock +33 -32
  85. package/scripts/read-config +6 -0
  86. package/scripts/runner +203 -19
  87. package/scripts/verify-intent +1 -1
  88. package/skills/auto/SKILL.md +8 -8
  89. package/skills/auto/references/end-tail.md +9 -11
  90. package/skills/conventions/references/completion-and-done.md +9 -10
  91. package/skills/conventions/references/knowledge-graph.md +9 -0
  92. package/skills/conventions/references/locks-and-worktrees.md +10 -12
  93. package/skills/direct/SKILL.md +2 -2
  94. package/skills/doctor/SKILL.md +4 -4
  95. package/skills/intent-creating/evals/evals.json +1 -1
  96. package/skills/intent-executing/SKILL.md +7 -1
  97. package/skills/releasing/SKILL.md +2 -2
  98. package/skills/releasing/references/promotion-and-tagging.md +1 -1
  99. package/skills/releasing/references/release-lines.md +1 -1
  100. package/skills/tutorial/references/track-2-auto.md +1 -1
  101. package/templates/index.md +1 -1
  102. package/templates/project.yml +1 -1
  103. package/scripts/lib/bridge.rb +0 -116
@@ -12,6 +12,7 @@ require "time"
12
12
  require_relative "session_ledger"
13
13
  require_relative "handoff"
14
14
  require_relative "lock"
15
+ require_relative "active_delivery"
15
16
  require_relative "report_screen"
16
17
 
17
18
  module DaySummary
@@ -43,7 +44,7 @@ module DaySummary
43
44
  open: open_items(store, day),
44
45
  done: last_done(store, day),
45
46
  live: live_intents(home, now: now),
46
- others: active_sessions(store, session, now: now, ttl: heartbeat_ttl),
47
+ others: active_sessions(store, session, now: now, ttl: heartbeat_ttl, home: home),
47
48
  }
48
49
  hidden = Hash.new(0)
49
50
  cap!(lists, hidden, :open, OPEN_CAP, keep: :newest)
@@ -142,7 +143,7 @@ module DaySummary
142
143
  "(no savepoint yet)"
143
144
  end
144
145
 
145
- def active_sessions(store, session, now:, ttl:)
146
+ def active_sessions(store, session, now:, ttl:, home: nil)
146
147
  tmp_root = SessionLedger.tmp_root(store)
147
148
  return [] unless File.directory?(tmp_root)
148
149
 
@@ -155,7 +156,7 @@ module DaySummary
155
156
  age = heartbeat_age(dir, now)
156
157
  next if age.nil? || age > ttl
157
158
 
158
- "- #{sid} (#{(age / 60).floor}m ago, on #{pointer_of(dir)})"
159
+ "- #{sid} (#{(age / 60).floor}m ago, on #{delivering_label(store, home, sid, now)})"
159
160
  end
160
161
  rescue SystemCallError
161
162
  []
@@ -178,14 +179,21 @@ module DaySummary
178
179
  nil
179
180
  end
180
181
 
181
- def pointer_of(dir)
182
- path = File.join(dir, "current")
183
- return "?" unless File.file?(path)
184
-
185
- value = File.read(path).strip
186
- value.empty? ? "?" : value[0, 80]
187
- rescue SystemCallError
188
- "?"
182
+ # The delivering intent's id, or "day ledger" (344 n2, D4-D7): the
183
+ # basename before "--" of ActiveDelivery.resolve_by_short_session's
184
+ # match, else the session records into today's day ledger.
185
+ def delivering_label(store, home, sid, now)
186
+ return "day ledger" if home.to_s.empty?
187
+
188
+ dir = ActiveDelivery.resolve_by_short_session(
189
+ global_store: store,
190
+ project_roots: ActiveDelivery.project_roots(home),
191
+ short_session: sid,
192
+ now: now
193
+ )
194
+ dir ? File.basename(dir).split("--", 2).first : "day ledger"
195
+ rescue StandardError
196
+ "day ledger"
189
197
  end
190
198
 
191
199
  # --- rendering, pure -------------------------------------------------------------
@@ -27,11 +27,16 @@ class Doctor
27
27
  "hermes" => { name: "Hermes", dir: File.join(Dir.home, ".hermes") },
28
28
  }.freeze
29
29
 
30
- # The Claude events hooks_registered expects in settings.json: the six-event map of
30
+ # The Claude events hooks_registered expects in settings.json: the eight-event map of
31
31
  # cut-inventory 3b (intent 309 added SessionEnd, registered for close since intent 301;
32
- # intent 316a added MessageDisplay, registered for message-display, Claude only).
32
+ # intent 316a added MessageDisplay, registered for message-display, Claude only; intent
33
+ # 355 added PreToolUse, registered for call-budget, Claude only; intent 340b added Stop,
34
+ # registered for stop, Claude only). Stop's registration is static (D5/D7) even though
35
+ # its runtime arm defaults off (D9, runner.stop_hook), so an install missing it is
36
+ # exactly as broken as one missing PreCompact, and this list keeps hooks_registered and
37
+ # hooks_match_registry agreeing on that (row 4.32).
33
38
  CLAUDE_HOOK_EVENTS = %w[SessionStart PreToolUse PreCompact PostToolUse UserPromptSubmit SessionEnd
34
- MessageDisplay].freeze
39
+ MessageDisplay Stop].freeze
35
40
 
36
41
  # Launchers the installer places in the agent's hooks dir that are NOT hooks
37
42
  # (intent 204): plastic-statusline is the settings["statusLine"] command, wired
@@ -15,21 +15,12 @@ require_relative "session_ledger"
15
15
  # heartbeat is only refreshed on prompts and edits, so an old heartbeat is evidence, not
16
16
  # proof, that the session is gone. Fail-open: an unreadable directory is skipped, never
17
17
  # raised. Depends on the including class for `plastic_home` and `check`.
18
+ #
19
+ # Intent 344 (G11, D10): the third check this category used to carry,
20
+ # `no_pointer_session_tmp`, is retired along with the per-session state it read.
18
21
  module DoctorSessionLedger
19
22
  ORPHAN_TTL_SECONDS = 24 * 60 * 60
20
23
 
21
- # Row H (spec D9): a `.tmp/<sid>/` directory with no `current` pointer is a
22
- # DIFFERENT orphan class than ORPHAN_TTL_SECONDS's -- a session where
23
- # session start never ran at all (a `-p` print session, a resumed
24
- # background job, an unregistered SessionStart hook), not a session that
25
- # ran and then never closed. No choice of session id ever creates a
26
- # pointer for that class, so it would otherwise stay invisible for the
27
- # full 24 hours (or forever, once hook-capture/hook-record stop creating
28
- # it at all). Short relative to ORPHAN_TTL_SECONDS on purpose: a session
29
- # between its own start and its first pointer write is normal and must not
30
- # be flagged, but that window is seconds, not hours.
31
- NO_POINTER_TTL_SECONDS = 5 * 60
32
-
33
24
  # Seconds since the session's last heartbeat: the ISO-8601 content of `heartbeat`,
34
25
  # else that file's mtime, else the directory's mtime.
35
26
  def heartbeat_age(dir, now)
@@ -49,7 +40,6 @@ module DoctorSessionLedger
49
40
 
50
41
  store_dir = File.join(plastic_home, "store")
51
42
  orphans = orphaned_session_dirs(store_dir, now)
52
- no_pointer = no_pointer_session_dirs(store_dir, now)
53
43
  shape = day_ledger_shape_problems(store_dir)
54
44
 
55
45
  checks = []
@@ -67,20 +57,6 @@ module DoctorSessionLedger
67
57
  "session is gone: a live session rewrites its heartbeat on every " \
68
58
  "prompt and edit, so only a listed directory may be removed, by hand.")
69
59
  end
70
- checks << if no_pointer.empty?
71
- check(category: "session_ledger", name: "no_pointer_session_tmp", status: "pass",
72
- message: "No .tmp/<session>/ directory has gone without a `current` pointer for " \
73
- "longer than #{NO_POINTER_TTL_SECONDS} seconds")
74
- else
75
- check(category: "session_ledger", name: "no_pointer_session_tmp", status: "warn",
76
- message: "#{no_pointer.size} .tmp/<session>/ director#{no_pointer.size == 1 ? "y" : "ies"} " \
77
- "with no `current` pointer for longer than #{NO_POINTER_TTL_SECONDS} seconds " \
78
- "(session start never ran for this session)",
79
- details: no_pointer, fixable: true,
80
- fix_hint: "Remove each listed .tmp/<session>/ directory after confirming that " \
81
- "session never started: no `current` pointer means session start never " \
82
- "ran for it, so this is not a live session missing a checklist entry.")
83
- end
84
60
  checks << if shape.empty?
85
61
  check(category: "session_ledger", name: "day_ledger_shape", status: "pass",
86
62
  message: "Every .sessions/ entry is a YYYYMMDD day directory with its <day>.md file")
@@ -113,31 +89,6 @@ module DoctorSessionLedger
113
89
  [] # unreadable .tmp/: nothing to report, never a crash
114
90
  end
115
91
 
116
- # Row H (spec D9): `.tmp/<sid>/` directories with no `current` pointer, past
117
- # NO_POINTER_TTL_SECONDS. A different signal than #orphaned_session_dirs:
118
- # that one is age-only and blind to whether a pointer exists at all, so a
119
- # young no-pointer dir (a session between its start and its first pointer
120
- # write) must not appear here even though it may well appear there once it
121
- # ages past ORPHAN_TTL_SECONDS -- the two checks answer different questions
122
- # and a dir can legitimately show up in neither, either, or both.
123
- def no_pointer_session_dirs(store_dir, now)
124
- tmp_root = SessionLedger.tmp_root(store_dir)
125
- return [] unless File.directory?(tmp_root)
126
-
127
- Dir.children(tmp_root).sort.filter_map do |name|
128
- dir = File.join(tmp_root, name)
129
- next unless File.directory?(dir)
130
- next if File.exist?(File.join(dir, "current"))
131
-
132
- age = heartbeat_age(dir, now)
133
- next if age <= NO_POINTER_TTL_SECONDS
134
-
135
- "global: #{dir} (session #{name}, no `current` pointer, last heartbeat #{age.round}s ago)"
136
- end
137
- rescue SystemCallError
138
- [] # unreadable .tmp/: nothing to report, never a crash
139
- end
140
-
141
92
  def day_ledger_shape_problems(store_dir)
142
93
  root = SessionLedger.sessions_root(store_dir)
143
94
  return [] unless File.directory?(root)
@@ -0,0 +1,88 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # EnginePermissions: the engine deny rule (intent 340b, G7c, n3). A permissions.deny
5
+ # block merged into settings.json at install so the engine directories are not
6
+ # editable by a dispatched agent (C25's self-preservation half).
7
+ #
8
+ # Claude Code accepts a Write(...) path rule and never consults it: Edit(...) is the
9
+ # rule that covers Write, MultiEdit and NotebookEdit. Every entry below names Edit,
10
+ # never Write, and every entry ends in /** because a bare directory path matches the
11
+ # directory itself, not the files under it.
12
+ #
13
+ # This is a second ownership mechanism, deliberately not a reuse of HookRegistry's
14
+ # merge, which knows its own entries by a plastic- launcher basename inside a command
15
+ # string. A deny entry is a bare string with no marker in it, so ownership here is
16
+ # exact-string membership in ENTRIES. The consequence is deliberate: an entry the
17
+ # owner has edited no longer matches the frozen string, so it is the owner's from
18
+ # that point on - the merge appends Plastic's own alongside it, and removal leaves
19
+ # the edited one in place untouched.
20
+ #
21
+ # Pure functions only: no file I/O here. InstallerCore#merge_engine_permissions and
22
+ # #remove_engine_permissions own the read-modify-write against settings.json,
23
+ # mirroring the split HookRegistry keeps from merge_claude_hooks.
24
+ module EnginePermissions
25
+ module_function
26
+
27
+ ENTRIES = [
28
+ "Edit(~/.plastic/scripts/**)",
29
+ "Edit(~/.plastic/skills/**)",
30
+ "Edit(~/.plastic/hooks/**)",
31
+ "Edit(~/.plastic/templates/**)",
32
+ ].freeze
33
+
34
+ # Returns a new settings hash with ENTRIES merged into permissions.deny, appended
35
+ # (never duplicated) alongside whatever is already there. Survives a permissions
36
+ # or deny value that is not the shape expected (row 3.12): a non-Hash permissions
37
+ # value or a non-Array deny value is replaced rather than raised on. Never
38
+ # mutates the argument.
39
+ def merge_into(settings)
40
+ settings = settings.is_a?(Hash) ? settings.dup : {}
41
+
42
+ permissions = settings["permissions"]
43
+ permissions = permissions.is_a?(Hash) ? permissions.dup : {}
44
+
45
+ deny = permissions["deny"]
46
+ deny = deny.is_a?(Array) ? deny.dup : []
47
+
48
+ ENTRIES.each { |entry| deny << entry unless deny.include?(entry) }
49
+
50
+ permissions["deny"] = deny
51
+ settings["permissions"] = permissions
52
+ settings
53
+ end
54
+
55
+ # Returns a new settings hash with exactly the ENTRIES strings removed from
56
+ # permissions.deny, leaving every other entry (the owner's own deny rules, and
57
+ # any Plastic entry the owner has since edited) untouched. Prunes the deny array
58
+ # once it is empty, and the whole permissions block once nothing else remains
59
+ # under it (row 3.10). A settings/permissions/deny shape that is not what is
60
+ # expected is left alone rather than raised on (row 3.12's uninstall side).
61
+ def remove_from(settings)
62
+ return settings unless settings.is_a?(Hash)
63
+
64
+ permissions = settings["permissions"]
65
+ return settings unless permissions.is_a?(Hash)
66
+
67
+ deny = permissions["deny"]
68
+ return settings unless deny.is_a?(Array)
69
+
70
+ settings = settings.dup
71
+ permissions = permissions.dup
72
+ deny = deny.reject { |entry| ENTRIES.include?(entry) }
73
+
74
+ if deny.empty?
75
+ permissions.delete("deny")
76
+ else
77
+ permissions["deny"] = deny
78
+ end
79
+
80
+ if permissions.empty?
81
+ settings.delete("permissions")
82
+ else
83
+ settings["permissions"] = permissions
84
+ end
85
+
86
+ settings
87
+ end
88
+ end
@@ -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)
@@ -4,11 +4,11 @@
4
4
  # GraphEdges (intent 334, n1): the shared `needs` syntax and cycle-path check
5
5
  # (327 D2r-D4r, D12r). Parses a "## Graph" section's edge lines into a node
6
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, fold A5): the same list-item
7
+ # own ## Graph section both use unchanged (C1, review A5): the same list-item
8
8
  # grammar, the same root keyword, the same cycle walk. Deliberately loose
9
9
  # about what an id looks like - a roadmap id is numeric ("334", "340a"), a
10
10
  # node id carries a kind prefix ("n1") - the kind-prefix rule belongs to
11
- # NodeFile and WorkGraphValidator, never here (D12r, fold A6).
11
+ # NodeFile and WorkGraphValidator, never here (D12r, review A6).
12
12
  #
13
13
  # Grammar (D2r/D3r): a list item shaped "- <id> needs <target> [<target>
14
14
  # ...]", ending at end of line. The single literal target "nothing" declares
@@ -18,7 +18,7 @@
18
18
  # all, "nothing" alongside other targets, or a target token carrying
19
19
  # sentence punctuation (a period, a colon, ...) rather than the loose
20
20
  # id-token charset, is an error naming the line, never a silently
21
- # mis-parsed edge or an invented node (fold A7).
21
+ # mis-parsed edge or an invented node (review A7).
22
22
  #
23
23
  # Pure and side-effect-free; never raises across the boundary, matching the
24
24
  # Result-hash convention every library in scripts/lib/ follows.
@@ -30,7 +30,7 @@ module GraphEdges
30
30
  ROOT_TARGET = "nothing"
31
31
 
32
32
  # {nodes:, edges:, errors:} - nodes is the declared ids together with every
33
- # id any edge targets (fold A5), in first-seen order; edges maps a declared
33
+ # id any edge targets (review A5), in first-seen order; edges maps a declared
34
34
  # id to its target ids (empty for a root); errors names each malformed
35
35
  # edge-shaped line. Prose lines contribute nothing and raise nothing.
36
36
  def parse(section_text)
@@ -7,9 +7,9 @@ require_relative "atomic_write"
7
7
  # GraphFile (intent 334, n2): the four graph.md sections (## Goal,
8
8
  # ## Decisions, ## Graph, ## Status), the verify:-none directive, and the two
9
9
  # writers - write_status and append_decision. Both writers refuse a cyclic
10
- # graph and both go through AtomicWrite (fold D19r). Fence-aware everywhere a
10
+ # graph and both go through AtomicWrite (review D19r). Fence-aware everywhere a
11
11
  # heading is located, so a fenced example carrying a fake "## " line never
12
- # splits or ends a real section (fold A8's sibling concern, applied to
12
+ # splits or ends a real section (review A8's sibling concern, applied to
13
13
  # section boundaries rather than edge lines).
14
14
  module GraphFile
15
15
  module_function
@@ -19,8 +19,8 @@ module GraphFile
19
19
 
20
20
  # {ok:, goal:, decisions:, graph:, status:, verify:, errors:}. `graph` is
21
21
  # GraphEdges.parse's own Result hash, over the Graph section text with any
22
- # verify:-none directive line stripped first (fold A8). A missing ## Graph
23
- # section, or one that declares no nodes, is an error naming which (fold
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
24
  # A10).
25
25
  def parse(path)
26
26
  return failure(["graph file not found: #{path}"]) unless File.exist?(path)