@zalom/plastic 2.0.0-alpha.23 → 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 (63) hide show
  1. package/PLASTIC.md +6 -5
  2. package/agents/plastic-node-research.md +4 -4
  3. package/agents/plastic-node-verify.md +4 -4
  4. package/agents/plastic-node-work.md +4 -4
  5. package/package.json +1 -1
  6. package/scripts/dashboard.rb +1 -1
  7. package/scripts/end-intent +19 -16
  8. package/scripts/exec-worktree +5 -5
  9. package/scripts/hook-call-budget +6 -6
  10. package/scripts/hook-capture +15 -15
  11. package/scripts/hook-record +21 -14
  12. package/scripts/hook-savepoint +2 -1
  13. package/scripts/hook-session-start +9 -10
  14. package/scripts/lib/active_delivery.rb +44 -0
  15. package/scripts/lib/arm.rb +41 -102
  16. package/scripts/lib/codex_adapter.rb +2 -2
  17. package/scripts/lib/{packet_wrapper.rb → data_boundary.rb} +7 -7
  18. package/scripts/lib/day_summary.rb +19 -11
  19. package/scripts/lib/doctor_session_ledger.rb +3 -52
  20. package/scripts/lib/exec_worktree.rb +24 -21
  21. package/scripts/lib/graph_measure_budget.rb +29 -28
  22. package/scripts/lib/handoff.rb +4 -8
  23. package/scripts/lib/harness_adapter.rb +6 -6
  24. package/scripts/lib/index_entry.rb +53 -0
  25. package/scripts/lib/insights.rb +1 -1
  26. package/scripts/lib/installer_core.rb +120 -7
  27. package/scripts/lib/lock.rb +8 -9
  28. package/scripts/lib/{node_packet.rb → node_input.rb} +49 -49
  29. package/scripts/lib/node_input_compatibility.rb +62 -0
  30. package/scripts/lib/node_ledger.rb +4 -2
  31. package/scripts/lib/outcome_report.rb +1 -1
  32. package/scripts/lib/project_config.rb +45 -0
  33. package/scripts/lib/ready_set.rb +1 -1
  34. package/scripts/lib/roadmap_savepoint.rb +1 -1
  35. package/scripts/lib/runner_absorb.rb +5 -5
  36. package/scripts/lib/runner_dispatch.rb +44 -44
  37. package/scripts/lib/runner_sweep.rb +4 -4
  38. package/scripts/lib/runner_until_empty.rb +1 -1
  39. package/scripts/lib/savepoint.rb +6 -7
  40. package/scripts/lib/scaffold_intent.rb +6 -3
  41. package/scripts/lib/session_close.rb +30 -28
  42. package/scripts/lib/session_git.rb +20 -14
  43. package/scripts/lib/session_ledger.rb +44 -4
  44. package/scripts/lib/worktree.rb +24 -24
  45. package/scripts/lib/worktree_sweep.rb +3 -3
  46. package/scripts/{node-packet → node-input} +15 -15
  47. package/scripts/node-run +19 -19
  48. package/scripts/plastic-lock +33 -32
  49. package/scripts/runner +1 -1
  50. package/skills/auto/SKILL.md +7 -7
  51. package/skills/auto/references/end-tail.md +9 -11
  52. package/skills/conventions/references/completion-and-done.md +9 -10
  53. package/skills/conventions/references/knowledge-graph.md +1 -1
  54. package/skills/conventions/references/locks-and-worktrees.md +10 -12
  55. package/skills/direct/SKILL.md +2 -2
  56. package/skills/doctor/SKILL.md +1 -1
  57. package/skills/intent-executing/SKILL.md +1 -1
  58. package/skills/releasing/SKILL.md +2 -2
  59. package/skills/releasing/references/promotion-and-tagging.md +1 -1
  60. package/skills/releasing/references/release-lines.md +1 -1
  61. package/templates/index.md +1 -1
  62. package/templates/project.yml +1 -1
  63. package/scripts/lib/bridge.rb +0 -116
@@ -108,16 +108,16 @@ module Worktree
108
108
 
109
109
  # --- provisioning ----------------------------------------------------------
110
110
 
111
- # Resolve the slug from the bridge's intent.store, create code + store
111
+ # Resolve the slug from the delivery's intent.store, create code + store
112
112
  # worktrees (idempotent: reuse an existing worktree path, do not error), write
113
- # the `worktree` block plus `provisioned: true` onto bridge_data, return it.
113
+ # the `worktree` block plus `provisioned: true` onto delivery, return it.
114
114
  #
115
115
  # Fails open with a stderr log when the repo is non-git or unresolvable:
116
116
  # sets `provisioned: false` and leaves `code: null`. All git ops use
117
117
  # `git -C <resolved path>` -- never cwd (decision D6).
