@zalom/plastic 2.0.0-alpha.2 → 2.0.0-alpha.20
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/agents/plastic-enforcer.md +3 -1
- package/agents/plastic-executor.md +9 -3
- package/hooks/hooks.json +12 -0
- package/hooks/message-display +134 -0
- package/hooks/statusline +4 -27
- package/package.json +2 -2
- package/scripts/agent-report +8 -2
- package/scripts/dashboard.rb +297 -18
- package/scripts/doctor.rb +652 -5
- package/scripts/end-intent +219 -0
- package/scripts/hook-capture +25 -113
- package/scripts/hook-message-display +81 -0
- package/scripts/hook-record +12 -4
- package/scripts/hook-session-start +23 -1
- package/scripts/index-projection +74 -0
- package/scripts/intent-screen +77 -0
- package/scripts/lib/action_graph_shim.rb +277 -0
- package/scripts/lib/arm.rb +26 -1
- package/scripts/lib/atomic_write.rb +31 -0
- package/scripts/lib/core_integrity.rb +71 -0
- package/scripts/lib/dashboard_screen.rb +40 -0
- package/scripts/lib/day_summary.rb +7 -2
- package/scripts/lib/doctor_core.rb +101 -5
- package/scripts/lib/doctor_session_ledger.rb +52 -0
- package/scripts/lib/graph_edges.rb +121 -0
- package/scripts/lib/graph_file.rb +246 -0
- package/scripts/lib/graph_tree.rb +98 -0
- package/scripts/lib/guarded_append.rb +155 -0
- package/scripts/lib/hook_registry.rb +14 -0
- package/scripts/lib/hook_replay.rb +211 -0
- package/scripts/lib/index_projection.rb +201 -0
- package/scripts/lib/installer_core.rb +131 -4
- package/scripts/lib/intent_screen.rb +309 -0
- package/scripts/lib/intent_screen_ansi.rb +262 -0
- package/scripts/lib/message_display.rb +586 -0
- package/scripts/lib/node_file.rb +214 -0
- package/scripts/lib/node_ids.rb +99 -0
- package/scripts/lib/node_ledger.rb +377 -0
- package/scripts/lib/node_packet.rb +886 -0
- package/scripts/lib/node_return.rb +199 -0
- package/scripts/lib/node_worktree.rb +337 -0
- package/scripts/lib/outcome_report.rb +440 -0
- package/scripts/lib/packet_wrapper.rb +132 -0
- package/scripts/lib/ready_set.rb +462 -0
- package/scripts/lib/release_guard.rb +16 -0
- package/scripts/lib/report_screen.rb +1967 -0
- package/scripts/lib/roadmap_graph.rb +210 -0
- package/scripts/lib/roadmap_migration.rb +95 -0
- package/scripts/lib/roadmap_queue.rb +155 -5
- package/scripts/lib/roadmap_render.rb +150 -0
- package/scripts/lib/roadmap_savepoint.rb +62 -12
- package/scripts/lib/runner_absorb.rb +620 -0
- package/scripts/lib/runner_answer.rb +206 -0
- package/scripts/lib/runner_core.rb +194 -0
- package/scripts/lib/runner_dispatch.rb +482 -0
- package/scripts/lib/runner_policy.rb +142 -0
- package/scripts/lib/runner_proposals.rb +254 -0
- package/scripts/lib/runner_rewind.rb +201 -0
- package/scripts/lib/runner_sweep.rb +231 -0
- package/scripts/lib/savepoint.rb +149 -12
- package/scripts/lib/screen_paint.rb +555 -0
- package/scripts/lib/screens/dashboard.rb +20 -0
- package/scripts/lib/screens/plan.rb +18 -0
- package/scripts/lib/screens/roadmap.rb +15 -0
- package/scripts/lib/session_git.rb +49 -18
- package/scripts/lib/session_ledger.rb +128 -0
- package/scripts/lib/verify_intent.rb +33 -0
- package/scripts/lib/work_graph_validator.rb +201 -0
- package/scripts/node-packet +92 -0
- package/scripts/node-transition +291 -0
- package/scripts/outcome-report +74 -0
- package/scripts/plastic-lock +8 -1
- package/scripts/ready-set +126 -0
- package/scripts/release-check +118 -0
- package/scripts/report-screen +281 -0
- package/scripts/roadmap-graph +119 -0
- package/scripts/roadmap-savepoint +7 -0
- package/scripts/runner +392 -0
- package/scripts/savepoint-note +69 -0
- package/scripts/spawn-preamble +9 -2
- package/scripts/validate-work-graph +39 -0
- package/scripts/verify-intent +2 -1
- package/skills/auto/SKILL.md +24 -17
- package/skills/auto/references/human-report-contract.md +136 -54
- package/skills/conventions/references/locks-and-worktrees.md +12 -0
- package/skills/dashboard/SKILL.md +13 -2
- package/skills/dashboard/templates/dashboard-global.md +1 -1
- package/skills/dashboard/templates/dashboard-project.md +2 -2
- package/skills/doctor/SKILL.md +10 -4
- package/skills/intent-continuing/SKILL.md +51 -41
- package/skills/intent-continuing/references/board-fill.md +9 -0
- package/skills/intent-continuing/references/boarding-matrix.md +6 -5
- package/skills/intent-continuing/references/context-management.md +1 -1
- package/skills/intent-ending/SKILL.md +36 -16
- package/skills/intent-executing/SKILL.md +17 -5
- package/skills/intent-executing/implementer-prompt.md +6 -1
- package/skills/intent-speccing/SKILL.md +7 -4
- package/skills/releasing/SKILL.md +39 -0
- package/skills/releasing/references/promotion-and-tagging.md +10 -6
- package/skills/releasing/references/release-lines.md +1 -1
- package/skills/roadmap/SKILL.md +26 -0
- package/skills/roadmap/references/file-format.md +10 -0
- package/templates/dashboard-screen.md +22 -0
- package/templates/display-fixture.md +21 -0
- package/templates/graph.md +16 -0
- package/templates/intent-screen.md +17 -0
- package/templates/node-decision.md +11 -0
- package/templates/node-research.md +11 -0
- package/templates/node-verify.md +13 -0
- package/templates/node-work.md +22 -0
- package/templates/outcome.md +19 -1
- package/templates/report-plan.md +15 -0
- package/templates/report-roadmap-delivered.md +10 -0
- package/templates/report-roadmap-plan.md +9 -0
- package/templates/report-roadmap-state.md +9 -0
- package/templates/report-state.md +11 -0
- package/templates/roadmap.md +13 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "intent_screen"
|
|
5
|
+
|
|
6
|
+
# IntentScreenAnsi (intent 316a, O2) - renders one intent screen with raw
|
|
7
|
+
# truecolor ANSI escapes, productionizing 318's mockup--render.rb. Standard
|
|
8
|
+
# library only. Calls the SAME public IntentScreen.* field methods
|
|
9
|
+
# scripts/intent-screen calls (store_fields, index_fields, savepoint_fields,
|
|
10
|
+
# checklist_items, progress_fields, next_fields, insight_fields,
|
|
11
|
+
# items_present?, fallback_name, step_text) and re-derives nothing, so every
|
|
12
|
+
# field the ANSI block prints is the identical value the plain screen prints
|
|
13
|
+
# (D3), just carried through a different layout.
|
|
14
|
+
#
|
|
15
|
+
# `color:` is a constructor/call argument, never an environment read (D18):
|
|
16
|
+
# the plain path (`color: false`) is one call away and testable without
|
|
17
|
+
# touching NO_COLOR or a TTY. No ENV, no Dir.pwd, no Dir.home.
|
|
18
|
+
#
|
|
19
|
+
# Harness-agnostic core: no harness assumption lives here. `markdown_safe:`
|
|
20
|
+
# (intent 316a1, D3/D5) is the one choice a caller supplies rather than a
|
|
21
|
+
# choice this module makes for itself: a display surface that passes raw
|
|
22
|
+
# ANSI through untouched should not inherit a concession it never needed.
|
|
23
|
+
# See docs/reference/harness-adapters.md for which caller asks for it and
|
|
24
|
+
# why.
|
|
25
|
+
module IntentScreenAnsi
|
|
26
|
+
ESC = "\e"
|
|
27
|
+
RESET = "#{ESC}[0m".freeze
|
|
28
|
+
BOLD = "#{ESC}[1m".freeze
|
|
29
|
+
TEAL = "#{ESC}[38;2;45;212;191m".freeze
|
|
30
|
+
AMBER = "#{ESC}[38;2;245;158;11m".freeze
|
|
31
|
+
GRAPHITE_BG = "#{ESC}[48;2;31;41;55m".freeze
|
|
32
|
+
MIDGREY = "#{ESC}[38;2;148;163;184m".freeze
|
|
33
|
+
NEARWHITE = "#{ESC}[38;2;243;244;246m".freeze
|
|
34
|
+
|
|
35
|
+
BAR_CELLS = 24
|
|
36
|
+
EIGHTHS = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"].freeze
|
|
37
|
+
|
|
38
|
+
# Intent 317a1 (D14): the approved design is a 115-column layout, measured
|
|
39
|
+
# from design--terminal-output.html:52-58 with its tags stripped.
|
|
40
|
+
DEFAULT_WIDTH = 115
|
|
41
|
+
|
|
42
|
+
ELLIPSIS = "…"
|
|
43
|
+
|
|
44
|
+
# Intent 317a1 (D9-D11, D15): the three-column field table's geometry.
|
|
45
|
+
# ANSI_RE strips escapes so padding measures what the terminal actually
|
|
46
|
+
# draws (D10); VALUE_COL_MAX is the design's own value-column figure and
|
|
47
|
+
# the owner's "50 chars" (mockup--report-screen.md:74); NOTE_FLOOR is the
|
|
48
|
+
# minimum note length that earns the own-line fallback when the note alone
|
|
49
|
+
# (not the value) is what overflows the same-line budget.
|
|
50
|
+
ANSI_RE = /\e\[[0-9;]*m/.freeze
|
|
51
|
+
VALUE_COL_MAX = 50
|
|
52
|
+
NOTE_FLOOR = 24
|
|
53
|
+
|
|
54
|
+
def self.visible_width(text)
|
|
55
|
+
text.to_s.gsub(ANSI_RE, "").length
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.render(intent_dir:, store_root:, color: true, width: DEFAULT_WIDTH, markdown_safe: false)
|
|
59
|
+
base = File.basename(intent_dir)
|
|
60
|
+
id = base.split("--", 2).first
|
|
61
|
+
intent_text = File.read(File.join(intent_dir, "#{base}.md"))
|
|
62
|
+
|
|
63
|
+
fields = {}
|
|
64
|
+
fields.merge!(IntentScreen.store_fields(store_root))
|
|
65
|
+
status, title = IntentScreen.index_fields(store_root, id)
|
|
66
|
+
fields["status"] = status
|
|
67
|
+
fields["status.note"] = status == "unlisted" ? "no INDEX.md line names this id" : "listed under ## #{status} in INDEX.md"
|
|
68
|
+
fields["id"] = id
|
|
69
|
+
fields["name"] = title || IntentScreen.fallback_name(intent_text)
|
|
70
|
+
fields.merge!(IntentScreen.savepoint_fields(intent_dir, intent_text))
|
|
71
|
+
items = IntentScreen.checklist_items(intent_dir)
|
|
72
|
+
fields.merge!(IntentScreen.progress_fields(items))
|
|
73
|
+
fields.merge!(IntentScreen.next_fields(items, status, checklist_present: IntentScreen.items_present?(intent_dir), escape_pipes: false))
|
|
74
|
+
fields.merge!(IntentScreen.insight_fields(intent_text, escape_pipes: false))
|
|
75
|
+
fields.transform_values! { |v| markdown_safe ? clean(v) : v }
|
|
76
|
+
|
|
77
|
+
done_n = fields["progress.done"].to_i
|
|
78
|
+
total_n = fields["progress.total"].to_i
|
|
79
|
+
|
|
80
|
+
out = +""
|
|
81
|
+
out << fit("▶ #{fields['id']} · #{fields['name']}", width) { |t| styled(t, color, BOLD, NEARWHITE) }
|
|
82
|
+
out << "\n\n"
|
|
83
|
+
|
|
84
|
+
# The 4th column marks a row whose value is already a finished, pre-fit
|
|
85
|
+
# string (the Progress bar, built above from styled glyphs plus a count)
|
|
86
|
+
# rather than raw field text still needing `fit_plain`. Naming that
|
|
87
|
+
# explicitly here reads better than testing the value for a leading ESC
|
|
88
|
+
# byte further down, which is really just asking "is this the Progress
|
|
89
|
+
# row?" through a type check.
|
|
90
|
+
field_rows = [
|
|
91
|
+
["Store", fields["store"], fields["store.note"], false],
|
|
92
|
+
["Status", fields["status"], fields["status.note"], false],
|
|
93
|
+
["Stage", fields["stage"], fields["stage.note"], false],
|
|
94
|
+
["Savepoint", fields["savepoint"], fields["savepoint.note"], false],
|
|
95
|
+
["Progress", "#{render_bar(done_n, total_n, color)} #{done_n} / #{total_n}", fields["progress.note"], true],
|
|
96
|
+
["Next", fields["next"], fields["next.note"], false],
|
|
97
|
+
["Insight", fields["insight"], fields["insight.note"], false],
|
|
98
|
+
]
|
|
99
|
+
key_width = field_rows.map { |k, _, _, _| k.length }.max
|
|
100
|
+
|
|
101
|
+
out << field_table_lines(field_rows, width: width, color: color, key_width: key_width)
|
|
102
|
+
|
|
103
|
+
out << "\n"
|
|
104
|
+
out << fit("Steps", width) { |t| styled(t, color, BOLD, NEARWHITE) }
|
|
105
|
+
out << "\n\n"
|
|
106
|
+
|
|
107
|
+
if items.empty?
|
|
108
|
+
out << " no steps yet\n"
|
|
109
|
+
else
|
|
110
|
+
# Padded to the widest label (matrix B2): at 10+ steps "S10" is one
|
|
111
|
+
# column wider than "S1..S9", and without padding every badge past S9
|
|
112
|
+
# drifts out of column with the rows above it.
|
|
113
|
+
label_width = "S#{items.size}".length
|
|
114
|
+
items.each_with_index do |item, i|
|
|
115
|
+
num = "S#{i + 1}".ljust(label_width)
|
|
116
|
+
badge = status_cell(item[:done], color)
|
|
117
|
+
prefix_plain = " #{num} [ #{item[:done] ? 'done' : 'open'} ] "
|
|
118
|
+
text_budget = [width - prefix_plain.length, 0].max
|
|
119
|
+
step = IntentScreen.step_text(item[:text])
|
|
120
|
+
text = fit_plain(markdown_safe ? clean(step) : step, text_budget)
|
|
121
|
+
out << " #{num} [#{badge}] #{text}\n"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
out
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# --- shared field-table geometry (D9-D12, D14, D15) ------------------------
|
|
129
|
+
|
|
130
|
+
# The field table's basis, cap, budget arithmetic, both fallbacks, and the
|
|
131
|
+
# right-strip, shared by `render` above and `ScreenPaint.paint_table`'s
|
|
132
|
+
# `**Key**` branch, so the two renderers can never drift apart (D12) - the
|
|
133
|
+
# duplication this replaced was character-for-character identical apart
|
|
134
|
+
# from the module prefix. `rows` is `[key, value, note]`, or `[key, value,
|
|
135
|
+
# note, true]` when `value` is already a finished, pre-fit string (the
|
|
136
|
+
# Progress bar) rather than raw text still needing `fit_plain`.
|
|
137
|
+
#
|
|
138
|
+
# Notes become a third column, padded to the widest RENDERED noted value
|
|
139
|
+
# (capped at VALUE_COL_MAX, D15) so every note starts at one raw-text
|
|
140
|
+
# position; a row whose value or note will not fit drops to the
|
|
141
|
+
# note-on-its-own-line form instead of squeezing anything invisibly. When
|
|
142
|
+
# the same-line budget has already collapsed to zero or less, the note
|
|
143
|
+
# falls straight to its own line rather than being fed to `fit` and
|
|
144
|
+
# silently returning "" - the note must be cut with a visible ellipsis or
|
|
145
|
+
# kept whole, never dropped outright (317a1 post-exec review, finding 4).
|
|
146
|
+
def self.field_table_lines(rows, width:, color:, key_width:)
|
|
147
|
+
prefix_width = key_width + 4 # " " + key.ljust + " "
|
|
148
|
+
|
|
149
|
+
rendered_rows = rows.map do |key, value, note, prebuilt|
|
|
150
|
+
value_budget = [width - prefix_width, 0].max
|
|
151
|
+
value_text = prebuilt ? value : fit_plain(value.to_s, value_budget)
|
|
152
|
+
[key.to_s, value_text, note.to_s]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
noted_widths = rendered_rows.filter_map { |_, value_text, note| visible_width(value_text) unless note.empty? }
|
|
156
|
+
value_col = [noted_widths.max.to_i, VALUE_COL_MAX].min
|
|
157
|
+
note_budget = width - prefix_width - value_col - 2
|
|
158
|
+
|
|
159
|
+
out = +""
|
|
160
|
+
rendered_rows.each do |key, value_text, note|
|
|
161
|
+
row = " #{styled(key.ljust(key_width), color, BOLD)} #{value_text}"
|
|
162
|
+
if note.empty?
|
|
163
|
+
out << row.rstrip << "\n"
|
|
164
|
+
next
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
value_fits = visible_width(value_text) <= value_col
|
|
168
|
+
note_width = visible_width(note)
|
|
169
|
+
note_overflows = note_width > note_budget
|
|
170
|
+
note_fits = !note_overflows || (note_budget.positive? && note_width <= NOTE_FLOOR)
|
|
171
|
+
if value_fits && note_fits
|
|
172
|
+
pad = value_col - visible_width(value_text)
|
|
173
|
+
out << row << (" " * pad) << " " << fit(note, note_budget) { |t| styled(t, color, MIDGREY) } << "\n"
|
|
174
|
+
else
|
|
175
|
+
out << row.rstrip << "\n"
|
|
176
|
+
own_budget = [width - prefix_width, 0].max
|
|
177
|
+
out << (" " * prefix_width) << fit(note, own_budget) { |t| styled(t, color, MIDGREY) } << "\n"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
out
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# --- markdown-noise stripping, adapter-optional (intent 316a1, D3/D5) ------
|
|
184
|
+
#
|
|
185
|
+
# Not every display surface passes text through a Markdown renderer, so
|
|
186
|
+
# stripping is not this module's call to make (see `markdown_safe:` on
|
|
187
|
+
# `render` above; the justification for WHY a caller would ever ask for
|
|
188
|
+
# this lives with that caller, in scripts/lib/message_display.rb). When
|
|
189
|
+
# asked, strips backticks and neutralises `*`/`_` runs from a value.
|
|
190
|
+
# Single underscores are left alone: they are common inside ordinary words
|
|
191
|
+
# (`intent_screen.rb`) and GFM does not treat an intraword underscore as
|
|
192
|
+
# emphasis; only a run of 2+ (the bold marker `__`) is markdown-active.
|
|
193
|
+
def self.clean(text)
|
|
194
|
+
text.to_s.delete("`*").gsub(/_{2,}/, "")
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# --- width cap (D15, matrix 18) --------------------------------------------
|
|
198
|
+
|
|
199
|
+
# Truncates `text` to `max` visible columns with a trailing ellipsis when
|
|
200
|
+
# cut, then yields the truncated plain text to the block for coloring.
|
|
201
|
+
# Coloring never adds visible width. The cap itself is harness-neutral: a
|
|
202
|
+
# fixed width, not a re-flow, is what lets a column layout survive whatever
|
|
203
|
+
# display eventually shows it — no display's own wrapping is assumed here.
|
|
204
|
+
def self.fit(text, max)
|
|
205
|
+
plain = fit_plain(text, max)
|
|
206
|
+
block_given? ? yield(plain) : plain
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def self.fit_plain(text, max)
|
|
210
|
+
return "" if max <= 0
|
|
211
|
+
return text if text.length <= max
|
|
212
|
+
return ELLIPSIS[0, max] if max <= 1
|
|
213
|
+
|
|
214
|
+
"#{text[0, max - 1]}#{ELLIPSIS}"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# --- palette ----------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
def self.styled(text, color, *codes)
|
|
220
|
+
return text unless color
|
|
221
|
+
|
|
222
|
+
"#{codes.join}#{text}#{RESET}"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def self.status_cell(done, color)
|
|
226
|
+
label = done ? " done " : " open "
|
|
227
|
+
return label unless color
|
|
228
|
+
|
|
229
|
+
hue = done ? TEAL : AMBER
|
|
230
|
+
"#{hue}#{BOLD}#{label}#{RESET}"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# `.dup` matters, not just style (318's own note, carried forward): these
|
|
234
|
+
# constants are built via string interpolation, which frozen_string_literal
|
|
235
|
+
# does NOT freeze automatically — only static literals get that. `.freeze`
|
|
236
|
+
# above makes them immutable, but `bar << ...` below still needs its OWN
|
|
237
|
+
# mutable copy or it would raise (or, without the freeze, silently corrupt
|
|
238
|
+
# the shared constant for every later call in the same process — matrix 16).
|
|
239
|
+
def self.render_bar(done, total, color)
|
|
240
|
+
ratio = total.zero? ? 0.0 : done.to_f / total
|
|
241
|
+
|
|
242
|
+
unless color
|
|
243
|
+
on = total.zero? ? 0 : (done * BAR_CELLS) / total
|
|
244
|
+
return ("#" * on) + ("." * (BAR_CELLS - on))
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
units = (ratio * BAR_CELLS * 8).round.clamp(0, BAR_CELLS * 8)
|
|
248
|
+
full, rem = units.divmod(8)
|
|
249
|
+
full = [full, BAR_CELLS].min
|
|
250
|
+
|
|
251
|
+
bar = TEAL.dup
|
|
252
|
+
bar << ("█" * full)
|
|
253
|
+
if full < BAR_CELLS && rem.positive?
|
|
254
|
+
bar << EIGHTHS[rem]
|
|
255
|
+
full += 1
|
|
256
|
+
end
|
|
257
|
+
track = BAR_CELLS - full
|
|
258
|
+
bar << GRAPHITE_BG << (" " * track) if track.positive?
|
|
259
|
+
bar << RESET
|
|
260
|
+
bar
|
|
261
|
+
end
|
|
262
|
+
end
|