@zalom/plastic 2.0.0-alpha.21 → 2.0.0-alpha.23
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 +20 -18
- package/agents/plastic-enforcer.md +6 -3
- package/agents/plastic-executor.md +4 -0
- package/agents/plastic-node-research.md +28 -0
- package/agents/plastic-node-verify.md +27 -0
- package/agents/plastic-node-work.md +32 -0
- package/bin/lib/context_budget.rb +1 -1
- package/hooks/hooks.json +12 -0
- package/hooks/statusline +28 -0
- package/hooks/stop +5 -0
- package/package.json +1 -1
- package/scripts/doctor.rb +159 -10
- package/scripts/end-intent +3 -3
- package/scripts/graph-measure +249 -0
- package/scripts/hook-capture +1 -0
- package/scripts/hook-savepoint +24 -2
- package/scripts/hook-session-start +333 -321
- package/scripts/hook-stop +57 -0
- package/scripts/insight-append +18 -4
- package/scripts/lib/active_delivery.rb +61 -0
- package/scripts/lib/agent_models.rb +10 -1
- package/scripts/lib/codex_adapter.rb +197 -0
- package/scripts/lib/doctor_core.rb +8 -3
- package/scripts/lib/engine_permissions.rb +88 -0
- package/scripts/lib/graph_edges.rb +4 -4
- package/scripts/lib/graph_file.rb +4 -4
- package/scripts/lib/graph_measure.rb +645 -0
- package/scripts/lib/graph_measure_budget.rb +408 -0
- package/scripts/lib/graph_measure_cohorts.rb +487 -0
- package/scripts/lib/graph_measure_models.rb +411 -0
- package/scripts/lib/graph_measure_report.rb +532 -0
- package/scripts/lib/graph_tree.rb +2 -2
- package/scripts/lib/handoff.rb +36 -5
- package/scripts/lib/harness_adapter.rb +184 -0
- package/scripts/lib/hook_registry.rb +13 -1
- package/scripts/lib/hook_replay.rb +23 -5
- package/scripts/lib/index_projection.rb +1 -1
- package/scripts/lib/installer_core.rb +112 -6
- package/scripts/lib/intent_screen.rb +1 -1
- package/scripts/lib/intent_validator.rb +2 -2
- package/scripts/lib/meter_watch.rb +15 -9
- package/scripts/lib/node_file.rb +3 -3
- package/scripts/lib/node_ledger.rb +8 -1
- package/scripts/lib/node_progress.rb +153 -0
- package/scripts/lib/outcome_report.rb +1 -1
- package/scripts/lib/report_screen.rb +10 -6
- package/scripts/lib/roadmap_graph.rb +1 -1
- package/scripts/lib/roadmap_queue.rb +1 -1
- package/scripts/lib/roadmap_render.rb +1 -1
- package/scripts/lib/runner_absorb.rb +31 -5
- package/scripts/lib/runner_dispatch.rb +26 -11
- package/scripts/lib/runner_until_empty.rb +252 -0
- package/scripts/lib/runner_watch.rb +389 -0
- package/scripts/lib/savepoint.rb +3 -3
- package/scripts/lib/session_git.rb +2 -2
- package/scripts/lib/stop_gate.rb +95 -0
- package/scripts/lib/verify_intent.rb +2 -2
- package/scripts/lib/work_graph_validator.rb +6 -6
- package/scripts/new-intent +1 -1
- package/scripts/node-run +224 -0
- package/scripts/read-config +6 -0
- package/scripts/runner +203 -19
- package/scripts/skill-lint +115 -6
- package/scripts/verify-intent +1 -1
- package/skills/auto/SKILL.md +54 -56
- package/skills/auto/references/agent-architecture.md +10 -8
- package/skills/auto/references/human-report-contract.md +1 -1
- package/skills/conventions/references/completion-and-done.md +7 -7
- package/skills/conventions/references/knowledge-graph.md +9 -0
- package/skills/conventions/references/locks-and-worktrees.md +3 -3
- package/skills/conventions/references/maintenance-and-revisions.md +1 -1
- package/skills/doctor/SKILL.md +3 -3
- package/skills/doctor/report.md +1 -1
- package/skills/intent-continuing/references/boarding-matrix.md +2 -2
- package/skills/intent-creating/SKILL.md +58 -133
- package/skills/intent-creating/evals/evals.json +1 -1
- package/skills/intent-ending/SKILL.md +48 -56
- package/skills/intent-ending/evals/evals.json +1 -1
- package/skills/intent-executing/SKILL.md +43 -136
- package/skills/intent-speccing/SKILL.md +3 -0
- package/skills/releasing/SKILL.md +1 -1
- package/skills/releasing/references/release-lines.md +1 -1
- package/skills/tutorial/SKILL.md +2 -1
- package/skills/tutorial/references/track-1-guided.md +21 -40
- package/skills/tutorial/references/track-2-auto.md +3 -3
- package/templates/agents.md +2 -2
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "graph_file"
|
|
5
|
+
require_relative "graph_edges"
|
|
6
|
+
require_relative "node_ledger"
|
|
7
|
+
require_relative "intent_screen"
|
|
8
|
+
|
|
9
|
+
# NodeProgress (intent 337a, n1): the graph-era progress reader every report
|
|
10
|
+
# screen's node count calls instead of counting checklist.md items. A
|
|
11
|
+
# graph-era intent (graph.md AND nodes/ both present, D4) counts declared
|
|
12
|
+
# nodes - GraphFile.parse's ## Graph section, via GraphEdges - against
|
|
13
|
+
# NodeLedger.status's last-non-torn-line-per-subject state; a checklist-era
|
|
14
|
+
# intent gets nil, so its screen keeps counting checklist.md exactly as
|
|
15
|
+
# before. No second parser: every number here comes from GraphFile,
|
|
16
|
+
# GraphEdges or NodeLedger, never a hand-rolled scan.
|
|
17
|
+
#
|
|
18
|
+
# Pure: an explicit intent_dir and store_root: in, a fields hash or nil out;
|
|
19
|
+
# no ENV, no Dir.pwd, no raise across the boundary (a malformed record
|
|
20
|
+
# answers nil or zeroed fields, matrix row 1.16).
|
|
21
|
+
module NodeProgress
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# Same keys IntentScreen.progress_fields returns, plus "progress.unit".
|
|
25
|
+
# nil for a pre-graph intent (D4), so the caller keeps counting
|
|
26
|
+
# checklist.md.
|
|
27
|
+
def fields(intent_dir, store_root: nil)
|
|
28
|
+
return nil unless graph_era?(intent_dir)
|
|
29
|
+
|
|
30
|
+
nodes = declared_nodes(File.join(intent_dir, "graph.md"))
|
|
31
|
+
return nil if nodes.nil? || nodes.empty?
|
|
32
|
+
|
|
33
|
+
status = NodeLedger.status(File.join(intent_dir, "savepoint.md"))
|
|
34
|
+
superseded = nodes.count { |n| status[n] == "superseded" }
|
|
35
|
+
abandoned = nodes.count { |n| status[n] == "abandoned" }
|
|
36
|
+
remaining = nodes.reject { |n| status[n] == "superseded" || status[n] == "abandoned" }
|
|
37
|
+
done = remaining.count { |n| status[n] == "done" }
|
|
38
|
+
running = remaining.count { |n| status[n] == "running" }
|
|
39
|
+
total = remaining.length
|
|
40
|
+
|
|
41
|
+
inferred = done.zero? && total.positive? && delivered?(intent_dir, store_root)
|
|
42
|
+
done = total if inferred
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
"progress.bar" => bar_for(done, total),
|
|
46
|
+
"progress.done" => done.to_s,
|
|
47
|
+
"progress.total" => total.to_s,
|
|
48
|
+
"progress.note" => note_for(done, total, running, inferred, superseded, abandoned),
|
|
49
|
+
"progress.unit" => "nodes",
|
|
50
|
+
}
|
|
51
|
+
rescue StandardError
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# D4: graph-era means graph.md AND nodes/ both exist.
|
|
56
|
+
def graph_era?(intent_dir)
|
|
57
|
+
File.exist?(File.join(intent_dir, "graph.md")) && Dir.exist?(File.join(intent_dir, "nodes"))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Declared node ids from the ## Graph section (GraphFile/GraphEdges), never
|
|
61
|
+
# from Dir over nodes/. nil for a missing ## Graph section or one that
|
|
62
|
+
# declares no nodes (matrix row 1.16).
|
|
63
|
+
def declared_nodes(graph_md_path)
|
|
64
|
+
parsed = GraphFile.parse(graph_md_path)
|
|
65
|
+
graph = parsed && parsed[:graph]
|
|
66
|
+
return nil unless graph
|
|
67
|
+
|
|
68
|
+
ids = graph[:nodes]
|
|
69
|
+
ids.nil? || ids.empty? ? nil : ids
|
|
70
|
+
end
|
|
71
|
+
private_class_method :declared_nodes
|
|
72
|
+
|
|
73
|
+
def bar_for(done, total)
|
|
74
|
+
width = IntentScreen::BAR_WIDTH
|
|
75
|
+
on = total.zero? ? 0 : (done * width) / total
|
|
76
|
+
(IntentScreen::ON * on) + (IntentScreen::OFF * (width - on))
|
|
77
|
+
end
|
|
78
|
+
private_class_method :bar_for
|
|
79
|
+
|
|
80
|
+
# D8's four exact note strings, plus a superseded/abandoned suffix (n6, B1):
|
|
81
|
+
# a node whose resolved state is superseded or abandoned leaves the total
|
|
82
|
+
# and the numerator, and the note names it, superseded first, appended as
|
|
83
|
+
# ", K superseded" and/or ", J abandoned". Unchanged when there are none of
|
|
84
|
+
# either. The inferred case never carries a suffix: it counts the already-
|
|
85
|
+
# reduced total, so the note stays exactly "inferred: delivered before the
|
|
86
|
+
# node ledger".
|
|
87
|
+
def note_for(done, total, running, inferred, superseded, abandoned)
|
|
88
|
+
return "inferred: delivered before the node ledger" if inferred
|
|
89
|
+
|
|
90
|
+
base =
|
|
91
|
+
if total.zero?
|
|
92
|
+
"no nodes counted"
|
|
93
|
+
else
|
|
94
|
+
open = total - done
|
|
95
|
+
if open.zero?
|
|
96
|
+
"all nodes done"
|
|
97
|
+
elsif running.positive?
|
|
98
|
+
"#{open} nodes open, #{running} running"
|
|
99
|
+
else
|
|
100
|
+
"#{open} nodes open"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
suffix = []
|
|
105
|
+
suffix << "#{superseded} superseded" if superseded.positive?
|
|
106
|
+
suffix << "#{abandoned} abandoned" if abandoned.positive?
|
|
107
|
+
suffix.empty? ? base : "#{base}, #{suffix.join(', ')}"
|
|
108
|
+
end
|
|
109
|
+
private_class_method :note_for
|
|
110
|
+
|
|
111
|
+
# Delivered, proven twice (the inferred fallback's precondition):
|
|
112
|
+
# outcome.md on disk plus either an INDEX ## Completed line (store_root:
|
|
113
|
+
# given) or a Done savepoint line (store_root: nil, the roadmap-entries
|
|
114
|
+
# path that pays for no INDEX read per entry).
|
|
115
|
+
def delivered?(intent_dir, store_root)
|
|
116
|
+
return false unless File.exist?(File.join(intent_dir, "outcome.md"))
|
|
117
|
+
|
|
118
|
+
store_root ? index_completed?(store_root, intent_id(intent_dir)) : done_savepoint_line?(intent_dir)
|
|
119
|
+
end
|
|
120
|
+
private_class_method :delivered?
|
|
121
|
+
|
|
122
|
+
def intent_id(intent_dir)
|
|
123
|
+
File.basename(intent_dir).split("--", 2).first
|
|
124
|
+
end
|
|
125
|
+
private_class_method :intent_id
|
|
126
|
+
|
|
127
|
+
def index_completed?(store_root, id)
|
|
128
|
+
path = File.join(store_root, "INDEX.md")
|
|
129
|
+
return false unless File.exist?(path)
|
|
130
|
+
|
|
131
|
+
content = File.read(path).scrub
|
|
132
|
+
section = content[/^## Completed\n(.*?)(?=^## |\z)/m, 1]
|
|
133
|
+
return false unless section
|
|
134
|
+
|
|
135
|
+
section.each_line.any? { |line| line.strip.match?(/\A-\s*\[#{Regexp.escape(id)}(?=[\s\]])/) }
|
|
136
|
+
end
|
|
137
|
+
private_class_method :index_completed?
|
|
138
|
+
|
|
139
|
+
# Only a Done line whose text begins with "delivered" proves delivered (n6,
|
|
140
|
+
# B3): end-intent writes "Done delivered" or "Done abandoned"
|
|
141
|
+
# (scripts/end-intent, Savepoint.append_terminal_savepoint), and only the
|
|
142
|
+
# first disposition is delivery.
|
|
143
|
+
def done_savepoint_line?(intent_dir)
|
|
144
|
+
path = File.join(intent_dir, "savepoint.md")
|
|
145
|
+
return false unless File.exist?(path)
|
|
146
|
+
|
|
147
|
+
File.read(path).scrub.each_line.any? do |line|
|
|
148
|
+
m = line.strip.match(IntentScreen::SAVEPOINT_RE)
|
|
149
|
+
m && m[2] == "Done" && m[3].to_s.start_with?("delivered")
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
private_class_method :done_savepoint_line?
|
|
153
|
+
end
|
|
@@ -377,7 +377,7 @@ module OutcomeReport
|
|
|
377
377
|
# --- n4: findings, read and capped ------------------------------------------
|
|
378
378
|
|
|
379
379
|
# Two lines at the screens' own 115-column limit (spec D15). A named
|
|
380
|
-
# constant so the number is never
|
|
380
|
+
# constant so the number is never embedded into a regex.
|
|
381
381
|
FINDING_CAP = 200
|
|
382
382
|
|
|
383
383
|
# `### Findings` under the intent record's own `## Insights` section (spec
|
|
@@ -21,6 +21,7 @@ require_relative "graph_tree"
|
|
|
21
21
|
require_relative "screen_paint"
|
|
22
22
|
require_relative "outcome_report"
|
|
23
23
|
require_relative "node_file"
|
|
24
|
+
require_relative "node_progress"
|
|
24
25
|
|
|
25
26
|
module ReportScreen
|
|
26
27
|
NOT_RECORDED = "not recorded"
|
|
@@ -920,7 +921,7 @@ def self.matching_action_heading(intent_dir, label)
|
|
|
920
921
|
f["status.note"] = status == "unlisted" ? "no INDEX.md line names this id" : "listed under ## #{status} in INDEX.md"
|
|
921
922
|
f.merge!(IntentScreen.savepoint_fields(intent_dir, text.to_s))
|
|
922
923
|
items = IntentScreen.checklist_items(intent_dir)
|
|
923
|
-
f.merge!(IntentScreen.progress_fields(items))
|
|
924
|
+
f.merge!(NodeProgress.fields(intent_dir, store_root: store_root) || IntentScreen.progress_fields(items))
|
|
924
925
|
f.merge!(IntentScreen.next_fields(items, status, checklist_present: IntentScreen.items_present?(intent_dir)))
|
|
925
926
|
f.merge!(IntentScreen.insight_fields(text.to_s))
|
|
926
927
|
|
|
@@ -931,7 +932,7 @@ def self.matching_action_heading(intent_dir, label)
|
|
|
931
932
|
["Status", f["status"], f["status.note"]],
|
|
932
933
|
["Stage", f["stage"], f["stage.note"]],
|
|
933
934
|
["Savepoint", f["savepoint"], f["savepoint.note"]],
|
|
934
|
-
["Progress", "#{f['progress.bar']} #{f['progress.done']} / #{f['progress.total']}", f["progress.note"]],
|
|
935
|
+
["Progress", "#{f['progress.bar']} #{f['progress.done']} / #{f['progress.total']}#{f['progress.unit'] ? " #{f['progress.unit']}" : ''}", f["progress.note"]],
|
|
935
936
|
["Next", f["next"], f["next.note"]],
|
|
936
937
|
["Insight", f["insight"], f["insight.note"]],
|
|
937
938
|
["Changed", changed_value, CHANGED_NOTE],
|
|
@@ -1101,9 +1102,10 @@ def self.matching_action_heading(intent_dir, label)
|
|
|
1101
1102
|
text = intent_text(e[:dir]).to_s
|
|
1102
1103
|
savepoint = IntentScreen.savepoint_fields(e[:dir], text)
|
|
1103
1104
|
items = IntentScreen.checklist_items(e[:dir])
|
|
1104
|
-
progress = IntentScreen.progress_fields(items)
|
|
1105
|
+
progress = NodeProgress.fields(e[:dir], store_root: store_root) || IntentScreen.progress_fields(items)
|
|
1106
|
+
unit = progress["progress.unit"] ? " #{progress['progress.unit']}" : ""
|
|
1105
1107
|
ch = state_fields(intent_dir: e[:dir], store_root: store_root, changed: changed)[:rows].find { |l, _, _| l == "Changed" }[1]
|
|
1106
|
-
table << "| #{e[:id]} | #{savepoint['stage']} | #{progress['progress.bar']} #{progress['progress.done']} / #{progress['progress.total']} | #{escape(ch)} | #{lead(e[:dir], now: now)} |"
|
|
1108
|
+
table << "| #{e[:id]} | #{savepoint['stage']} | #{progress['progress.bar']} #{progress['progress.done']} / #{progress['progress.total']}#{unit} | #{escape(ch)} | #{lead(e[:dir], now: now)} |"
|
|
1107
1109
|
end
|
|
1108
1110
|
blocks = entries.map { |e| render_collapsed_block(e[:dir], store_root, changed: changed) }
|
|
1109
1111
|
head_and_table = ([header, ""] + table).join("\n")
|
|
@@ -1721,8 +1723,10 @@ def self.matching_action_heading(intent_dir, label)
|
|
|
1721
1723
|
def self.roadmap_entry_progress(dir)
|
|
1722
1724
|
return NOT_RECORDED unless dir
|
|
1723
1725
|
items = IntentScreen.checklist_items(dir)
|
|
1724
|
-
fields = IntentScreen.progress_fields(items)
|
|
1725
|
-
|
|
1726
|
+
fields = NodeProgress.fields(dir) || IntentScreen.progress_fields(items)
|
|
1727
|
+
unit = fields["progress.unit"] ? " #{fields['progress.unit']}" : ""
|
|
1728
|
+
inferred = fields["progress.note"].to_s.start_with?("inferred") ? " inferred" : ""
|
|
1729
|
+
"#{fields['progress.bar']} #{fields['progress.done']} / #{fields['progress.total']}#{unit}#{inferred}"
|
|
1726
1730
|
end
|
|
1727
1731
|
|
|
1728
1732
|
def self.roadmap_progress_bar(done, total)
|
|
@@ -51,7 +51,7 @@ module RoadmapGraph
|
|
|
51
51
|
entry_ids = order.dup
|
|
52
52
|
|
|
53
53
|
# 1.6: an entry the graph never named (not declared, not targeted) is
|
|
54
|
-
#
|
|
54
|
+
# included as a root, so a partly migrated roadmap never silently
|
|
55
55
|
# drops real work from the batches.
|
|
56
56
|
edges = parsed_edges[:edges].dup
|
|
57
57
|
entry_ids.each do |id|
|
|
@@ -306,7 +306,7 @@ class RoadmapQueue
|
|
|
306
306
|
# and never an invented dispatchable entry with no title. An id a batch
|
|
307
307
|
# lists that the graph does not name is never dropped either: it keeps
|
|
308
308
|
# the pre-change wave behavior (the G4 partial-migration path) by being
|
|
309
|
-
#
|
|
309
|
+
# included as needing nothing (never called on a cyclic graph: #analyze
|
|
310
310
|
# checks that first and reports "error" instead).
|
|
311
311
|
def graph_frontier_for(candidate)
|
|
312
312
|
edges = candidate[:graph_edges][:edges].dup
|
|
@@ -14,7 +14,7 @@ require_relative "atomic_write"
|
|
|
14
14
|
# byte of the file alone.
|
|
15
15
|
#
|
|
16
16
|
# The regroup is surgical, never a rebuild from parsed fields (327's own
|
|
17
|
-
# lesson,
|
|
17
|
+
# lesson, merged at the 2026-09-10 plan review, rows 3.2/3.13/3.14): only
|
|
18
18
|
# entry lines RoadmapGraph itself recognized (the canonical checkbox +
|
|
19
19
|
# status grammar) ever move. A line the grammar cannot parse, and any prose
|
|
20
20
|
# a human wrote between batch headings, is untouched OUTPUT text - it is
|
|
@@ -86,6 +86,17 @@ module RunnerAbsorb
|
|
|
86
86
|
return refused("node_not_running") unless current_state == "running"
|
|
87
87
|
|
|
88
88
|
holder = holder_for(entries, node)
|
|
89
|
+
# 340b n8: the harness that ran this attempt, read off the node's own
|
|
90
|
+
# `running` line - never the harness of the session doing the absorbing
|
|
91
|
+
# (row 8.12). `extra_fields` carries it into every terminal fields hash
|
|
92
|
+
# below through the same `.merge(extra_fields)` every path already uses,
|
|
93
|
+
# so it reaches `done`, every `failed_verification`/`needs_decision`, and
|
|
94
|
+
# `blocked` alike; a running line with no `harness=` leaves it out of
|
|
95
|
+
# `extra_fields` entirely, so nothing invents a value (row 8.13).
|
|
96
|
+
harness = harness_for(entries, node)
|
|
97
|
+
extra_fields = {}
|
|
98
|
+
extra_fields[:harness] = harness if harness
|
|
99
|
+
|
|
89
100
|
node_decl = ((context.graph || {})[:nodes] || {})[node] || {}
|
|
90
101
|
kind = node_decl[:kind]
|
|
91
102
|
declared_files = normalize_files(node_decl[:files])
|
|
@@ -97,12 +108,11 @@ module RunnerAbsorb
|
|
|
97
108
|
# empty), and land `done` carrying the executor's own self-reported
|
|
98
109
|
# commit on work nothing had verified ever reached the intent branch.
|
|
99
110
|
unless (context.graph || {})[:ok] && !kind.nil?
|
|
100
|
-
fields = { reason: "invalid_graph", holder: holder }
|
|
111
|
+
fields = { reason: "invalid_graph", holder: holder }.merge(extra_fields)
|
|
101
112
|
return write_transition(savepoint_path, context, node, "blocked", fields, now: now, ledger: ledger)
|
|
102
113
|
end
|
|
103
114
|
|
|
104
115
|
checks_ran = []
|
|
105
|
-
extra_fields = {}
|
|
106
116
|
|
|
107
117
|
# 1. integrity ------------------------------------------------------------
|
|
108
118
|
checks_ran << "integrity"
|
|
@@ -112,7 +122,7 @@ module RunnerAbsorb
|
|
|
112
122
|
extra_fields["allow_core_drift"] = "true"
|
|
113
123
|
else
|
|
114
124
|
fields = { reason: "core_integrity", core_drift: drift_summary(integrity), holder: holder,
|
|
115
|
-
gates: checks_ran.join("+") }
|
|
125
|
+
gates: checks_ran.join("+") }.merge(extra_fields)
|
|
116
126
|
return write_transition(savepoint_path, context, node, "blocked", fields, now: now, ledger: ledger)
|
|
117
127
|
end
|
|
118
128
|
end
|
|
@@ -301,7 +311,13 @@ module RunnerAbsorb
|
|
|
301
311
|
{ state: "append_failed", written: false, fields: fields, gates: fields[:gates],
|
|
302
312
|
commit: fields[:commit], error: e.message }
|
|
303
313
|
rescue ArgumentError => e
|
|
304
|
-
|
|
314
|
+
# 340b n8, row 8.11: this fallback still merges no caller `extra_fields`
|
|
315
|
+
# (unchanged from before this node), but `fields[:harness]` is read
|
|
316
|
+
# straight off the fields hash the failed write attempted, which already
|
|
317
|
+
# carries `harness` whenever the caller's own `extra_fields` had it -
|
|
318
|
+
# `.compact` drops the key rather than writing it empty when it did not.
|
|
319
|
+
fallback_fields = { reason: "return_unwritable", holder: fields[:holder], gates: fields[:gates],
|
|
320
|
+
harness: fields[:harness] }.compact
|
|
305
321
|
fallback = begin
|
|
306
322
|
ledger.append_transition(savepoint_path, subject: node, state: "blocked", fields: fallback_fields, now: now)
|
|
307
323
|
rescue GuardedAppend::Unavailable
|
|
@@ -352,6 +368,16 @@ module RunnerAbsorb
|
|
|
352
368
|
end
|
|
353
369
|
private_class_method :holder_for
|
|
354
370
|
|
|
371
|
+
# 340b n8/row 8.12: mirrors holder_for exactly - the value on the node's
|
|
372
|
+
# OWN last `running` line, never a fresh resolve of the current session's
|
|
373
|
+
# own harness. nil when that line carried none (row 8.13), which is what
|
|
374
|
+
# keeps `extra_fields` from ever inventing the key.
|
|
375
|
+
def harness_for(entries, node)
|
|
376
|
+
last = entries.select { |e| !e[:torn] && e[:subject] == node && e[:state] == "running" }.last
|
|
377
|
+
last && (last[:fields] || {})["harness"]
|
|
378
|
+
end
|
|
379
|
+
private_class_method :harness_for
|
|
380
|
+
|
|
355
381
|
def drift_summary(integrity)
|
|
356
382
|
parts = []
|
|
357
383
|
parts << "drifted:#{integrity[:drifted].join(',')}" if Array(integrity[:drifted]).any?
|
|
@@ -519,7 +545,7 @@ module RunnerAbsorb
|
|
|
519
545
|
# `content` itself ends in a newline (the file's own trailing newline
|
|
520
546
|
# made visible as an array slot). When Insights is the LAST section (no
|
|
521
547
|
# further "## " heading), `insights_end` walks all the way to
|
|
522
|
-
# `lines.length`,
|
|
548
|
+
# `lines.length`, merging that sentinel INTO the section - the very next
|
|
523
549
|
# insert then landed AFTER it, turning the file's trailing newline into
|
|
524
550
|
# a spurious blank line and leaving the new bullet as the final element
|
|
525
551
|
# with no newline of its own. Excluding the sentinel here restores both:
|
|
@@ -14,6 +14,7 @@ require_relative "runner_policy"
|
|
|
14
14
|
require_relative "worktree"
|
|
15
15
|
require_relative "savepoint"
|
|
16
16
|
require_relative "guarded_append"
|
|
17
|
+
require_relative "harness_adapter"
|
|
17
18
|
|
|
18
19
|
# RunnerDispatch (intent 340, G7, n5): validates the graph, computes the
|
|
19
20
|
# ready set, applies RunnerPolicy, mints leases, builds packets, writes
|
|
@@ -65,16 +66,25 @@ module RunnerDispatch
|
|
|
65
66
|
# :errors, :rearm_command, :dispatched, :stop, :parked, :status, :blockers,
|
|
66
67
|
# :plan - fields that do not apply to a given outcome stay nil/empty rather
|
|
67
68
|
# than being omitted, so a caller never has to guard with `dig`.
|
|
68
|
-
def dispatch(context, limit: DEFAULT_LIMIT, now: Time.now, config: {}, caps: ReadySet::DEFAULT_CAPS,
|
|
69
|
+
def dispatch(context, limit: DEFAULT_LIMIT, now: Time.now, config: {}, harness: nil, caps: ReadySet::DEFAULT_CAPS,
|
|
69
70
|
validator: WorkGraphValidator.method(:validate),
|
|
70
71
|
ready_analyzer: ReadySet.method(:analyze),
|
|
71
72
|
packet_builder: NodePacket.method(:build),
|
|
72
73
|
worktree: NodeWorktree,
|
|
73
74
|
ledger: NodeLedger,
|
|
74
|
-
runner: Worktree::ShellRunner.new
|
|
75
|
+
runner: Worktree::ShellRunner.new,
|
|
76
|
+
harness_adapter: HarnessAdapter)
|
|
75
77
|
intent_dir = context.intent_dir
|
|
76
78
|
savepoint_path = File.join(intent_dir.to_s, "savepoint.md")
|
|
77
79
|
|
|
80
|
+
# Intent 340b, G7c, n1, rows 1.19/1.20/D21: resolved ONCE for the whole
|
|
81
|
+
# step through HarnessAdapter, never a literal - `harness:` (this call's
|
|
82
|
+
# `--harness` override, or nil) wins over `config`'s own `agent.type`.
|
|
83
|
+
# Every node this call dispatches carries the SAME value, and the
|
|
84
|
+
# caller (scripts/runner) reads it back off the report to pick which
|
|
85
|
+
# harness's block to render.
|
|
86
|
+
harness_key = harness_adapter.resolve_key(config: config, override: harness)
|
|
87
|
+
|
|
78
88
|
# Row 5.31/5.32: this is RunnerDispatch's OWN lock check, never a shelled
|
|
79
89
|
# `node-transition` call - append_transition below is used in-process
|
|
80
90
|
# (RunnerAbsorb's own pattern), so nothing here inherits node-transition's
|
|
@@ -161,8 +171,8 @@ module RunnerDispatch
|
|
|
161
171
|
next
|
|
162
172
|
end
|
|
163
173
|
|
|
164
|
-
result = dispatch_one(context, node: node, kind: kind, now: now, config: config,
|
|
165
|
-
edges: edges, nodes_decl: nodes_decl, packet_builder: packet_builder,
|
|
174
|
+
result = dispatch_one(context, node: node, kind: kind, now: now, config: config, harness: harness_key,
|
|
175
|
+
caps: caps, edges: edges, nodes_decl: nodes_decl, packet_builder: packet_builder,
|
|
166
176
|
worktree: worktree, ledger: ledger, runner: runner)
|
|
167
177
|
if result[:packet_build_failed]
|
|
168
178
|
packet_failures << result
|
|
@@ -182,7 +192,8 @@ module RunnerDispatch
|
|
|
182
192
|
RunnerCore.render_status(context) if dispatched.any? || stop || parked.any?
|
|
183
193
|
|
|
184
194
|
build_report(context: context, dispatched: dispatched, stop: stop, parked: parked,
|
|
185
|
-
ceiling_blocked: ceiling_blocked, packet_failures: packet_failures, running_count: running_count
|
|
195
|
+
ceiling_blocked: ceiling_blocked, packet_failures: packet_failures, running_count: running_count,
|
|
196
|
+
harness: harness_key)
|
|
186
197
|
end
|
|
187
198
|
|
|
188
199
|
# --- guarded re-entries into graph.md (M9) ----------------------------------
|
|
@@ -203,8 +214,8 @@ module RunnerDispatch
|
|
|
203
214
|
|
|
204
215
|
# --- one node's whole dispatch (packet, lease, `running`) -------------------
|
|
205
216
|
|
|
206
|
-
def dispatch_one(context, node:, kind:, now:, config:, caps:, edges:, nodes_decl:, packet_builder:,
|
|
207
|
-
ledger:, runner:)
|
|
217
|
+
def dispatch_one(context, node:, kind:, now:, config:, harness:, caps:, edges:, nodes_decl:, packet_builder:,
|
|
218
|
+
worktree:, ledger:, runner:)
|
|
208
219
|
intent_dir = context.intent_dir
|
|
209
220
|
savepoint_path = File.join(intent_dir.to_s, "savepoint.md")
|
|
210
221
|
|
|
@@ -253,7 +264,11 @@ module RunnerDispatch
|
|
|
253
264
|
precondition = lambda do |c|
|
|
254
265
|
ReadySet.ready?(content: c, subject: node, graph: { edges: edges }, nodes: nodes_decl, caps: caps)[:ready]
|
|
255
266
|
end
|
|
256
|
-
|
|
267
|
+
# Row 1.19/1.20/D21: harness= rides alongside model= on every `running`
|
|
268
|
+
# line, resolved once by the caller through HarnessAdapter and threaded
|
|
269
|
+
# straight through here - never re-resolved, never a literal.
|
|
270
|
+
fields = { holder: holder, expires: expires, packet: build_result[:sha], model: model, harness: harness,
|
|
271
|
+
calls: calls_cap }
|
|
257
272
|
|
|
258
273
|
result = begin
|
|
259
274
|
ledger.append_transition(savepoint_path, subject: node, state: "running", fields: fields, now: now,
|
|
@@ -400,7 +415,7 @@ module RunnerDispatch
|
|
|
400
415
|
|
|
401
416
|
def empty_result
|
|
402
417
|
{ ok: true, reason: nil, errors: [], rearm_command: nil, dispatched: [], stop: nil, parked: [],
|
|
403
|
-
status: nil, blockers: [], plan: nil }
|
|
418
|
+
status: nil, blockers: [], plan: nil, harness: nil }
|
|
404
419
|
end
|
|
405
420
|
private_class_method :empty_result
|
|
406
421
|
|
|
@@ -415,9 +430,9 @@ module RunnerDispatch
|
|
|
415
430
|
private_class_method :invalid_graph_result
|
|
416
431
|
|
|
417
432
|
def build_report(context:, dispatched:, stop:, parked:, ceiling_blocked: false, packet_failures: [],
|
|
418
|
-
running_count: 0)
|
|
433
|
+
running_count: 0, harness: nil)
|
|
419
434
|
base = empty_result.merge(dispatched: dispatched, stop: stop, parked: parked,
|
|
420
|
-
plan: render_plan(dispatched))
|
|
435
|
+
plan: render_plan(dispatched), harness: harness)
|
|
421
436
|
|
|
422
437
|
if dispatched.any?
|
|
423
438
|
base.merge(status: "dispatched")
|