118
- def provision(bridge_data, home: Dir.home, runner: ShellRunner.new)
119
- return bridge_data unless bridge_data.is_a?(Hash)
120
- intent = bridge_data["intent"] || {}
118
+ def provision(delivery, home: Dir.home, runner: ShellRunner.new)
119
+ return delivery unless delivery.is_a?(Hash)
120
+ intent = delivery["intent"] || {}
121
121
  intent_id = intent["id"].to_s
122
122
  store = intent["store"].to_s
123
123
  intent_slug = slug_from_dir(intent["dir"]) || slug_from_dir(store)
@@ -173,15 +173,15 @@ module Worktree
173
173
  # replaced by the fresh unprovisioned block; the guard must not fail harder
174
174
  # than the bug it guards.
175
175
  if code_ok
176
- bridge_data["worktree"] = block
176
+ delivery["worktree"] = block
177
177
  else
178
- existing = bridge_data["worktree"]
178
+ existing = delivery["worktree"]
179
179
  keep = existing.is_a?(Hash) && !blank?(existing["code"]) &&
180
180
  Dir.exist?(existing["code"].to_s)
181
- bridge_data["worktree"] = block unless keep
181
+ delivery["worktree"] = block unless keep
182
182
  end
183
183
 
184
- bridge_data
184
+ delivery
185
185
  end
186
186
 
187
187
  # Remove the worktree (then `git worktree prune`), clear the worktree block.
@@ -189,21 +189,21 @@ module Worktree
189
189
  # policy on top via `finish`; this is the plain remove. Pass `remove: false` to
190
190
  # clear the block WITHOUT touching git (so `finish` can merge first, then call
191
191
  # release to drop the worktree once the code branch is integrated).
192
- def release(bridge_data, home: Dir.home, runner: ShellRunner.new, remove: true)
193
- return bridge_data unless bridge_data.is_a?(Hash)
194
- block = bridge_data["worktree"]
195
- return bridge_data unless block.is_a?(Hash)
192
+ def release(delivery, home: Dir.home, runner: ShellRunner.new, remove: true)
193
+ return delivery unless delivery.is_a?(Hash)
194
+ block = delivery["worktree"]
195
+ return delivery unless block.is_a?(Hash)
196
196
 
197
197
  if remove
198
- slug = slug_for_store(bridge_data.dig("intent", "store").to_s, home: home)
198
+ slug = slug_for_store(delivery.dig("intent", "store").to_s, home: home)
199
199
  repo = repo_for(slug, home: home)
200
200
 
201
201
  remove_worktree(runner, repo: repo, worktree: block["code"]) if repo && block["code"]
202
202
  prune(runner, repo: repo) if repo
203
203
  end
204
204
 
205
- bridge_data.delete("worktree")
206
- bridge_data
205
+ delivery.delete("worktree")
206
+ delivery
207
207
  end
208
208
 
209
209
  # --- cleanup policy (merge-vs-remove) -------------------------------------
@@ -222,19 +222,19 @@ module Worktree
222
222
  # Fail-open and idempotent throughout: a missing block, missing branch, or any
223
223
  # git failure never raises and never blocks teardown. All git ops use
224
224
  # `git -C <path>`, never cwd (decision D6). No-op when nothing was provisioned.
225
- def finish(bridge_data, home: Dir.home, runner: ShellRunner.new, merge: false)
226
- return bridge_data unless bridge_data.is_a?(Hash)
227
- block = bridge_data["worktree"]
228
- return bridge_data unless block.is_a?(Hash)
225
+ def finish(delivery, home: Dir.home, runner: ShellRunner.new, merge: false)
226
+ return delivery unless delivery.is_a?(Hash)
227
+ block = delivery["worktree"]
228
+ return delivery unless block.is_a?(Hash)
229
229
 
230
230
  if merge
231
- slug = slug_for_store(bridge_data.dig("intent", "store").to_s, home: home)
231
+ slug = slug_for_store(delivery.dig("intent", "store").to_s, home: home)
232
232
  repo = repo_for(slug, home: home)
233
233
  branch = block["code_branch"]
234
234
  merge_branch(runner, repo: repo, branch: branch) if repo && !blank?(branch)
235
235
  end
236
236
 
237
- release(bridge_data, home: home, runner: runner, remove: true)
237
+ release(delivery, home: home, runner: runner, remove: true)
238
238
  end
239
239
 
240
240
  # Merge `branch` into the repo's default branch from the main checkout. The
@@ -298,8 +298,8 @@ module Worktree
298
298
  # --- lock ------------------------------------------------------------------
299
299
 
300
300
  # True iff ANOTHER session's delivery.lock is FRESH on this intent's dir
301
- # (intent 108, D2): the durable lock file decides; /tmp bridges are not
302
- # consulted and no pid is probed. current_session being the owner or a
301
+ # (intent 108, D2): the durable lock file decides alone, and no pid is
302
+ # probed. current_session being the owner or a
303
303
  # delegate does not count as "other". A stale lock does not hold (explicit
