@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
@@ -10,18 +10,18 @@ require_relative "graph_file"
10
10
  require_relative "node_ledger"
11
11
  require_relative "savepoint"
12
12
  require_relative "insights"
13
- require_relative "packet_wrapper"
13
+ require_relative "data_boundary"
14
14
  require_relative "atomic_write"
15
15
  require_relative "arm"
16
16
 
17
- # NodePacket (intent 338, G5): builds a node's whole input from disk, the
17
+ # NodeInput (intent 338, G5): builds a node's whole input from disk, the
18
18
  # five blocks 327 section 8 fixed - the node, the ledger, the record, the
19
19
  # knowledge hop, and where to work. n2 (this half) is the readers: every one
20
20
  # of them a keyword seam with a real default, so the suite never shells out
21
21
  # to git, never reads the owner's real store, and never sets an environment
22
- # variable (spec D16). n3 adds assembly, the budget, and the packet's
22
+ # variable (spec D16). n3 adds assembly, the budget, and the node input's
23
23
  # identity on top of the same file.
24
- module NodePacket
24
+ module NodeInput
25
25
  module_function
26
26
 
27
27
  DEFAULT_BUDGET_TOKENS = 8000
@@ -31,11 +31,11 @@ module NodePacket
31
31
  INSIGHTS_KEEP = 3
32
32
 
33
33
  # C7's "the executor stops without it" (spec D9), rendered whenever a
34
- # packet carries no lease and whenever the worktree Arm.worktree_block
34
+ # node input carries no lease and whenever the worktree Arm.worktree_block
35
35
  # reports is not provisioned.
36
36
  STOP_DIRECTIVE = "STOP: no lease is recorded for this node. Do not edit files or run any command until a runner dispatches this node with a holder, an expiry and a model."
37
37
 
38
- # Pinned so `packet=<sha>` is a function of the repo's history alone
38
+ # Pinned so `input=<sha>` is a function of the repo's history alone
39
39
  # (post-execution review finding B4): unpinned, `git log --stat` varies
40
40
  # with the terminal's COLUMNS (abbreviates paths, narrows the graph
41
41
  # column), the caller's `color.ui` (ANSI escapes land in the ledger data
@@ -57,7 +57,7 @@ module NodePacket
57
57
 
58
58
  # `landed commits` is one of the three never-cut blocks (matrix 3.9), so an
59
59
  # unbounded `git log --stat` (ten verbose commit messages, say) could route
60
- # the whole packet straight to exit 4 with no cut able to help.
60
+ # the whole node input straight to exit 4 with no cut able to help.
61
61
  def truncate_landed_commits(out)
62
62
  return out.to_s if out.to_s.bytesize <= LANDED_COMMITS_MAX_BYTES
63
63
 
@@ -158,7 +158,7 @@ module NodePacket
158
158
  # of the post-execution review): `node-transition` is a concurrent
159
159
  # appending writer, so re-reading `savepoint.md` once per predecessor let a
160
160
  # line landing mid-build make the Transitions, Lease and attempt number of
161
- # one packet disagree with each other. `build` reads once and threads the
161
+ # one node input disagree with each other. `build` reads once and threads the
162
162
  # same entries through every block; a direct caller with no entries to
163
163
  # share still gets a real default that reads the file itself.
164
164
  def predecessor_block(intent_dir:, node:, graph_reader: GraphFile.method(:parse), entries: nil)
@@ -186,12 +186,12 @@ module NodePacket
186
186
  end
187
187
  private_class_method :last_running_entry
188
188
 
189
- # Whether the packet renders no lease at all (matrix 2.11a, post-execution
189
+ # Whether the node input renders no lease at all (matrix 2.11a, post-execution
190
190
  # review finding A1): neither a flag-supplied lease nor a recorded
191
191
  # `running` line for this node. `lease_block` itself only ever renders
192
192
  # `lease: none` for this case (spec D3: block 2 is retrieved data, and C7's
193
193
  # stop directive is instruction, so it can never live inside that data
194
- # block, on pain of being self-cancelling under the packet's own trust
194
+ # block, on pain of being self-cancelling under the node input's own trust
195
195
  # rule). `build` uses this to decide whether the stop directive belongs in
196
196
  # block 5 instead, deduplicated against the worktree's own copy.
197
197
  def lease_missing?(node:, holder:, expires:, model:, entries:)
