@zalom/plastic 2.0.0-alpha.15 → 2.0.0-alpha.17
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/package.json +1 -1
- package/scripts/hook-message-display +7 -0
- package/scripts/lib/message_display.rb +83 -7
- package/scripts/lib/report_screen.rb +55 -16
- package/scripts/lib/screen_paint.rb +50 -3
- package/skills/intent-ending/SKILL.md +4 -3
- package/skills/intent-executing/SKILL.md +1 -1
- package/templates/outcome.md +5 -2
package/package.json
CHANGED
|
@@ -51,11 +51,18 @@ begin
|
|
|
51
51
|
tmp_root = ENV["PLASTIC_TMP"].to_s.empty? ? Dir.tmpdir : ENV["PLASTIC_TMP"]
|
|
52
52
|
plastic_home = File.expand_path(ENV["PLASTIC_HOME"] || "~/.plastic")
|
|
53
53
|
|
|
54
|
+
# PLASTIC_HOOK_TRACE=<file> (331a1): opt-in, off by default, one JSON
|
|
55
|
+
# object appended per chunk. This script is the only place allowed to
|
|
56
|
+
# read the environment; MessageDisplay takes the sink as an argument.
|
|
57
|
+
trace_path = ENV["PLASTIC_HOOK_TRACE"].to_s
|
|
58
|
+
trace = trace_path.empty? ? nil : MessageDisplay.file_trace(trace_path)
|
|
59
|
+
|
|
54
60
|
handler = MessageDisplay.new(
|
|
55
61
|
tmp_root: tmp_root,
|
|
56
62
|
plastic_home: plastic_home,
|
|
57
63
|
color: color_enabled?(plastic_home),
|
|
58
64
|
now: Time.now,
|
|
65
|
+
trace: trace,
|
|
59
66
|
)
|
|
60
67
|
result = handler.handle(payload)
|
|
61
68
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require "json"
|
|
5
6
|
require_relative "screen_paint"
|
|
6
7
|
|
|
7
8
|
# MessageDisplay (intent 316a, O4/O5, round 3 concurrency fix) - the Claude
|
|
@@ -110,9 +111,13 @@ class MessageDisplay
|
|
|
110
111
|
# NOSCREEN (D2), never read by this class as a decision in its own right.
|
|
111
112
|
PENDING_FILE = "PENDING"
|
|
112
113
|
|
|
114
|
+
# `trace` (331a1): an optional callable taking one Hash per chunk. The class
|
|
115
|
+
# stays pure - it never reads PLASTIC_HOOK_TRACE, never opens a file of its
|
|
116
|
+
# own; the CLI reads the variable and injects `file_trace`. nil, the
|
|
117
|
+
# default, costs the common path one `unless` and nothing else.
|
|
113
118
|
def initialize(tmp_root:, plastic_home:, color:, now:, wait_ms: 300, poll_ms: 20,
|
|
114
119
|
index_wait_ms: 20, max_wait_ms: 2000,
|
|
115
|
-
sleeper: ->(seconds) { sleep(seconds) })
|
|
120
|
+
sleeper: ->(seconds) { sleep(seconds) }, trace: nil)
|
|
116
121
|
@tmp_root = tmp_root
|
|
117
122
|
@plastic_home = plastic_home
|
|
118
123
|
@color = color
|
|
@@ -122,6 +127,7 @@ class MessageDisplay
|
|
|
122
127
|
@index_wait_ms = index_wait_ms
|
|
123
128
|
@max_wait_ms = max_wait_ms
|
|
124
129
|
@sleeper = sleeper
|
|
130
|
+
@trace = trace
|
|
125
131
|
end
|
|
126
132
|
|
|
127
133
|
def handle(payload)
|
|
@@ -141,11 +147,28 @@ class MessageDisplay
|
|
|
141
147
|
|
|
142
148
|
dir = self.class.buffer_path(tmp_root: @tmp_root, session_id: session_id, message_id: message_id)
|
|
143
149
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
150
|
+
@trace_detail = {}
|
|
151
|
+
result =
|
|
152
|
+
if index == 0
|
|
153
|
+
handle_chunk_zero(dir, delta, cwd, final)
|
|
154
|
+
else
|
|
155
|
+
handle_later_chunk(dir, index, delta, final)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
emit_trace(index, final, result)
|
|
159
|
+
result
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# One row per chunk, only when a sink was injected. Never raises: a trace
|
|
163
|
+
# is a diagnostic and must not be able to change what the hook returns.
|
|
164
|
+
def emit_trace(index, final, result)
|
|
165
|
+
return unless @trace
|
|
166
|
+
|
|
167
|
+
row = { "index" => index, "final" => final,
|
|
168
|
+
"displayed_bytes" => result.is_a?(String) ? result.bytesize : nil }
|
|
169
|
+
@trace.call(row.merge(@trace_detail.to_h))
|
|
170
|
+
rescue StandardError
|
|
171
|
+
nil
|
|
149
172
|
end
|
|
150
173
|
|
|
151
174
|
# The message directory both this class and the bash launcher (hooks/
|
|
@@ -164,6 +187,17 @@ class MessageDisplay
|
|
|
164
187
|
File.join(buffer_path(tmp_root: tmp_root, session_id: session_id, message_id: message_id), SCREEN_FILE)
|
|
165
188
|
end
|
|
166
189
|
|
|
190
|
+
# A trace sink that appends one JSON object per chunk to `path`. Every
|
|
191
|
+
# failure is swallowed: an unwritable path must never turn a diagnostic
|
|
192
|
+
# into a broken hook (D5, fail open).
|
|
193
|
+
def self.file_trace(path)
|
|
194
|
+
lambda do |row|
|
|
195
|
+
File.open(path, "a") { |f| f.puts(JSON.generate(row)) }
|
|
196
|
+
rescue StandardError
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
167
201
|
def self.noscreen_path(tmp_root:, session_id:, message_id:)
|
|
168
202
|
File.join(buffer_path(tmp_root: tmp_root, session_id: session_id, message_id: message_id), NOSCREEN_FILE)
|
|
169
203
|
end
|
|
@@ -186,10 +220,12 @@ class MessageDisplay
|
|
|
186
220
|
def handle_chunk_zero(dir, delta, _cwd, final)
|
|
187
221
|
split = split_at_opener(delta)
|
|
188
222
|
unless split
|
|
223
|
+
@trace_detail["decision"] = "noscreen"
|
|
189
224
|
write_noscreen(dir)
|
|
190
225
|
return nil
|
|
191
226
|
end
|
|
192
227
|
|
|
228
|
+
@trace_detail["decision"] = "engage"
|
|
193
229
|
engage(dir, 0, split, final)
|
|
194
230
|
end
|
|
195
231
|
|
|
@@ -199,10 +235,25 @@ class MessageDisplay
|
|
|
199
235
|
# is already on disk. Only once its own delta carries no opener does it
|
|
200
236
|
# fall back to the original decision-driven wait.
|
|
201
237
|
def handle_later_chunk(dir, index, delta, final)
|
|
238
|
+
# 331a1: an opener engages only a message that is NOT already a screen.
|
|
239
|
+
# 331a's late engagement exists for the prose-first reply, where chunk 0
|
|
240
|
+
# wrote NOSCREEN and a later chunk carries the title; it must not fire
|
|
241
|
+
# again once SCREEN is on disk. A reply can hold several screens back to
|
|
242
|
+
# back - the roster is a table then ten cards, the session report is many
|
|
243
|
+
# delivered screens - and re-engaging on each one returned that chunk's
|
|
244
|
+
# own prefix as raw Markdown and rewrote the start index, so the final
|
|
245
|
+
# chunk spliced from the LAST opener and everything before it reached the
|
|
246
|
+
# terminal unpainted. Inside an engaged message a later opener is simply
|
|
247
|
+
# content: it is buffered like any other line and the painter, which
|
|
248
|
+
# already understands a run of screens, lays all of them out.
|
|
202
249
|
split = split_at_opener(delta)
|
|
203
|
-
|
|
250
|
+
if split && !engaged?(dir)
|
|
251
|
+
@trace_detail["decision"] = "engage"
|
|
252
|
+
return engage(dir, index, split, final)
|
|
253
|
+
end
|
|
204
254
|
|
|
205
255
|
decision = wait_for_decision(dir, gate_delta: final ? nil : delta, index: index)
|
|
256
|
+
@trace_detail["decision"] = decision.to_s
|
|
206
257
|
|
|
207
258
|
return nil unless decision == :screen
|
|
208
259
|
|
|
@@ -210,6 +261,10 @@ class MessageDisplay
|
|
|
210
261
|
final ? finalize_final(dir, index) : ""
|
|
211
262
|
end
|
|
212
263
|
|
|
264
|
+
def engaged?(dir)
|
|
265
|
+
File.exist?(File.join(dir, SCREEN_FILE))
|
|
266
|
+
end
|
|
267
|
+
|
|
213
268
|
# Engages the message starting at THIS chunk (whatever its index): writes
|
|
214
269
|
# SCREEN carrying this chunk's index (replacing any NOSCREEN, D2/D6),
|
|
215
270
|
# buffers the opener onward at this chunk's own index, and returns the
|
|
@@ -426,6 +481,7 @@ class MessageDisplay
|
|
|
426
481
|
|
|
427
482
|
region_stop = ScreenPaint.region_end(lines, start)
|
|
428
483
|
painted = ScreenPaint.paint(lines[start...region_stop].join, color: true, markdown_safe: true)
|
|
484
|
+
trace_region(buffered, lines, start, region_stop, painted)
|
|
429
485
|
return buffered unless painted
|
|
430
486
|
|
|
431
487
|
# 331a (D4): a lone CLOSING fence immediately after the painted region is
|
|
@@ -445,6 +501,26 @@ class MessageDisplay
|
|
|
445
501
|
out
|
|
446
502
|
end
|
|
447
503
|
|
|
504
|
+
# What the final chunk actually painted, for the opt-in trace: how much was
|
|
505
|
+
# buffered, where the region ran, and the first line the grammar rejected -
|
|
506
|
+
# the one fact a terminal capture cannot give you. Computed only when a
|
|
507
|
+
# sink was injected, so an ordinary run pays nothing.
|
|
508
|
+
def trace_region(buffered, lines, start, region_stop, painted)
|
|
509
|
+
return unless @trace
|
|
510
|
+
|
|
511
|
+
rejected = ScreenPaint.first_rejected(lines, start)
|
|
512
|
+
@trace_detail.merge!(
|
|
513
|
+
"buffered_bytes" => buffered.bytesize,
|
|
514
|
+
"buffered_lines" => lines.length,
|
|
515
|
+
"region_start" => start,
|
|
516
|
+
"region_stop" => region_stop,
|
|
517
|
+
"painted" => !painted.nil?,
|
|
518
|
+
"first_rejected_line" => rejected && { "index" => rejected[0], "text" => rejected[1][0, 200] },
|
|
519
|
+
)
|
|
520
|
+
rescue StandardError
|
|
521
|
+
nil
|
|
522
|
+
end
|
|
523
|
+
|
|
448
524
|
def write_chunk(dir, index, delta)
|
|
449
525
|
atomic_write(File.join(dir, index.to_s), delta)
|
|
450
526
|
end
|
|
@@ -577,30 +577,69 @@ module ReportScreen
|
|
|
577
577
|
rows.compact
|
|
578
578
|
end
|
|
579
579
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
580
|
+
# Intent 331b (plan.md, "The one non-additive edit"): the standalone-token
|
|
581
|
+
# rule, extracted so `action_file_for` (the plan screen's Action column)
|
|
582
|
+
# calls the exact same rule as `matching_action_heading` and the two can
|
|
583
|
+
# never drift on what counts as a match.
|
|
584
|
+
def self.heading_tokens(heading)
|
|
585
|
+
heading.to_s.sub(/\A#+\s*/, "").split(/[^A-Za-z0-9]+/)
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# Rows 25-27: D19/D1r - the label must appear as a standalone token in an
|
|
589
|
+
# action file heading (any level), AND that heading must own at least one
|
|
590
|
+
# matrix data row - a heading that only names the label, with no table
|
|
591
|
+
# beneath it (or a table with a separator but no data row), is skipped and
|
|
592
|
+
# the walk keeps going. Lexicographic path order (D8), then file order.
|
|
593
|
+
#
|
|
594
|
+
# Merge note (322 into alpha, 2026-09-05): 322's table-owning rule and 331b's
|
|
595
|
+
# extracted `heading_tokens` are both kept. The token split now comes from the
|
|
596
|
+
# shared helper so `action_file_for` cannot drift from this walk, while the
|
|
597
|
+
# `table_rows(body).any?` guard stays the thing that decides the match.
|
|
598
|
+
def self.matching_action_heading(intent_dir, label)
|
|
599
|
+
Dir.glob(File.join(intent_dir, "actions", "*.md")).sort.each do |path|
|
|
600
|
+
split_by_headings(File.read(path)).each do |heading, body|
|
|
601
|
+
next unless heading_tokens(heading).include?(label)
|
|
602
|
+
return [heading, body] if table_rows(body).any?
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
[nil, nil]
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
# D3r: the row-cell fallback, for the shape where the label never appears in
|
|
609
|
+
# a heading at all, only as the first cell of a matrix data row. Restricted
|
|
610
|
+
# to tables under a heading that names itself a matrix (/matrix/i) - never a
|
|
611
|
+
# step list or any other table - so it cannot answer for a record that has
|
|
612
|
+
# no matrix anywhere (the close-gate defeat the plan review measured).
|
|
613
|
+
# Emphasis (bold/italic/code) is stripped from the compared cell; the count
|
|
614
|
+
# sums matching rows across every matrix heading, in every action file.
|
|
615
|
+
def self.matching_matrix_rows(intent_dir, label)
|
|
616
|
+
count = 0
|
|
592
617
|
Dir.glob(File.join(intent_dir, "actions", "*.md")).sort.each do |path|
|
|
593
618
|
split_by_headings(File.read(path)).each do |heading, body|
|
|
594
|
-
|
|
619
|
+
next unless heading.to_s.match?(/matrix/i)
|
|
620
|
+
table_rows(body).each do |cells|
|
|
621
|
+
cell = cells[0].to_s.gsub(/[*_`]/, "").strip
|
|
622
|
+
count += 1 if cell == label
|
|
623
|
+
end
|
|
595
624
|
end
|
|
596
625
|
end
|
|
597
|
-
|
|
626
|
+
count
|
|
598
627
|
end
|
|
599
628
|
|
|
629
|
+
# D7: a label with no letter never resolves, on either path - it is a
|
|
630
|
+
# bullet-derived Delivered number (delivered_rows), never a label anyone
|
|
631
|
+
# wrote, and would otherwise fabricate proof from a numbered heading like
|
|
632
|
+
# "## 1. What this intent is" or from a numbered matrix row-cell column.
|
|
600
633
|
def self.proven_by(intent_dir, label)
|
|
634
|
+
return NOT_RECORDED unless label.to_s.match?(/[A-Za-z]/)
|
|
635
|
+
|
|
601
636
|
_heading, body = matching_action_heading(intent_dir, label)
|
|
602
|
-
|
|
603
|
-
|
|
637
|
+
if body
|
|
638
|
+
n = table_rows(body).length
|
|
639
|
+
return n.positive? ? "#{n} test#{n == 1 ? '' : 's'}" : NOT_RECORDED
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
n = matching_matrix_rows(intent_dir, label)
|
|
604
643
|
n.positive? ? "#{n} test#{n == 1 ? '' : 's'}" : NOT_RECORDED
|
|
605
644
|
end
|
|
606
645
|
|
|
@@ -38,6 +38,19 @@ module ScreenPaint
|
|
|
38
38
|
STEP_LINE_RE = /\A(S\d+)\s+\[ (open|done) \]\s+(.*)\z/.freeze
|
|
39
39
|
TIMELINE_RE = /\A(\d\d:\d\d)\s{2}(\S+)\s{2}(.*)\z/.freeze
|
|
40
40
|
COUNT_LINE_RE = /\A\d+ open( · .*)?\z/.freeze
|
|
41
|
+
# 331a1: the session verb prints this note between the delivered screens and
|
|
42
|
+
# the roster (report_screen.rb, render_session). It is our own output, so the
|
|
43
|
+
# painter must not reject it - the live hook trace caught the region stopping
|
|
44
|
+
# dead on it, with the whole roster below reaching the terminal as plain
|
|
45
|
+
# Markdown. Pinned to the exact shape rather than "any sentence": region_end
|
|
46
|
+
# exists to stop at the model's own prose, and a loose rule would swallow it.
|
|
47
|
+
SKIP_NOTE_RE = /\A\d+ completed intents? skipped: .+\z/.freeze
|
|
48
|
+
# 331a2 (D5, S3): render_session's own rescue card when a directory's delivered screen
|
|
49
|
+
# raises (report_screen.rb:1466-1468) - our own output, not model prose. The census (331a2)
|
|
50
|
+
# proved it classified :unknown, orphaning the roster below it in a real reply. `:meta`, not
|
|
51
|
+
# a new kind: it is a one-line grey note under a title and paint's `:meta` arm reads only
|
|
52
|
+
# `line.strip`, with no positional dependency.
|
|
53
|
+
RESCUE_CARD_RE = /\A## \S+ · could not render \(.*\)\z/.freeze
|
|
41
54
|
BOLD_LEAD_RE = /\A\*\*([^*]+)\*\*(.*)\z/.freeze
|
|
42
55
|
|
|
43
56
|
# Intent 317a1 (D3, D4, D5): the data-table palette. Kind and note columns
|
|
@@ -195,11 +208,12 @@ module ScreenPaint
|
|
|
195
208
|
return :table if text.lstrip.start_with?("|")
|
|
196
209
|
return :bold if BOLD_LEAD_RE.match?(stripped) && text == stripped
|
|
197
210
|
return :meta if idx && opener_idx && idx == opener_idx + 1 && stripped.include?(" · ")
|
|
211
|
+
return :meta if RESCUE_CARD_RE.match?(stripped)
|
|
198
212
|
return :indented if text.start_with?(" ")
|
|
199
213
|
return :field if FIELD_LINE_RE.match?(text)
|
|
200
214
|
return :step if STEP_LINE_RE.match?(text)
|
|
201
215
|
return :timeline if TIMELINE_RE.match?(text)
|
|
202
|
-
return :count if COUNT_LINE_RE.match?(stripped)
|
|
216
|
+
return :count if COUNT_LINE_RE.match?(stripped) || SKIP_NOTE_RE.match?(stripped)
|
|
203
217
|
return :closer if ["None", "not recorded", "No intents in delivery.", "No intents delivered in this session."].include?(stripped)
|
|
204
218
|
:unknown
|
|
205
219
|
end
|
|
@@ -207,11 +221,22 @@ module ScreenPaint
|
|
|
207
221
|
# Where the screen region ends inside a larger message (B10): walk from the
|
|
208
222
|
# opener while every line classifies; the first unknown line - ordinary
|
|
209
223
|
# prose, a prose bullet - is the boundary. Never consumes past the screen.
|
|
224
|
+
#
|
|
225
|
+
# 331a1: `opener_idx` tracks the NEAREST preceding opener, not the first
|
|
226
|
+
# one in the message. A reply can carry several screens back to back - the
|
|
227
|
+
# roster is a table then ten cards, the session report is many delivered
|
|
228
|
+
# screens - and `classify`'s positional rule is "the line right after a
|
|
229
|
+
# title", which means the title above it. Testing every line against the
|
|
230
|
+
# message's first opener made the timestamp under the SECOND screen
|
|
231
|
+
# :unknown, and the region stopped there: 30 lines of a 350-line session
|
|
232
|
+
# report painted, the rest reaching the terminal as plain Markdown.
|
|
210
233
|
def region_end(lines, start_idx)
|
|
211
234
|
i = start_idx + 1
|
|
235
|
+
opener_idx = start_idx
|
|
212
236
|
while i < lines.length
|
|
213
|
-
kind = classify(lines[i], idx: i, opener_idx:
|
|
237
|
+
kind = classify(lines[i], idx: i, opener_idx: opener_idx)
|
|
214
238
|
break if kind == :unknown
|
|
239
|
+
opener_idx = i if kind == :opener
|
|
215
240
|
# A bare "**Section**" head belongs to the screen only when what follows
|
|
216
241
|
# is still grammar; "**What this means**" over prose bullets is the
|
|
217
242
|
# model's own commentary and stays outside, unsplit (B10).
|
|
@@ -225,6 +250,22 @@ module ScreenPaint
|
|
|
225
250
|
i
|
|
226
251
|
end
|
|
227
252
|
|
|
253
|
+
# The first line the grammar rejects, walking from the opener under the same
|
|
254
|
+
# nearest-opener rule region_end uses. Returns [index, line] or nil when the
|
|
255
|
+
# whole run classifies. 331a1: this is what the opt-in hook trace reports,
|
|
256
|
+
# so a live run says which line stopped the region instead of leaving it to
|
|
257
|
+
# be guessed from a terminal capture.
|
|
258
|
+
def first_rejected(lines, start_idx)
|
|
259
|
+
opener_idx = start_idx
|
|
260
|
+
((start_idx + 1)...lines.length).each do |i|
|
|
261
|
+
kind = classify(lines[i], idx: i, opener_idx: opener_idx)
|
|
262
|
+
return [i, lines[i].to_s.rstrip] if kind == :unknown
|
|
263
|
+
|
|
264
|
+
opener_idx = i if kind == :opener
|
|
265
|
+
end
|
|
266
|
+
nil
|
|
267
|
+
end
|
|
268
|
+
|
|
228
269
|
def bare_bold?(line)
|
|
229
270
|
m = BOLD_LEAD_RE.match(line.strip)
|
|
230
271
|
m && m[2].to_s.strip.empty?
|
|
@@ -271,8 +312,14 @@ module ScreenPaint
|
|
|
271
312
|
table.clear
|
|
272
313
|
end
|
|
273
314
|
|
|
315
|
+
# 331a1: the same nearest-opener rule region_end uses - a reply carrying
|
|
316
|
+
# several screens must classify each one's meta line against its own
|
|
317
|
+
# title, not against the first title in the message.
|
|
318
|
+
opener_idx = first_idx
|
|
319
|
+
|
|
274
320
|
lines.each_with_index do |line, idx|
|
|
275
|
-
kind = classify(line, idx: idx, opener_idx:
|
|
321
|
+
kind = classify(line, idx: idx, opener_idx: opener_idx)
|
|
322
|
+
opener_idx = idx if kind == :opener
|
|
276
323
|
# Intent 317a1 (O6, D8): the run counter tracks consecutive :indented
|
|
277
324
|
# lines so only the SECOND and later lines under a heading like
|
|
278
325
|
# "**Asked**" grey as a note; a :table line (which `next`s below) must
|
|
@@ -67,9 +67,10 @@ fill `## Summary`, `## Delivered`, `## Verification`, `## Follow-ups`. `## Deliv
|
|
|
67
67
|
`| Row | What |` table: one row per thing delivered, in plain wording a reader
|
|
68
68
|
recognizes, not a method name or an implementation summary (that detail
|
|
69
69
|
belongs in `## Summary`). Each row's label must appear as a standalone token
|
|
70
|
-
in an action-file heading (`### S1 - ...`
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
in an action-file heading that owns the matrix table (`### S1 - ...` with a
|
|
71
|
+
table beneath it proves row S1; a table-less heading naming the label is
|
|
72
|
+
skipped); that heading's matrix rows become the row's Proven-by cell on
|
|
73
|
+
`report-screen delivered`'s post-delivery screen. `## Needs you` is the literal None or a
|
|
73
74
|
`| N | What | Why |` table. On abandon, `## Summary` states the abandonment reason and the trail (see Pivot
|
|
74
75
|
below). A placeholder outcome.md is backfilled from the record instead, with the
|
|
75
76
|
close's disposition and the `--outcome-summary` line as its summary. Also author
|
|
@@ -75,7 +75,7 @@ Apply the auto skill's risk rule to the executor's return and the diff: a matrix
|
|
|
75
75
|
|
|
76
76
|
Whenever a review verdict returns - the plan review before code, or the post-execution review above - the lead appends a `Review` line: `ruby ~/.plastic/scripts/savepoint-note <intent_dir> --kind Review --text "<verdict, what changed>"` (intent 317, D17). This is the other half of what `report-screen delay` reads.
|
|
77
77
|
|
|
78
|
-
**The D19 heading convention.** An action file's `## Delivered` row (in `outcome.md`) is proven by
|
|
78
|
+
**The D19 heading convention.** An action file's `## Delivered` row (in `outcome.md`) is proven by the first `actions/ACTION_N.md` heading that carries that row's label as a standalone token AND owns the matrix table (322 D1r) - `### Row A -` with a table beneath it proves row A, `### S1 -` proves row S1; a heading that only names the label, with no table under it, is skipped. Write action-file section headings so the label they prove is unambiguous (never a substring another label could also match, like `A` inside `AB`); `report-screen delivered`'s Proven-by column renders `not recorded` when no heading owns a matching table and no matrix row cell carries the label either.
|
|
79
79
|
|
|
80
80
|
### Step 4: Update Intent and Complete
|
|
81
81
|
Capture observations in `## Insights`. When ALL checklist items are checked:
|
package/templates/outcome.md
CHANGED
|
@@ -10,8 +10,11 @@ disposition: delivered|abandoned
|
|
|
10
10
|
<!-- One row per thing delivered, in plain wording a reader recognizes, not
|
|
11
11
|
an implementation summary; the technical detail belongs in ## Summary. Each
|
|
12
12
|
row's label must appear as a standalone token in an actions/*.md heading
|
|
13
|
-
(for example "### S1 - ..."
|
|
14
|
-
|
|
13
|
+
that owns the matrix table (for example "### S1 - ..." with a table beneath
|
|
14
|
+
it proves row S1); that heading's matrix rows become the row's Proven-by
|
|
15
|
+
cell on the delivered screen (intent 317 D19, 317a, 322 D1r). A label with no
|
|
16
|
+
owning heading falls back to a matrix row cell that carries it, when one
|
|
17
|
+
under a heading named "matrix" exists (322 D3r). -->
|
|
15
18
|
| Row | What |
|
|
16
19
|
| --- | --- |
|
|
17
20
|
| S1 | ... |
|