304
304
  # takeover reclaims it).
305
305
  def lock_held_by_other?(intent_id:, store:, current_session:, home: Dir.home,
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative "worktree"
5
- require_relative "bridge"
5
+ require_relative "index_entry"
6
6
 
7
7
  # WorktreeSweep -- the one-time orphan sweep for store worktrees retired by
8
8
  # intent 178 (D2). Pure candidate classification plus a thin apply step, both
@@ -62,7 +62,7 @@ module WorktreeSweep
62
62
 
63
63
  # The INDEX.md section heading (e.g. "Active", "Completed") that lists this
64
64
  # intent dir, or nil if no INDEX.md entry links to it. Reuses
65
- # Bridge.index_entry_match so this never drifts from the shared parser.
65
+ # IndexEntry.match so this never drifts from the shared parser.
66
66
  def index_status(intent_dir, name)
67
67
  store = File.dirname(intent_dir)
68
68
  index = File.join(File.dirname(store), "INDEX.md")
@@ -75,7 +75,7 @@ module WorktreeSweep
75
75
  section = stripped.sub(/\A##\s*/, "").strip
76
76
  next
77
77
  end
78
- m = Bridge.index_entry_match(stripped)
78
+ m = IndexEntry.match(stripped)
79
79
  next unless m
80
80
  return section if m[3].to_s.include?(name)
81
81
  end
@@ -2,30 +2,30 @@
2
2
  # encoding: UTF-8
3
3
  # frozen_string_literal: true
4
4
 
5
- require_relative "lib/node_packet"
5
+ require_relative "lib/node_input"
6
6
  require_relative "lib/savepoint"
7
7
 
8
- # node-packet - the CLI over NodePacket (intent 338, G5). Builds one node's
9
- # whole input from disk and writes it to packets/<node>--a<N>.packet (or
8
+ # node-input - the CLI over NodeInput (intent 338, G5). Builds one node's
9
+ # whole input from disk and writes it to attempts/<node>--a<N>.input (or
10
10
  # --out), printing a parsable summary and the exact node-transition running
11
11
  # command intent 340's runner should record.
12
12
  #
13
13
  # Usage:
14
- # node-packet <intent_dir> --node <id> [--budget N] [--hop-tokens N]
14
+ # node-input <intent_dir> --node <id> [--budget N] [--hop-tokens N]
15
15
  # [--holder H] [--expires E] [--model M] [--attempt N]
16
16
  # [--out PATH] [--force]
17
17
  #
18
18
  # Exit codes (spec D17, shared with node-transition's family):
19
- # 0 - the packet was written (or the identical bytes already existed)
19
+ # 0 - the node input was written (or the identical bytes already existed)
20
20
  # 2 - usage: not an intent directory, missing --node, or an unknown node
21
21
  # 3 - an unreadable, cyclic or unparsable graph, node file, or record
22
22
  # 4 - overflow past the third cut; nothing was written
23
23
  # 5 - an existing attempt file whose bytes differ; nothing was written
24
- module NodePacketCLI
24
+ module NodeInputCLI
25
25
  module_function
26
26
 
27
27
  def usage
28
- warn "Usage: node-packet <intent_dir> --node <id> [--budget N] [--hop-tokens N] " \
28
+ warn "Usage: node-input <intent_dir> --node <id> [--budget N] [--hop-tokens N] " \
29
29
  "[--holder H] [--expires E] [--model M] [--attempt N] [--out PATH] [--force]"
30
30
  end
31
31
 
@@ -42,7 +42,7 @@ module NodePacketCLI
42
42
  intent_dir_arg = args.shift
43
43
 
44
44
  unless intent_directory?(intent_dir_arg && File.expand_path(intent_dir_arg))
45
- warn "node-packet: #{intent_dir_arg.inspect} is not an intent directory"
45
+ warn "node-input: #{intent_dir_arg.inspect} is not an intent directory"
46
46
  usage
47
47
  exit 2
48
48
  end
@@ -50,7 +50,7 @@ module NodePacketCLI
50
50
 
51
51
  node = opt(args, "--node")
52
52
  if node.to_s.strip.empty?
53
- warn "node-packet: --node is required"
53
+ warn "node-input: --node is required"
54
54
  usage
55
55
  exit 2
56
56
  end
@@ -64,11 +64,11 @@ module NodePacketCLI
64
64
  model = opt(args, "--model")
65
65
  force = args.include?("--force")
66
66
 
67
- result = NodePacket.build(
67
+ result = NodeInput.build(
68
68
  intent_dir: intent_dir,
69
69
  node: node,
70
- budget_tokens: budget ? budget.to_i : NodePacket::DEFAULT_BUDGET_TOKENS,
71
- hop_tokens: hop_tokens ? hop_tokens.to_i : NodePacket::DEFAULT_HOP_TOKENS,
70
+ budget_tokens: budget ? budget.to_i : NodeInput::DEFAULT_BUDGET_TOKENS,
71
+ hop_tokens: hop_tokens ? hop_tokens.to_i : NodeInput::DEFAULT_HOP_TOKENS,
72
72
  holder: holder,
73
73
  expires: expires,
74
74
  model: model,
@@ -78,15 +78,15 @@ module NodePacketCLI
78
78
  )
79
79
 
80
80
  if result[:ok]
81
- puts NodePacket.summary_line(result)
81
+ puts NodeInput.summary_line(result)
82
82
  puts result[:running_command]
83
83
  exit 0
84
84
  end
85
85
 
86
- Array(result[:errors]).each { |e| warn "node-packet: #{e}" }
86
+ Array(result[:errors]).each { |e| warn "node-input: #{e}" }
87
87
  puts result[:needs_decision_command] if result[:needs_decision_command]
88
88
  exit result[:exit_code]
89
89
  end
90
90
  end
91
91
 
92
- NodePacketCLI.main(ARGV) if $PROGRAM_NAME == __FILE__
92
+ NodeInputCLI.main(ARGV) if $PROGRAM_NAME == __FILE__
package/scripts/node-run CHANGED
@@ -8,7 +8,7 @@ require "yaml"
8
8
  require "fileutils"
9
9
  require_relative "lib/savepoint"
10
10
  require_relative "lib/node_ledger"
11
- require_relative "lib/node_packet"
11
+ require_relative "lib/node_input"
12
12
  require_relative "lib/node_worktree"
13
13
  require_relative "lib/node_file"
14
14
  require_relative "lib/ready_set"
@@ -19,9 +19,9 @@ require_relative "lib/codex_adapter"
19
19
  #
20
20
  # On Claude Code a subagent dispatch IS the executor; on Codex the executor
21
21
  # is a subprocess, so this script owns the timing of a whole node: it reads
22
- # the node's live `running` line, resolves the packet that line names,
22
+ # the node's live `running` line, resolves the node input that line names,
23
23
  # builds the `codex exec` argv for the node's kind (CodexAdapter), runs it
24
- # in the node's worktree with the packet on stdin, reads the executor's
24
+ # in the node's worktree with the node input on stdin, reads the executor's
25
25
  # final message, and writes it to the attempt's own return path.
26
26
  #
27
27
  # It writes NO ledger transition, on any path - success, refusal or error
@@ -41,8 +41,8 @@ require_relative "lib/codex_adapter"
41
41
  # 2 - usage: not an intent directory, or missing --node
42
42
  # 3 - the node has no live `running` line
43
43
  # 4 - the running line is held by another session
44
- # 5 - the packet the running line names is missing from disk
45
- # 6 - the packet on disk does not hash to the running line's packet=
44
+ # 5 - the node input the running line names is missing from disk
45
+ # 6 - the node input on disk does not hash to the running line's input=
46
46
  # 7 - no worktree could be resolved to run in
47
47
  module NodeRunCLI
48
48
  module_function
@@ -60,12 +60,12 @@ module NodeRunCLI
60
60
  dir && File.directory?(dir) && File.exist?(Savepoint.intent_file(dir))
61
61
  end
62
62
 
63
- # <intent_dir>/packets/<node>--a<N>.return.yml (spec): beside the packet,
64
- # one per attempt, the same treatment packet files already get - never
63
+ # <intent_dir>/attempts/<node>--a<N>.return.yml (spec): beside the node input,
64
+ # one per attempt, the same treatment input files already get - never
65
65
  # reused across attempts, so a second attempt's return can never be
66
66
  # mistaken for the first's.
67
67
  def return_path(intent_dir, node, attempt)
68
- File.join(intent_dir, "packets", "#{node}--a#{attempt}.return.yml")
68
+ File.join(intent_dir, "attempts", "#{node}--a#{attempt}.return.yml")
69
69
  end
70
70
 
71
71
  # The node's own declared kind, read straight off its node file - "work"
@@ -167,25 +167,25 @@ module NodeRunCLI
167
167
 
168
168
  kind = resolve_kind(intent_dir, node)
169
169
 
170
- # matrix row 6.11: the attempt number comes from NodePacket's own
170
+ # matrix row 6.11: the attempt number comes from NodeInput's own
171
171
  # counter, never a hand count. `lease_flag_given: false` because the
172
172
  # running line this attempt is acting on ALREADY exists on disk (it is
173
173
  # what we just read above) - counting it a second time would double it
174
174
  # (the same count, plus one, that produced it in the first place when
175
- # the packet was originally built).
176
- attempt = NodePacket.compute_attempt_number(intent_dir: intent_dir, node: node, lease_flag_given: false)
177
- packet_path = NodePacket.packet_path(intent_dir: intent_dir, node: node, attempt: attempt)
175
+ # the node input was originally built).
176
+ attempt = NodeInput.compute_attempt_number(intent_dir: intent_dir, node: node, lease_flag_given: false)
177
+ input_path = NodeInput.input_path(intent_dir: intent_dir, node: node, attempt: attempt)
178
178
 
179
- unless File.exist?(packet_path)
180
- warn "node-run: #{node}'s packet is missing at #{packet_path}"
179
+ unless File.exist?(input_path)
180
+ warn "node-run: #{node}'s node input is missing at #{input_path}"
181
181
  exit 5
182
182
  end
183
183
 
184
- packet_bytes = File.binread(packet_path)
185
- actual_sha = Digest::SHA256.hexdigest(packet_bytes)[0, 12]
186
- expected_sha = fields["packet"]
184
+ input_bytes = File.binread(input_path)
185
+ actual_sha = Digest::SHA256.hexdigest(input_bytes)[0, 12]
186
+ expected_sha = fields["input"]
187
187
  unless expected_sha && actual_sha == expected_sha
188
- warn "node-run: #{node}'s packet at #{packet_path} does not hash to packet=#{expected_sha.inspect} " \
188
+ warn "node-run: #{node}'s node input at #{input_path} does not hash to input=#{expected_sha.inspect} " \
189
189
  "(computed #{actual_sha})"
190
190
  exit 6
191
191
  end
@@ -203,7 +203,7 @@ module NodeRunCLI
203
203
  timeout_seconds = timeout_flag ? timeout_flag.to_i : CodexAdapter.timeout_seconds(kind)
204
204
  codex_argv = CodexAdapter.build_argv(kind: kind, worktree: worktree, output_last_message: message_path)
205
205
 
206
- result = CodexAdapter.execute(codex_argv, stdin_data: packet_bytes, timeout_seconds: timeout_seconds,
206
+ result = CodexAdapter.execute(codex_argv, stdin_data: input_bytes, timeout_seconds: timeout_seconds,
207
207
  output_last_message_path: message_path)
208
208
 
209
209
  # matrix rows 6.20-6.22: a nonzero exit or a timeout is authoritative -
@@ -19,12 +19,11 @@
19
19
  # corrupt lock with the resolving doctor hint
20
20
  # who compact, read-only durable owner/activity view
21
21
  # status report the lock file, its freshness, the derived worktree,
22
- # whether this session's pointer names the intent, and any
23
- # live per-artifact claims
22
+ # whether this session is the one delivering the intent, and
23
+ # any live per-artifact claims
24
24
  # fix idempotent repair of the lock for the current session; never
25
25
  # touches a fresh foreign lock
26
- # release owner clears the lock and points the session back at the day
27
- # ledger (End tail / abandoning a boarding)
26
+ # release owner clears the lock (End tail / abandoning a boarding)
28
27
  # reclaim explicit takeover of a stale lock, audited in savepoint.md
29
28
  # delegate owner registers a subagent session under the lock (D4)
30
29
  # claim acquire the named --artifact's claim token (intent 111);
@@ -32,18 +31,18 @@
32
31
  # including this same session; takes over a stale claim
33
32
  # release-claim free the named --artifact's claim token
34
33
  #
35
- # Without --intent-dir the intent is resolved from this session's pointer
36
- # (~/.plastic/store/.tmp/<session>/current) when it names an intent id, looked
37
- # up in the working directory's project store and then the global store. The
38
- # /tmp bridge JSON this CLI used to resolve through was removed in 2.0
39
- # (intent 307). Exit 0 on success/report; exit 1 when the verb is blocked.
34
+ # Without --intent-dir the intent is resolved from this session's held
35
+ # delivery lock (ActiveDelivery.resolve over the global store and the
36
+ # configured project roots): exactly one fresh lock this session owns or
37
+ # delegates for. Session resolution no longer reads any per-session state
38
+ # file; that path was retired in 2.0 (intent 307) and 344 G11.
39
+ # Exit 0 on success/report; exit 1 when the verb is blocked.
40
40
 
41
41
  require "json"
42
42
  require "yaml"
43
- require_relative "lib/bridge"
44
43
  require_relative "lib/lock"
45
44
  require_relative "lib/arm"
46
- require_relative "lib/session_ledger"
45
+ require_relative "lib/active_delivery"
47
46
 
48
47
  VERBS = %w[arm status who fix release reclaim delegate claim release-claim].freeze
49
48
 
@@ -88,16 +87,7 @@ hint_harness = harness || "claude"
88
87
  plastic_home_env = ENV["PLASTIC_HOME"].to_s.strip
89
88
  home = plastic_home_env.empty? ? Dir.home : File.dirname(File.expand_path(plastic_home_env))
90
89
 
91
- # The stores this session's pointer is resolved against: the project store of
92
- # the working directory (projects.yml, longest path match), then the global one.
93
- def candidate_stores(home, cwd)
94
- plastic_home = File.join(home, ".plastic")
95
- stores = []
96
- slug = SessionLedger.project_slug(cwd, plastic_home: plastic_home)
97
- stores << File.join(plastic_home, "projects", slug, "store") if slug && slug != "global"
98
- stores << File.join(plastic_home, "store")
99
- stores
100
- end
90
+ plastic_home = File.join(home, ".plastic")
101
91
 
102
92
  dir = opts[:dir]
103
93
  if dir.nil? || dir.strip.empty?
@@ -105,11 +95,21 @@ if dir.nil? || dir.strip.empty?
105
95
  warn "plastic-lock: #{verb} needs --intent-dir <intent dir>"
106
96
  exit 1
107
97
  end
108
- dir = Arm.intent_dir_from_pointer(session, home: home, stores: candidate_stores(home, Dir.pwd))
109
- end
110
- if dir.nil?
111
- warn "plastic-lock: no intent resolved; pass --intent-dir <intent dir>"
112
- exit 1
98
+ roots = ActiveDelivery.project_roots(plastic_home)
99
+ global_store = File.join(plastic_home, "store")
100
+ dir = ActiveDelivery.resolve(global_store: global_store, project_roots: roots, session: session)
101
+ if dir.nil?
102
+ candidates = ActiveDelivery.candidate_intent_dirs(global_store: global_store, project_roots: roots).uniq
103
+ held = candidates.select do |candidate|
104
+ Lock.fresh?(candidate) && Lock.holds?(candidate, session: session)
105
+ end
106
+ if held.size > 1
107
+ warn "plastic-lock: more than one held delivery lock names this session; pass --intent-dir <intent dir>"
108
+ else
109
+ warn "plastic-lock: no intent resolved; pass --intent-dir <intent dir>"
110
+ end
111
+ exit 1
112
+ end
113
113
  end
114
114
  dir = File.expand_path(dir)
115
115
 
@@ -144,7 +144,7 @@ if verb == "who"
144
144
  exit 0
145
145
  end
146
146
 
147
- intent_id = Bridge.intent_id_from_dir(dir)
147
+ intent_id = Arm.intent_id_for(dir)
148
148
  store = File.dirname(dir)
149
149
  key = Arm.resolve_session(session, store: store, intent_id: intent_id)
150
150
 
@@ -163,7 +163,7 @@ when "arm"
163
163
  puts JSON.pretty_generate(
164
164
  "intent_dir" => dir, "session" => result[:session], "status" => result[:status].to_s,
165
165
  "run_mode" => result[:lock] && result[:lock]["run_mode"],
166
- "worktree" => wt, "pointer" => result[:pointer]
166
+ "worktree" => wt
167
167
  )
168
168
  when :held
169
169
  warn "plastic-lock: delivery lock for intent #{intent_id} is held by session " \
@@ -188,7 +188,10 @@ when "arm"
188
188
  end
189
189
  when "status"
190
190
  lock = Lock.read(dir)
191
- pointer = Arm.read_pointer(key, home: home)
191
+ resolved = ActiveDelivery.resolve(global_store: File.join(plastic_home, "store"),
192
+ project_roots: ActiveDelivery.project_roots(plastic_home),
193
+ session: key)
194
+ delivering = !resolved.nil? && File.expand_path(resolved) == File.expand_path(dir)
192
195
  report = {
193
196
  "intent_dir" => dir,
194
197
  "session" => key,
@@ -196,8 +199,7 @@ when "status"
196
199
  "lock_fresh" => Lock.fresh?(dir),
197
200
  "lock_corrupt" => Lock.corrupt?(dir),
198
201
  "worktree" => Arm.worktree_block(intent_dir: dir, home: home),
199
- "pointer" => pointer,
200
- "pointer_names_intent" => pointer == intent_id,
202
+ "delivering" => delivering,
201
203
  "claims" => Claim.claims_status(dir),
202
204
  }
203
205
  puts JSON.pretty_generate(report)
@@ -217,7 +219,6 @@ when "release"
217
219
  warn "plastic-lock: not the owner; run plastic-lock status"
218
220
  exit 1
219
221
  end
220
- Arm.reset_pointer(key, intent_id, home: home)
221
222
  puts "released (#{result})"
222
223
  when "reclaim"
223
224
  status, lock_data = Lock.takeover(dir, session: key, harness: harness,
package/scripts/runner CHANGED
@@ -412,7 +412,7 @@ module Runner
412
412
  exit(exit_code)
413
413
  end
414
414
 
415
- # Row 1.24: lives beside savepoint.md, never inside packets/ (which the
415
+ # Row 1.24: lives beside savepoint.md, never inside attempts/ (which the
416
416
  # reaper treats as attempt evidence). Row 1.32: registers itself in the
417
417
  # store's own top-level `.gitignore` (Worktree.ensure_gitignored's own
418
418
  # idempotent, non-raising append) the same way `*.lock` already is, so a
@@ -46,10 +46,10 @@ QMD-first (when available): when the user describes the work instead of naming a
46
46
  ## Take the intent (do this FIRST)
47
47
 
48
48
  Immediately after selecting the intent, take it for this session. One verb acquires the durable
49
- `delivery.lock` in the intent directory (stamped `run_mode: auto`), provisions the code worktree
50
- at `<repo>/.claude/worktrees/{id}--{slug}` on branch `plastic/{id}--{slug}`, and points this
51
- session at the intent (`~/.plastic/store/.tmp/<session>/current`), so the record hook writes
52
- savepoint lines and heartbeats for it instead of the day ledger:
49
+ `delivery.lock` in the intent directory (stamped `run_mode: auto`) and provisions the code worktree
50
+ at `<repo>/.claude/worktrees/{id}--{slug}` on branch `plastic/{id}--{slug}`; the delivery lock
51
+ names this session as the one delivering the intent, so the record hook writes savepoint lines
52
+ and heartbeats for it instead of the day ledger:
53
53
 
54
54
  ```bash
55
55
  codex="${CODEX_THREAD_ID:-}"; claude="${CLAUDE_CODE_SESSION_ID:-}"
@@ -78,7 +78,7 @@ for work small enough to skip speccing, delivered inline with no separate plan r
78
78
  is no intent tier and no stage agent; depth follows the work.
79
79
 
80
80
  `runner step` computes readiness and prints a spawn block per dispatched node - agent, model,
81
- packet path, the test command, the call cap - fenced for a session to paste into the Agent
81
+ node input path, the test command, the call cap - fenced for a session to paste into the Agent
82
82
  tool; the runner never spawns (327 D42). `runner status` renders the ledger; `runner answer`
83
83
  closes a `needs_decision` node.
84
84
 
@@ -259,8 +259,8 @@ Read `../plastic-conventions/references/completion-and-done.md` for what "intent
259
259
  4. Review `## Insights` for observations that should become future intents; create them through
260
260
  `plastic-intent-creating` and update `chain`.
261
261
  5. Close through `plastic-intent-ending`, which runs `scripts/end-intent`: outcome, INDEX,
262
- savepoint, the store commit, and the disarm (the worktree released, `delivery.lock` cleared,
263
- the session pointer back on the day ledger), then the QMD reindex last and the single owner
262
+ savepoint, the store commit, and the disarm (the worktree released, `delivery.lock`
263
+ cleared), then the QMD reindex last and the single owner
264
264
  report. Pass `--session` and `--index-note`:
265
265
  ```bash
266
266
  ruby ~/.plastic/scripts/end-intent --store <store_path> --id <ID> --disposition delivered \
@@ -2,8 +2,8 @@
2
2
 
3
3
  Deep mechanics behind two spots in `SKILL.md`: how `plastic-lock arm` resolves a session id
4
4
  when it takes an intent, and why the End-tail steps in Completion (release the worktree, clear
5
- the lock, reset the pointer, reindex) run in that exact order. The `/tmp` bridge JSON these
6
- steps once also purged was removed in 2.0 (intent 307); `scripts/lib/arm.rb` is what remains.
5
+ the lock, reindex) run in that exact order. `scripts/lib/arm.rb` carries the arm, disarm, and
6
+ repair operations.
7
7
 
8
8
  ## Table of Contents
9
9
 
@@ -22,18 +22,16 @@ session-less close resolve to the same key.
22
22
  Arming acquires the durable `delivery.lock` in the intent dir, keyed by that resolved session
23
23
  and stamped with `run_mode`. Ownership is session-keyed, not process-keyed, so the arm command
24
24
  exiting is fine by construction: the lock stays yours for every later tool call in this
25
- session, and the record hook refreshes its lease on every write you make. The same call writes
26
- the intent id into the session pointer (`~/.plastic/store/.tmp/<session>/current`), which is
25
+ session, and the record hook refreshes its lease on every write you make. Holding that lock is
27
26
  why the capture and record hooks stop treating your prompts as day-ledger items for the rest
28
27
  of the delivery.
29
28
 
30
29
  ## Disarm ordering and worktree cleanup rationale
31
30
 
32
31
  `Arm.disarm` runs the ordered End tail: it releases the worktree first, then clears the
33
- intent's `delivery.lock` as its recorded owner, and only then resets the session pointer to
34
- today's day id (when it still names this intent). The worktree goes first so a released lock
35
- never points at a checkout another session could claim; the pointer goes last so the record
36
- hook keeps heartbeating the lock until the lock is gone.
32
+ intent's `delivery.lock` as its recorded owner. The worktree goes first so a released lock
33
+ never points at a checkout another session could claim; the lock goes last so the record
34
+ hook keeps heartbeating it until the delivery is actually done.
37
35
 
38
36
  **Mechanized since intent 188.** `scripts/end-intent` performs this disarm itself, as its own
39
37
  step 5, after steps 1-4 (outcome/INDEX/savepoint/commit) commit. A pre-flight lock guard
@@ -52,10 +50,10 @@ survives and can be reclaimed).
52
50
  When the work is being shipped through a release, do NOT rely on this plain remove.
53
51
  `skills/releasing/SKILL.md` runs its worktree-merge step BEFORE its `end-intent` call, merging
54
52
  the intent's code branch (`plastic/{id}--{slug}`) back to the repo's default branch BEFORE
55
- the worktree is removed, via `Worktree.finish(Arm.bridge_hash(intent_dir: ...), merge: true)`
53
+ the worktree is removed, via `Worktree.finish(Arm.delivery(intent_dir: ...), merge: true)`
56
54
  (merge-then-remove), so the integrated work is not lost. By the time `end-intent`'s own step 5
57
- runs afterward, the worktree is already gone (a harmless no-op) and only the delivery lock and
58
- the pointer are left to clear. Never leave an orphaned worktree, and run `git worktree prune`
55
+ runs afterward, the worktree is already gone (a harmless no-op) and only the delivery lock is
56
+ left to clear. Never leave an orphaned worktree, and run `git worktree prune`
59
57
  if you hit a stale reference.
60
58
 
61
59
  ## QMD reindex ordering rationale
@@ -17,10 +17,10 @@ self-declares its disposition through a `disposition: delivered|abandoned` front
17
17
  header. The delivered path authors it with the result; the abandoned path authors it with
18
18
  the abandonment reason and no longer leaves the scaffolded placeholder sentinel in place.
19
19
 
20
- The canonical End tail runs in this order, and the QMD reindex is always LAST, after the
21
- purge: `outcome.md -> INDEX terminal -> the terminal savepoint line -> commit -> disarm
22
- (Worktree.release -> Lock.release -> purge) -> QMD reindex`. Running the reindex last keeps the index from
23
- ever referencing a bridge or lock that disarm is about to remove.
20
+ The canonical End tail runs in this order: `outcome.md -> INDEX terminal -> the terminal
21
+ savepoint line -> commit -> disarm (Worktree.release -> Lock.release) -> QMD reindex`, the
22
+ reindex always LAST. Running the reindex last keeps the index from ever referencing a lock
23
+ that disarm just removed.
24
24
 
25
25
  `scripts/end-intent` performs this order's disarm step (verify the code worktree is clean,
26
26
  then merge/remove worktrees, then clear the lock) as its own step 5, mechanically, since
@@ -31,12 +31,11 @@ foreign session, reclaims a stale one with an audit line), and a dirty code work
31
31
  refuses before removal rather than force-discarding uncommitted changes.
32
32
 
33
33
  The post-done access window is lock-bounded: `[INDEX terminal -> Lock.release]`. Through it
34
- the completing session keeps full read and write access to the terminal directory and no
35
- purge can fire (108's lock-held keep-guard keeps the bridge while `delivery.lock` exists).
36
- Once the lock is released the window closes: the bridge becomes purge-eligible and the
37
- directory is frozen. A crash mid-tail is recovered by stale-lock reclaim plus finishing the
38
- tail; `doctor` surfaces this as a "stalled completion" (terminal in INDEX but the lock is
39
- still present or stale). Finishing the tail is FINISHING a completion, never a reactivation:
34
+ the completing session keeps full read and write access to the terminal directory (108's
35
+ lock-held keep-guard holds it open while `delivery.lock` exists). Once the lock is released
36
+ the window closes and the directory is frozen. A crash mid-tail is recovered by stale-lock
37
+ reclaim plus finishing the tail; `doctor` surfaces this as a "stalled completion" (terminal in
38
+ INDEX but the lock is still present or stale). Finishing the tail is FINISHING a completion, never a reactivation:
40
39
  a done intent is never moved back to `## Active`.
41
40
 
42
41
  One report per audience: a delivery produces `outcome.md` plus one EM-to-CTO owner report, and
@@ -50,7 +50,7 @@ This chapter holds the linking doctrine from Frontmatter and the branch-vs-root
50
50
 
51
51
  A thing is named after the concept family it lives under. A node is a graph-engineering
52
52
  concept, so its name comes from graph engineering (node, edge, ready set, critical path),
53
- from the Plastic concepts coined on top of it (intent, ledger, packet, lease, gate, runner),
53
+ from the Plastic concepts coined on top of it (intent, ledger, node input, lease, gate, runner),
54
54
  and from the software and AI engineering concepts those rest on (review, fix, test, verify,
55
55
  dispatch, executor, reviewer). A name from outside that stack is refused. Where no existing
56
56
  concept fits, that is a design finding to raise, not a word to coin.