@@ -304,12 +304,12 @@ module NodePacket
304
304
  chunks = Array(sources).map { |src| hop_chunk(store_dir, src) }
305
305
  text = chunks.join("\n\n").scrub
306
306
  cap = hop_tokens.to_i
307
- tokens = PacketWrapper.estimate_tokens(text)
307
+ tokens = DataBoundary.estimate_tokens(text)
308
308
  if tokens > cap
309
309
  note = "\n[hop truncated at #{cap} tokens]"
310
310
  max_bytes = [(cap * 4) - note.bytesize, 0].max
311
311
  text = "#{safe_byteslice(text, max_bytes)}#{note}"
312
- tokens = PacketWrapper.estimate_tokens(text)
312
+ tokens = DataBoundary.estimate_tokens(text)
313
313
  end
314
314
  { text: text, tokens: tokens }
315
315
  end
@@ -401,7 +401,7 @@ module NodePacket
401
401
  # `permitted_classes` (post-execution review finding B3): the same bug
402
402
  # class `record_sources` already fixed once in 607e31e. Any project.yml
403
403
  # that gains a date-typed value (a `created:` field, say) silently
404
- # stripped the test command from every packet for that project, since
404
+ # stripped the test command from every node input for that project, since
405
405
  # the rescue swallowed `Psych::DisallowedClass` and returned nil.
406
406
  data = begin
407
407
  YAML.safe_load(File.read(path), permitted_classes: [Date, Time])
@@ -439,14 +439,14 @@ module NodePacket
439
439
  end
440
440
 
441
441
  # `lease_missing` (post-execution review finding A1) hoists C7's stop
442
- # directive here, block 5 (instruction, spec D3), whenever the packet
442
+ # directive here, block 5 (instruction, spec D3), whenever the node input
443
443
  # carries no lease. `worktree_block` already renders its own copy when the
444
444
  # worktree is unprovisioned; the two conditions often fire together, so a
445
445
  # directive already present is never repeated.
446
446
  #
447
447
  # `call_cap` (intent 355, n2, D2): one sentence naming this attempt's tool
448
448
  # call cap and the return it hits at, so the executor learns the number
449
- # from the packet it starts with, never from a denied call mid-edit
449
+ # from the node input it starts with, never from a denied call mid-edit
450
450
  # (matrix 2.4). nil (a caller that names no cap) renders nothing here.
