@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.
- package/PLASTIC.md +6 -5
- package/agents/plastic-node-research.md +4 -4
- package/agents/plastic-node-verify.md +4 -4
- package/agents/plastic-node-work.md +4 -4
- package/package.json +1 -1
- package/scripts/dashboard.rb +1 -1
- package/scripts/end-intent +19 -16
- package/scripts/exec-worktree +5 -5
- package/scripts/hook-call-budget +6 -6
- package/scripts/hook-capture +15 -15
- package/scripts/hook-record +21 -14
- package/scripts/hook-savepoint +2 -1
- package/scripts/hook-session-start +9 -10
- package/scripts/lib/active_delivery.rb +44 -0
- package/scripts/lib/arm.rb +41 -102
- package/scripts/lib/codex_adapter.rb +2 -2
- package/scripts/lib/{packet_wrapper.rb → data_boundary.rb} +7 -7
- package/scripts/lib/day_summary.rb +19 -11
- package/scripts/lib/doctor_session_ledger.rb +3 -52
- package/scripts/lib/exec_worktree.rb +24 -21
- package/scripts/lib/graph_measure_budget.rb +29 -28
- package/scripts/lib/handoff.rb +4 -8
- package/scripts/lib/harness_adapter.rb +6 -6
- package/scripts/lib/index_entry.rb +53 -0
- package/scripts/lib/insights.rb +1 -1
- package/scripts/lib/installer_core.rb +120 -7
- package/scripts/lib/lock.rb +8 -9
- package/scripts/lib/{node_packet.rb → node_input.rb} +49 -49
- package/scripts/lib/node_input_compatibility.rb +62 -0
- package/scripts/lib/node_ledger.rb +4 -2
- package/scripts/lib/outcome_report.rb +1 -1
- package/scripts/lib/project_config.rb +45 -0
- package/scripts/lib/ready_set.rb +1 -1
- package/scripts/lib/roadmap_savepoint.rb +1 -1
- package/scripts/lib/runner_absorb.rb +5 -5
- package/scripts/lib/runner_dispatch.rb +44 -44
- package/scripts/lib/runner_sweep.rb +4 -4
- package/scripts/lib/runner_until_empty.rb +1 -1
- package/scripts/lib/savepoint.rb +6 -7
- package/scripts/lib/scaffold_intent.rb +6 -3
- package/scripts/lib/session_close.rb +30 -28
- package/scripts/lib/session_git.rb +20 -14
- package/scripts/lib/session_ledger.rb +44 -4
- package/scripts/lib/worktree.rb +24 -24
- package/scripts/lib/worktree_sweep.rb +3 -3
- package/scripts/{node-packet → node-input} +15 -15
- package/scripts/node-run +19 -19
- package/scripts/plastic-lock +33 -32
- package/scripts/runner +1 -1
- package/skills/auto/SKILL.md +7 -7
- package/skills/auto/references/end-tail.md +9 -11
- package/skills/conventions/references/completion-and-done.md +9 -10
- package/skills/conventions/references/knowledge-graph.md +1 -1
- package/skills/conventions/references/locks-and-worktrees.md +10 -12
- package/skills/direct/SKILL.md +2 -2
- package/skills/doctor/SKILL.md +1 -1
- package/skills/intent-executing/SKILL.md +1 -1
- package/skills/releasing/SKILL.md +2 -2
- package/skills/releasing/references/promotion-and-tagging.md +1 -1
- package/skills/releasing/references/release-lines.md +1 -1
- package/templates/index.md +1 -1
- package/templates/project.yml +1 -1
- 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 "
|
|
13
|
+
require_relative "data_boundary"
|
|
14
14
|
require_relative "atomic_write"
|
|
15
15
|
require_relative "arm"
|
|
16
16
|
|
|
17
|
-
#
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
622
|
-
# before it ever reaches the
|
|
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
|
|
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 =
|
|
633
|
+
token = DataBoundary.boundary_token(payloads)
|
|
634
634
|
|
|
635
|
-
node_text_safe =
|
|
636
|
-
where_text_safe =
|
|
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:
|
|
645
|
-
{ label:
|
|
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:
|
|
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 <<
|
|
650
|
+
parts << DataBoundary.wrap(ledger_text, label: LEDGER_LABEL, source: "savepoint.md", token: token).rstrip
|
|
651
651
|
parts << ""
|
|
652
|
-
parts <<
|
|
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 <<
|
|
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
|
-
|
|
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
|
|
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
|
|
672
|
-
actual =
|
|
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
|
|
675
|
+
raise "node input integrity check failed: expected #{wrapped_specs.inspect}, got #{actual.inspect}"
|
|
676
676
|
end
|
|
677
|
-
private_class_method :
|
|
677
|
+
private_class_method :assert_input_integrity!
|
|
678
678
|
|
|
679
679
|
def render_from_state(state)
|
|
680
|
-
|
|
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
|
-
|
|
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|
|
|
752
|
-
[name,
|
|
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=/
|
|
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 `
|
|
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
|
|
775
|
-
File.join(intent_dir, "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 = "
|
|
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) :
|
|
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
|
|
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
|
|
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
|
|
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
|
package/scripts/lib/ready_set.rb
CHANGED
|
@@ -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,
|
|
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/
|
|
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 "
|
|
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 =
|
|
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
|
|
574
|
-
#
|
|
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 =
|
|
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))
|