451
451
  def where_to_work_block(intent_dir:, worktree_reader: Arm.method(:worktree_block),
452
452
  project_reader: method(:default_project_reader), lease_missing: false, call_cap: nil,
@@ -563,7 +563,7 @@ module NodePacket
563
563
  end
564
564
 
565
565
  # ===========================================================================
566
- # n3: assembly, the budget, and the packet's identity
566
+ # n3: assembly, the budget, and the node input's identity
567
567
  # ===========================================================================
568
568
 
569
569
  # Block labels/sources for the wrapped (data) blocks, spec D3.
@@ -610,7 +610,7 @@ module NodePacket
610
610
  "#{parts.join("\n")}\n"
611
611
  end
612
612
 
613
- # One packet, node and where-to-work as plain instruction text, ledger,
613
+ # One node input, node and where-to-work as plain instruction text, ledger,
614
614
  # record and (when present) hop wrapped as labeled data sharing ONE
615
615
  # boundary token computed over their raw payloads (spec D2-D4, matrix
616
616
  # 3.1-3.3).
@@ -618,22 +618,22 @@ module NodePacket
618
618
  # Blocks 1 and 5 are raw-interpolated (spec D3: instruction, not data), but
619
619
  # that trust does not reach a `release.verify` a project.yml can carry
620
620
  # (post-execution review finding A2): a line that is, on its own, a
621
- # complete data marker is disarmed by `PacketWrapper.neutralize_marker_lines`
622
- # before it ever reaches the packet, so a forged marker cannot open a
621
+ # complete data marker is disarmed by `DataBoundary.neutralize_marker_lines`
622
+ # before it ever reaches the node input, so a forged marker cannot open a
623
623
  # block outside the wrapper's own boundary. `record_source`/`hop_source`
624
624
  # (finding C15) are the record's and the hop sources' real store-relative
625
625
  # paths, never the placeholder label "record"/"sources".
626
- def render_packet(node_text:, ledger_text:, intent_text:, decisions:, insights:, hop:, where_text:,
626
+ def render_input(node_text:, ledger_text:, intent_text:, decisions:, insights:, hop:, where_text:,
627
627
  record_source: "record", hop_source: "sources")
628
628
  record_text = render_record_text(intent: intent_text, decisions: decisions, insights: insights)
629
629
  hop_text = hop && hop[:text]
630
630
 
631
631
  payloads = [ledger_text, record_text]
632
632
  payloads << hop_text if hop_text
633
- token = PacketWrapper.boundary_token(payloads)
633
+ token = DataBoundary.boundary_token(payloads)
634
634
 
635
- node_text_safe = PacketWrapper.neutralize_marker_lines(node_text.to_s)
636
- where_text_safe = PacketWrapper.neutralize_marker_lines(where_text.to_s)
635
+ node_text_safe = DataBoundary.neutralize_marker_lines(node_text.to_s)
636
+ where_text_safe = DataBoundary.neutralize_marker_lines(where_text.to_s)
637
637
 
638
638
  # Normalized through `attr_safe` exactly as `wrap` normalizes the marker
639
639
  # attributes it writes (post-execution review finding A2's integrity
@@ -641,43 +641,43 @@ module NodePacket
641
641
  # `unwrap` reads back off the rendered marker line raised on every nil
642
642
  # source, because `attr_safe(nil)` renders as `""`, not `"nil".to_s`.
643
643
  wrapped_specs = [
644
- { label: PacketWrapper.attr_safe(LEDGER_LABEL), source: PacketWrapper.attr_safe("savepoint.md") },
645
- { label: PacketWrapper.attr_safe(RECORD_LABEL), source: PacketWrapper.attr_safe(record_source) },
644
+ { label: DataBoundary.attr_safe(LEDGER_LABEL), source: DataBoundary.attr_safe("savepoint.md") },
645
+ { label: DataBoundary.attr_safe(RECORD_LABEL), source: DataBoundary.attr_safe(record_source) },
646
646
  ]
647
- wrapped_specs << { label: PacketWrapper.attr_safe(HOP_LABEL), source: PacketWrapper.attr_safe(hop_source) } if hop_text
647
+ wrapped_specs << { label: DataBoundary.attr_safe(HOP_LABEL), source: DataBoundary.attr_safe(hop_source) } if hop_text
648
648
 
649
649
  parts = [node_text_safe.rstrip, ""]
650
- parts << PacketWrapper.wrap(ledger_text, label: LEDGER_LABEL, source: "savepoint.md", token: token).rstrip
650
+ parts << DataBoundary.wrap(ledger_text, label: LEDGER_LABEL, source: "savepoint.md", token: token).rstrip
651
651
  parts << ""
652
- parts << PacketWrapper.wrap(record_text, label: RECORD_LABEL, source: record_source, token: token).rstrip
652
+ parts << DataBoundary.wrap(record_text, label: RECORD_LABEL, source: record_source, token: token).rstrip
653
653
  parts << ""
654
654
  if hop_text
655
- parts << PacketWrapper.wrap(hop_text, label: HOP_LABEL, source: hop_source, token: token).rstrip
655
+ parts << DataBoundary.wrap(hop_text, label: HOP_LABEL, source: hop_source, token: token).rstrip
656
656
  parts << ""
657
657
  end
658
658
  parts << where_text_safe.rstrip
659
659
  rendered = "#{parts.join("\n")}\n"
660
660
 
661
- assert_packet_integrity!(rendered, wrapped_specs)
661
+ assert_input_integrity!(rendered, wrapped_specs)
662
662
  rendered
663
663
  end
664
664
 
665
665
  # The trust boundary's own invariant (post-execution review finding A2):
666
- # the finished packet must unwrap to exactly the data blocks that were
666
+ # the finished node input must unwrap to exactly the data blocks that were
667
667
  # wrapped, same count, same labels, same sources, in order. This is what
668
668
  # the escaping rule and the marker-line neutralization pass are FOR, so the
669
669
  # check belongs here, not only in a test that could rot independently of
670
670
  # the code it is meant to guard.
671
- def assert_packet_integrity!(rendered, wrapped_specs)
672
- actual = PacketWrapper.unwrap(rendered).map { |b| { label: b[:label], source: b[:source] } }
671
+ def assert_input_integrity!(rendered, wrapped_specs)
672
+ actual = DataBoundary.unwrap(rendered).map { |b| { label: b[:label], source: b[:source] } }
673
673
  return if actual == wrapped_specs
674
674
 
675
- raise "node packet integrity check failed: expected #{wrapped_specs.inspect}, got #{actual.inspect}"
675
+ raise "node input integrity check failed: expected #{wrapped_specs.inspect}, got #{actual.inspect}"
676
676
  end
677
- private_class_method :assert_packet_integrity!
677
+ private_class_method :assert_input_integrity!
678
678
 
679
679
  def render_from_state(state)
680
- render_packet(node_text: state[:node_text], ledger_text: state[:ledger_text], intent_text: state[:intent_text],
680
+ render_input(node_text: state[:node_text], ledger_text: state[:ledger_text], intent_text: state[:intent_text],
681
681
  decisions: state[:decisions], insights: state[:insights], hop: state[:hop],
682
682
  where_text: state[:where_text], record_source: state[:record_source] || "record",
683
683
  hop_source: state[:hop_source] || "sources")
@@ -685,7 +685,7 @@ module NodePacket
685
685
  private_class_method :render_from_state
686
686
 
687
687
  def estimate_rendered_tokens(rendered)
688
- PacketWrapper.estimate_tokens(rendered)
688
+ DataBoundary.estimate_tokens(rendered)
689
689
  end
690
690
 
691
691
  # The C28 cut ladder: drop the hop whole, then cut Insights to the last
@@ -748,8 +748,8 @@ module NodePacket
748
748
  "record" => record_text,
749
749
  "where to work" => state[:where_text],
750
750
  }
751
- name, text = candidates.max_by { |_, t| PacketWrapper.estimate_tokens(t.to_s) }
752
- [name, PacketWrapper.estimate_tokens(text.to_s)]
751
+ name, text = candidates.max_by { |_, t| DataBoundary.estimate_tokens(t.to_s) }
752
+ [name, DataBoundary.estimate_tokens(text.to_s)]
753
753
  end
754
754
  private_class_method :name_oversized_block
755
755
 
@@ -760,10 +760,10 @@ module NodePacket
760
760
 
761
761
  # C21: the number of `running` lines already recorded for `node`, plus one
762
762
  # when a lease is being supplied by flag (a NEW dispatch), floored at 1.
763
- # Minor 5: a torn `running` line (missing holder=/expires=/packet=/model=)
763
+ # Minor 5: a torn `running` line (missing holder=/expires=/input=/model=)
764
764
  # is skipped here exactly as ReadySet.attempts_count already skips it -
765
765
  # counting it desynchronizes this attempt number from the extensions file
766
- # a live node's own `packets/<node>--a<N>.extensions` names, since that
766
+ # a live node's own `attempts/<node>--a<N>.extensions` names, since that
767
767
  # file is keyed by the attempt sweep computed off the SAME filtered count.
768
768
  def compute_attempt_number(intent_dir:, node:, lease_flag_given:, entries: nil)
769
769
  entries ||= NodeLedger.entries(savepoint_path(intent_dir))
@@ -771,8 +771,8 @@ module NodePacket
771
771
  [count + (lease_flag_given ? 1 : 0), 1].max
772
772
  end
773
773
 
774
- def packet_path(intent_dir:, node:, attempt:)
775
- File.join(intent_dir, "packets", "#{node}--a#{attempt}.packet")
774
+ def input_path(intent_dir:, node:, attempt:)
775
+ File.join(intent_dir, "attempts", "#{node}--a#{attempt}.input")
776
776
  end
777
777
 
778
778
  def summary_line(result)
@@ -781,7 +781,7 @@ module NodePacket
781
781
  end
782
782
 
783
783
  def running_command(intent_dir:, node:, sha:, hop_tokens:)
784
- "node-transition #{intent_dir} --node #{node} --state running --field packet=#{sha} --field hop=#{hop_tokens}"
784
+ "node-transition #{intent_dir} --node #{node} --state running --field input=#{sha} --field hop=#{hop_tokens}"
785
785
  end
786
786
 
787
787
  def needs_decision_command(intent_dir:, node:, question:)
@@ -789,7 +789,7 @@ module NodePacket
789
789
  "node-transition #{intent_dir} --node #{node} --state needs_decision --field question=\"#{escaped}\""
790
790
  end
791
791
 
792
- # Build one node's whole packet from disk (spec D1). Returns
792
+ # Build one node's whole input from disk (spec D1). Returns
793
793
  # {ok:, exit_code:, path:, sha:, tokens:, hop_tokens:, attempt:,
794
794
  # cuts_applied:, running_command:, errors:} on success, or
795
795
  # {ok: false, exit_code:, errors:, needs_decision_command: (on overflow)}
@@ -831,7 +831,7 @@ module NodePacket
831
831
  # it (post-execution review finding B12): `node-transition` is a
832
832
  # concurrent appending writer, so re-reading `savepoint.md` once per
833
833
  # block risked a line landing mid-build and making the Transitions,
834
- # Lease and attempt number of one packet disagree with each other.
834
+ # Lease and attempt number of one node input disagree with each other.
835
835
  entries = NodeLedger.entries(savepoint_path(intent_dir))
836
836
 
837
837
  ledger_text = full_ledger_text(intent_dir: intent_dir, node: node, files: nb[:files], holder: holder,
@@ -843,7 +843,7 @@ module NodePacket
843
843
  hop_full = hop_block(store_dir: store_dir, sources: sources, hop_tokens: hop_tokens)
844
844
 
845
845
  # Finding A1: the stop directive belongs in block 5 (instruction)
846
- # whenever the packet carries no lease at all, never inside block 2's
846
+ # whenever the node input carries no lease at all, never inside block 2's
847
847
  # ledger data (spec D3's self-cancellation risk).
848
848
  missing_lease = lease_missing?(node: node, holder: holder, expires: expires, model: model, entries: entries)
849
849
  where_text = where_to_work_block(intent_dir: intent_dir, worktree_reader: worktree_reader,
@@ -867,7 +867,7 @@ module NodePacket
867
867
  decisions: cuts_applied.include?(:decisions) ? state[:decisions].last(DECISIONS_KEEP) : state[:decisions],
868
868
  )
869
869
  oversized_name, oversized_tokens = name_oversized_block(final_state)
870
- question = "packet for #{node} is #{tokens} tokens after every cut, over the #{budget_tokens}-token " \
870
+ question = "node input for #{node} is #{tokens} tokens after every cut, over the #{budget_tokens}-token " \
871
871
  "budget; #{oversized_name} alone is #{oversized_tokens} tokens, shorten it"
872
872
  return {
873
873
  ok: false, exit_code: 4, tokens: tokens,
@@ -878,7 +878,7 @@ module NodePacket
878
878
 
879
879
  attempt_n = attempt || compute_attempt_number(intent_dir: intent_dir, node: node,
880
880
  lease_flag_given: lease_flag_given?(holder), entries: entries)
881
- path = out ? File.expand_path(out) : packet_path(intent_dir: intent_dir, node: node, attempt: attempt_n)
881
+ path = out ? File.expand_path(out) : input_path(intent_dir: intent_dir, node: node, attempt: attempt_n)
882
882
  FileUtils.mkdir_p(File.dirname(path))
883
883
 
884
884
  if File.exist?(path)
@@ -0,0 +1,62 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # NodeInputCompatibility (intent 338a, G6, n1, D10/D11): the one seam that
5
+ # lets a `running` line written before the node-input rename keep reading
6
+ # correctly. It is the ONLY shipped file allowed to name the retired field,
7
+ # and it names it exactly once, as LEGACY_FIELD below.
8
+ #
9
+ # It maps, never rewrites: the raw ledger line on disk is never touched.
10
+ # `NodeLedger.parse_transition_line` calls #fields on every parsed field hash
11
+ # before returning it, so torn detection, attempt counting, node-run,
12
+ # graph-measure and outcome-report all see `input` whether the line was
13
+ # written by an old dispatch or a new one.
14
+ #
15
+ # No project requires (spec D10): a plain module over a plain hash, so
16
+ # `node_ledger.rb` never gains a require cycle through this file.
17
+ module NodeInputCompatibility
18
+ module_function
19
+
20
+ # The retired field name, named once. No other shipped file names it.
21
+ LEGACY_FIELD = "packet"
22
+
23
+ # The retired directory and suffix (338a n3, D8), named once beside
24
+ # LEGACY_FIELD. No other shipped file spells either literal; a caller
25
+ # that needs the legacy input location builds it from these constants.
26
+ LEGACY_DIRECTORY = "packets"
27
+ LEGACY_SUFFIX = ".packet"
28
+
29
+ # A copy of `fields` where LEGACY_FIELD maps to "input" when "input" is
30
+ # absent or blank. "input" wins when both are present (D11): the newer,
31
+ # authoritative name is never overwritten by the retired one. LEGACY_FIELD
32
+ # itself is dropped from the copy either way, so a caller that renders
33
+ # every field it does not recognize (outcome-report's evidence line) never
34
+ # sees the retired key leak through as a second, unrecognized field.
35
+ def fields(fields)
36
+ source = fields || {}
37
+ return source.dup unless source.key?(LEGACY_FIELD)
38
+
39
+ mapped = source.dup
40
+ unless present?(mapped["input"])
41
+ mapped["input"] = mapped[LEGACY_FIELD]
42
+ end
43
+ mapped.delete(LEGACY_FIELD)
44
+ mapped
45
+ end
46
+
47
+ # 338a n3 (D8): the new path when it exists, the legacy directory and
48
+ # suffix when only that file exists, and the new path when neither does
49
+ # (a fresh attempt names its own soon-to-be-written destination).
50
+ def input_path(intent_dir:, node:, attempt:)
51
+ new_path = File.join(intent_dir, "attempts", "#{node}--a#{attempt}.input")
52
+ return new_path if File.exist?(new_path)
53
+
54
+ legacy_path = File.join(intent_dir, LEGACY_DIRECTORY, "#{node}--a#{attempt}#{LEGACY_SUFFIX}")
55
+ File.exist?(legacy_path) ? legacy_path : new_path
56
+ end
57
+
58
+ def present?(value)
59
+ !(value.nil? || value.to_s.strip.empty?)
60
+ end
61
+ private_class_method :present?
62
+ end
@@ -4,6 +4,7 @@
4
4
  require "strscan"
5
5
  require_relative "savepoint"
6
6
  require_relative "guarded_append"
7
+ require_relative "node_input_compatibility"
7
8
 
8
9
  # NodeLedger - the node and intent transition line (intent 335, G2). Owns the
9
10
  # line's byte-exact format, the closed state vocabulary, the required fields per
@@ -40,7 +41,7 @@ module NodeLedger
40
41
  # a flat list; DONE_EVIDENCE_FIELDS below carries the "at least one of" half.
41
42
  REQUIRED_FIELDS = {
42
43
  "planned" => [],
43
- "running" => %w[holder expires packet model],
44
+ "running" => %w[holder expires input model],
44
45
  "done" => %w[gates],
45
46
  "failed_verification" => %w[gates reason],
46
47
  "needs_decision" => %w[question],
@@ -68,7 +69,7 @@ module NodeLedger
68
69
  # treatment `suite=`, `hop=` and `core_drift=` already get as extras, given
69
70
  # a stable declared position instead of falling to the alphabetical extras
70
71
  # tail (matrix row 1.17).
71
- FIELD_ORDER = %w[holder expires packet model harness gates commit verdict reason question by expired].freeze
72
+ FIELD_ORDER = %w[holder expires input model harness gates commit verdict reason question by expired].freeze
72
73
 
73
74
  SEPARATOR = " "
74
75
 
@@ -194,6 +195,7 @@ module NodeLedger
194
195
  timestamp, subject, rest = m.captures
195
196
  state, remainder = rest.split(/\s+/, 2)
196
197
  fields, comment = scan_fields(remainder.to_s)
198
+ fields = NodeInputCompatibility.fields(fields)
197
199
  { timestamp: timestamp, subject: subject, state: state, fields: fields, comment: comment, raw: raw }
198
200
  end
199
201
 
@@ -120,7 +120,7 @@ module OutcomeReport
120
120
  # --- n2: the generator -------------------------------------------------------
121
121
 
122
122
  PLACEHOLDER_SUMMARY = "(what was delivered)"
123
- NON_EVIDENCE_FIELD_KEYS = %w[holder expires packet model gates commit verdict reason question by expired suite].freeze
123
+ NON_EVIDENCE_FIELD_KEYS = %w[holder expires input model gates commit verdict reason question by expired suite].freeze
124
124
 
125
125
  # D14: a pipe in any generated table cell (or bullet line) is replaced with a
126
126
  # forward slash, never escaped - ReportScreen.table_rows splits on the bare
@@ -0,0 +1,45 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "yaml"
5
+
6
+ # ProjectConfig - reads a project's project.yml merged over the shipped defaults
7
+ # (moved off the retired shared-helpers module by intent 344, D2). A missing or
8
+ # malformed project.yml never raises: read returns the defaults with a warning
9
+ # on stderr instead.
10
+ module ProjectConfig
11
+ module_function
12
+
13
+ DEFAULTS = {
14
+ "governing_docs" => ["AGENTS.md"],
15
+ "release" => {
16
+ "on_complete" => "commit",
17
+ },
18
+ }.freeze
19
+
20
+ def read(slug)
21
+ path = File.join(Dir.home, ".plastic", "projects", slug, "project.yml")
22
+ config = if File.exist?(path)
23
+ YAML.safe_load(File.read(path)) || {}
24
+ else
25
+ {}
26
+ end
27
+
28
+ deep_merge(DEFAULTS, config)
29
+ rescue => e
30
+ $stderr.puts "Warning: failed to read project config for #{slug}: #{e.message}"
31
+ DEFAULTS.dup
32
+ end
33
+
34
+ def deep_merge(base, overlay)
35
+ result = base.dup
36
+ overlay.each do |key, value|
37
+ if value.is_a?(Hash) && result[key].is_a?(Hash)
38
+ result[key] = deep_merge(result[key], value)
39
+ else
40
+ result[key] = value
41
+ end
42
+ end
43
+ result
44
+ end
45
+ end
@@ -45,7 +45,7 @@ module ReadySet
45
45
 
46
46
  # The fixed key list a ranker row ever carries (D11/C16): every key is
47
47
  # state- or edge-derived, never a telemetry field (tokens, wall, model,
48
- # suite, packet). Pinned by test/ready_set_ranker_test.rb.
48
+ # suite, input). Pinned by test/ready_set_ranker_test.rb.
49
49
  ROW_KEYS = %i[id kind batch retry downstream_hops on_critical_path].freeze
50
50
 
51
51
  # --- the readiness decision (D1) ---------------------------------------------
@@ -8,7 +8,7 @@ require_relative "guarded_append"
8
8
  # RoadmapSavepoint - the roadmap's machine counterpart to its human `## Log` (intent 134).
9
9
  #
10
10
  # Mirrors scripts/lib/qmd_sync.rb's class shape and the cycle-step savepoint ledger in
11
- # scripts/lib/bridge.rb (append semantics: idempotent `(event, detail)` pair dedup, one
11
+ # scripts/lib/savepoint.rb (append semantics: idempotent `(event, detail)` pair dedup, one
12
12
  # deterministic append primitive). Constructor-DI, hermetic: no eval, no ENV or global config
13
13
  # seam, clock injected through `now:`. Two operations:
14
14
  #
@@ -10,7 +10,7 @@ require_relative "node_return"
10
10
  require_relative "node_ledger"
11
11
  require_relative "node_worktree"
12
12
  require_relative "node_file"
13
- require_relative "node_packet"
13
+ require_relative "node_input"
14
14
  require_relative "ready_set"
15
15
  require_relative "core_integrity"
16
16
  require_relative "runner_core"
@@ -132,7 +132,7 @@ module RunnerAbsorb
132
132
  text = read_return_text(return_path)
133
133
  parsed = NodeReturn.parse(text)
134
134
 
135
- attempt = NodePacket.compute_attempt_number(intent_dir: intent_dir, node: node, lease_flag_given: false,
135
+ attempt = NodeInput.compute_attempt_number(intent_dir: intent_dir, node: node, lease_flag_given: false,
136
136
  entries: entries)
137
137
  preserve_return_file(intent_dir, node, attempt, return_path, text)
138
138
 
@@ -570,11 +570,11 @@ module RunnerAbsorb
570
570
  end
571
571
  private_class_method :read_return_text
572
572
 
573
- # Row 4.42: the return is kept beside the packet at
574
- # packets/<node>--a<N>.return - copied there when `return_path` names
573
+ # Row 4.42: the return is kept beside the node input at
574
+ # attempts/<node>--a<N>.return - copied there when `return_path` names
575
575
  # somewhere else, left alone when it already is that file.
576
576
  def preserve_return_file(intent_dir, node, attempt, return_path, text)
577
- target = NodePacket.packet_path(intent_dir: intent_dir, node: node, attempt: attempt).sub(/\.packet\z/, ".return")
577
+ target = NodeInput.input_path(intent_dir: intent_dir, node: node, attempt: attempt).sub(/\.input\z/, ".return")
578
578
  return target if File.expand_path(return_path.to_s) == File.expand_path(target)
579
579
 
580
580
  FileUtils.mkdir_p(File.dirname(target))