@zalom/plastic 2.0.0-alpha.2 → 2.0.0-alpha.4
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-capture +2 -1
- package/scripts/intent-screen +52 -0
- package/scripts/lib/installer_core.rb +2 -0
- package/scripts/lib/intent_screen.rb +221 -0
- package/skills/intent-continuing/SKILL.md +16 -17
- package/skills/intent-continuing/references/boarding-matrix.md +5 -5
- package/skills/intent-continuing/references/context-management.md +1 -1
- package/templates/intent-screen.md +22 -0
package/package.json
CHANGED
package/scripts/hook-capture
CHANGED
|
@@ -31,7 +31,8 @@ end
|
|
|
31
31
|
exit 0 unless payload.is_a?(Hash)
|
|
32
32
|
|
|
33
33
|
session_id = payload["session_id"].to_s
|
|
34
|
-
prompt = payload["
|
|
34
|
+
prompt = payload["prompt"].to_s
|
|
35
|
+
prompt = payload["user_prompt"].to_s if prompt.empty?
|
|
35
36
|
cwd = payload["cwd"].to_s
|
|
36
37
|
cwd = Dir.pwd if cwd.empty?
|
|
37
38
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
# intent-screen (intent 316) - prints the intent screen for one intent directory:
|
|
5
|
+
# the title, the field table (Store, Status, Stage, Savepoint, Progress, Next,
|
|
6
|
+
# Insight, each with a note), and the Steps table, filled from the record by
|
|
7
|
+
# scripts/lib/intent_screen.rb. The session adds the What-this-means bullets and
|
|
8
|
+
# the close; it never edits the numbers.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# intent-screen <intent_dir> [--template <path>]
|
|
12
|
+
#
|
|
13
|
+
# The store root is the directory two levels above the intent (<root>/store/<id--slug>);
|
|
14
|
+
# the template defaults to templates/intent-screen.md next to this script's dir,
|
|
15
|
+
# in-repo (<repo>/scripts -> <repo>/templates) and installed (~/.plastic/scripts ->
|
|
16
|
+
# ~/.plastic/templates) alike.
|
|
17
|
+
#
|
|
18
|
+
# Exit codes:
|
|
19
|
+
# 0 - the screen is on stdout
|
|
20
|
+
# 2 - usage error, or the path is not an intent directory (one line on stderr)
|
|
21
|
+
|
|
22
|
+
require_relative "lib/intent_screen"
|
|
23
|
+
|
|
24
|
+
def usage_abort(message)
|
|
25
|
+
warn "intent-screen: #{message}"
|
|
26
|
+
exit 2
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
args = ARGV.dup
|
|
30
|
+
template_path = nil
|
|
31
|
+
positional = []
|
|
32
|
+
while (arg = args.shift)
|
|
33
|
+
case arg
|
|
34
|
+
when "--template"
|
|
35
|
+
template_path = args.shift or usage_abort("--template needs a path")
|
|
36
|
+
else
|
|
37
|
+
usage_abort("unknown flag #{arg.inspect}") if arg.start_with?("--")
|
|
38
|
+
positional << arg
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
usage_abort("usage: intent-screen <intent_dir> [--template <path>]") unless positional.length == 1
|
|
43
|
+
intent_dir = File.expand_path(positional.first)
|
|
44
|
+
usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
|
|
45
|
+
|
|
46
|
+
store_root = File.expand_path("../..", intent_dir)
|
|
47
|
+
template_path ||= File.expand_path("../templates/intent-screen.md", __dir__)
|
|
48
|
+
usage_abort("template not found at #{template_path}") unless File.exist?(template_path)
|
|
49
|
+
|
|
50
|
+
$stdout.write IntentScreen.render(intent_dir: intent_dir, store_root: store_root,
|
|
51
|
+
template: File.read(template_path))
|
|
52
|
+
exit 0
|
|
@@ -423,6 +423,8 @@ class InstallerCore
|
|
|
423
423
|
"scripts/lib/day_summary.rb" => "scripts/lib/day_summary.rb",
|
|
424
424
|
"scripts/write-handoff" => "scripts/write-handoff",
|
|
425
425
|
"scripts/day-summary" => "scripts/day-summary",
|
|
426
|
+
"scripts/lib/intent_screen.rb" => "scripts/lib/intent_screen.rb",
|
|
427
|
+
"scripts/intent-screen" => "scripts/intent-screen",
|
|
426
428
|
"scripts/hook-savepoint" => "scripts/hook-savepoint",
|
|
427
429
|
}
|
|
428
430
|
end
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
# IntentScreen (intent 316) - fills templates/intent-screen.md from one intent's
|
|
4
|
+
# record: the intent file, the tier's INDEX.md, savepoint.md, and checklist.md.
|
|
5
|
+
# Every number on the screen comes from here so the session never writes one by
|
|
6
|
+
# eye. Pure: explicit paths in, a Markdown string out; no ENV, no Dir.pwd.
|
|
7
|
+
module IntentScreen
|
|
8
|
+
BAR_WIDTH = 20
|
|
9
|
+
ON = "█"
|
|
10
|
+
OFF = "░"
|
|
11
|
+
PLACEHOLDER_SENTINEL = "<!-- plastic:placeholder -->"
|
|
12
|
+
SECTIONS = %w[Active Future Completed Abandoned].freeze
|
|
13
|
+
ITEM_RE = /^\s*- \[([ xX])\]\s+(.*)$/
|
|
14
|
+
STEP_PREFIX_RE = /\A(?:Step|S)\s*\d+\s*[-:·]\s*/i
|
|
15
|
+
INSIGHT_RE = /\A(\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ)\s+·\s+\S+\s+·\s+.+?\s+—\s+(.+)\z/
|
|
16
|
+
SAVEPOINT_RE = /\A(\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ)\s{2,}(\S+)\s{2,}(.+?)\s*\z/
|
|
17
|
+
|
|
18
|
+
# Where a resume lands, from the ledger's last line (the boarding matrix).
|
|
19
|
+
def self.landing_stage(stage, milestone)
|
|
20
|
+
case stage
|
|
21
|
+
when "Done" then "Done"
|
|
22
|
+
when "What" then "Why"
|
|
23
|
+
when "Why" then milestone.to_s.include?("spec.md") ? "How" : "Why"
|
|
24
|
+
when "How" then milestone.to_s.include?("checklist.md") ? "Exec" : "How"
|
|
25
|
+
when "Exec" then milestone.to_s.include?("outcome.md") ? "ready to complete" : "Exec"
|
|
26
|
+
else "Why"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.intent_dir?(dir)
|
|
31
|
+
return false unless dir && File.directory?(dir)
|
|
32
|
+
|
|
33
|
+
base = File.basename(dir)
|
|
34
|
+
return false unless base.match?(/\A[0-9][0-9a-z]*--[\w-]+\z/)
|
|
35
|
+
|
|
36
|
+
File.exist?(File.join(dir, "#{base}.md"))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.render(intent_dir:, store_root:, template:)
|
|
40
|
+
base = File.basename(intent_dir)
|
|
41
|
+
id = base.split("--", 2).first
|
|
42
|
+
intent_text = File.read(File.join(intent_dir, "#{base}.md"))
|
|
43
|
+
|
|
44
|
+
fields = {}
|
|
45
|
+
fields.merge!(store_fields(store_root))
|
|
46
|
+
status, title = index_fields(store_root, id)
|
|
47
|
+
fields["status"] = status
|
|
48
|
+
fields["status.note"] = status == "unlisted" ? "no INDEX.md line names this id" : "listed under ## #{status} in INDEX.md"
|
|
49
|
+
fields["id"] = id
|
|
50
|
+
fields["name"] = title || fallback_name(intent_text)
|
|
51
|
+
fields.merge!(savepoint_fields(intent_dir, intent_text))
|
|
52
|
+
items = checklist_items(intent_dir)
|
|
53
|
+
fields.merge!(progress_fields(items))
|
|
54
|
+
fields.merge!(next_fields(items, status, checklist_present: items_present?(intent_dir)))
|
|
55
|
+
fields.merge!(insight_fields(intent_text))
|
|
56
|
+
fields["steps.rows"] = steps_rows(items)
|
|
57
|
+
fields["meaning"] = ""
|
|
58
|
+
fields["close"] = ""
|
|
59
|
+
|
|
60
|
+
out = template.dup
|
|
61
|
+
fields.each { |k, v| out = out.gsub("{{#{k}}}", v.to_s) }
|
|
62
|
+
out.gsub(/\n{3,}/, "\n\n")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# --- store ---------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
def self.store_fields(store_root)
|
|
68
|
+
parent = File.basename(File.dirname(store_root))
|
|
69
|
+
if parent == "projects"
|
|
70
|
+
slug = File.basename(store_root)
|
|
71
|
+
{ "store" => "project:#{slug}", "store.note" => "the #{slug} project store" }
|
|
72
|
+
else
|
|
73
|
+
{ "store" => "global", "store.note" => "the global store" }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# --- INDEX.md -----------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def self.index_fields(store_root, id)
|
|
80
|
+
path = File.join(store_root, "INDEX.md")
|
|
81
|
+
return ["unlisted", nil] unless File.exist?(path)
|
|
82
|
+
|
|
83
|
+
section = nil
|
|
84
|
+
File.foreach(path) do |line|
|
|
85
|
+
if line.start_with?("## ")
|
|
86
|
+
name = line[3..].strip
|
|
87
|
+
section = SECTIONS.include?(name) ? name : nil
|
|
88
|
+
next
|
|
89
|
+
end
|
|
90
|
+
next unless section && line.strip.start_with?("- [")
|
|
91
|
+
|
|
92
|
+
m = line.match(/\A\s*- \[#{Regexp.escape(id)}\s+[-—]\s+(.+?)\]\(/)
|
|
93
|
+
return [section, m[1].strip] if m
|
|
94
|
+
end
|
|
95
|
+
["unlisted", nil]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.fallback_name(intent_text)
|
|
99
|
+
m = intent_text.match(/^intent:\s*["']?(.+?)["']?\s*$/)
|
|
100
|
+
text = m ? m[1] : ""
|
|
101
|
+
text.length > 60 ? "#{text[0, 57]}..." : text
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# --- savepoint.md ---------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
def self.savepoint_fields(intent_dir, intent_text)
|
|
107
|
+
path = File.join(intent_dir, "savepoint.md")
|
|
108
|
+
lines = File.exist?(path) ? File.readlines(path).map(&:strip).reject(&:empty?) : []
|
|
109
|
+
last = lines.reverse.map { |l| l.match(SAVEPOINT_RE) }.compact.first
|
|
110
|
+
unless last
|
|
111
|
+
return { "stage" => "Why", "stage.note" => "no savepoint line yet",
|
|
112
|
+
"savepoint" => "none", "savepoint.note" => "" }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
ts, stage, milestone = last[1], last[2], last[3]
|
|
116
|
+
landing = landing_stage(stage, milestone)
|
|
117
|
+
delivered = lines.map { |l| l.match(SAVEPOINT_RE) }.compact.map { |m| m[2] }.uniq
|
|
118
|
+
delivered &= %w[What Why How Exec]
|
|
119
|
+
note = if landing == "Done"
|
|
120
|
+
"delivered; the record is immutable"
|
|
121
|
+
elsif landing == "ready to complete"
|
|
122
|
+
"outcome.md is real; run the ending procedure"
|
|
123
|
+
else
|
|
124
|
+
"#{delivered.join(', ')} delivered; the work is open"
|
|
125
|
+
end
|
|
126
|
+
{ "stage" => landing, "stage.note" => note,
|
|
127
|
+
"savepoint" => "#{stage} · #{milestone}", "savepoint.note" => human_time(ts) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.human_time(ts)
|
|
131
|
+
m = ts.match(/\A(\d{4}-\d\d-\d\d)T(\d\d:\d\d)/)
|
|
132
|
+
m ? "#{m[1]} #{m[2]} UTC" : ts
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# --- checklist.md --------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def self.items_present?(intent_dir)
|
|
138
|
+
path = File.join(intent_dir, "checklist.md")
|
|
139
|
+
return false unless File.exist?(path)
|
|
140
|
+
|
|
141
|
+
!File.read(path).lstrip.start_with?(PLACEHOLDER_SENTINEL)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.checklist_items(intent_dir)
|
|
145
|
+
return [] unless items_present?(intent_dir)
|
|
146
|
+
|
|
147
|
+
File.readlines(File.join(intent_dir, "checklist.md")).filter_map do |line|
|
|
148
|
+
m = line.match(ITEM_RE)
|
|
149
|
+
next unless m
|
|
150
|
+
|
|
151
|
+
text = m[2].strip
|
|
152
|
+
next if text == "..."
|
|
153
|
+
|
|
154
|
+
{ done: m[1] != " ", text: text.sub(STEP_PREFIX_RE, "") }
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.progress_fields(items)
|
|
159
|
+
total = items.length
|
|
160
|
+
done = items.count { |i| i[:done] }
|
|
161
|
+
on = total.zero? ? 0 : (done * BAR_WIDTH) / total
|
|
162
|
+
bar = (ON * on) + (OFF * (BAR_WIDTH - on))
|
|
163
|
+
note = if total.zero?
|
|
164
|
+
"no checklist yet"
|
|
165
|
+
elsif done == total
|
|
166
|
+
"all steps done"
|
|
167
|
+
else
|
|
168
|
+
"#{total - done} steps open"
|
|
169
|
+
end
|
|
170
|
+
{ "progress.bar" => bar, "progress.done" => done.to_s, "progress.total" => total.to_s,
|
|
171
|
+
"progress.note" => note }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def self.next_fields(items, status, checklist_present:)
|
|
175
|
+
return { "next" => "", "next.note" => "" } if %w[Completed Abandoned].include?(status)
|
|
176
|
+
return { "next" => "write checklist.md", "next.note" => "How" } unless checklist_present
|
|
177
|
+
|
|
178
|
+
idx = items.index { |i| !i[:done] }
|
|
179
|
+
return { "next" => "", "next.note" => "all steps done" } unless idx
|
|
180
|
+
|
|
181
|
+
head, = split_first_clause(items[idx][:text])
|
|
182
|
+
{ "next" => "S#{idx + 1} · #{escape(head)}", "next.note" => "first open step" }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def self.steps_rows(items)
|
|
186
|
+
return "| | | no steps yet |" if items.empty?
|
|
187
|
+
|
|
188
|
+
items.each_with_index.map do |item, i|
|
|
189
|
+
"| S#{i + 1} | #{item[:done] ? 'done' : 'open'} | #{escape(item[:text])} |"
|
|
190
|
+
end.join("\n")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def self.escape(text)
|
|
194
|
+
text.gsub("|", "\\|")
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# --- ## Insights ----------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def self.insight_fields(intent_text)
|
|
200
|
+
section = intent_text.split(/^## Insights\s*$/, 2)[1].to_s.split(/^## /, 2)[0].to_s
|
|
201
|
+
entry = section.lines.map(&:strip).reverse.map { |l| l.match(INSIGHT_RE) }.compact.first
|
|
202
|
+
return { "insight" => "none yet", "insight.note" => "" } unless entry
|
|
203
|
+
|
|
204
|
+
ts, text = entry[1], entry[2].strip
|
|
205
|
+
head, tail = split_first_clause(text)
|
|
206
|
+
note = tail.empty? ? human_time(ts) : "#{human_time(ts)} · #{tail}"
|
|
207
|
+
{ "insight" => escape(head), "insight.note" => escape(note) }
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def self.split_first_clause(text)
|
|
211
|
+
m = text.match(/\A(.+?)[.;](\s+.*|\z)/m)
|
|
212
|
+
head = m ? m[1] : text
|
|
213
|
+
tail = m ? m[2].to_s.strip : ""
|
|
214
|
+
if head.length > 60
|
|
215
|
+
cut = head[0, 60].rindex(" ") || 60
|
|
216
|
+
tail = "#{head[cut..].strip} #{tail}".strip
|
|
217
|
+
head = head[0, cut].strip
|
|
218
|
+
end
|
|
219
|
+
[head, tail]
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -5,7 +5,7 @@ description: >-
|
|
|
5
5
|
where we left off", "where was I", "what should I work on", names a specific intent to
|
|
6
6
|
resume (by id or description, or `--intent {id}`), or names a roadmap or delivery batch to
|
|
7
7
|
resume (`--roadmap {slug}`, "where is the roadmap", "where did that batch land"). Presents
|
|
8
|
-
state and resumes at the last delivered
|
|
8
|
+
state and resumes at the last delivered stage; it never asks auto or guided, never boots
|
|
9
9
|
(the SessionStart hook owns boot), and never drives work autonomously (plastic-auto does).
|
|
10
10
|
Absorbs the former continuing, project-continuing, and roadmap-continuing skills and the
|
|
11
11
|
read half of the former intent-starting skill (intent 304).
|
|
@@ -79,19 +79,20 @@ QMD-first when the intent is named by description: run
|
|
|
79
79
|
authoritative intent file. The command is a no-op when QMD is absent; fall back to
|
|
80
80
|
`INDEX.md`.
|
|
81
81
|
|
|
82
|
-
If the intent is terminal (`## Completed` or `## Abandoned` in `INDEX.md`):
|
|
83
|
-
|
|
82
|
+
If the intent is terminal (`## Completed` or `## Abandoned` in `INDEX.md`): print the
|
|
83
|
+
intent screen (Status shows the terminal section, Next is empty), summarize its
|
|
84
|
+
`outcome.md`, and ask what is next; never reopen it.
|
|
84
85
|
|
|
85
86
|
For a live intent's directory:
|
|
86
87
|
|
|
87
88
|
1. **Read `savepoint.md` first.** It is a deterministic, append-only ledger, one line per
|
|
88
|
-
event, newest at the bottom: `{utc-iso8601} {Stage} {milestone}`. Classify the
|
|
89
|
+
event, newest at the bottom: `{utc-iso8601} {Stage} {milestone}`. Classify the stage
|
|
89
90
|
from the last line alone (the table in `references/boarding-matrix.md`, read when
|
|
90
91
|
classifying), then verify only that line's artifact is real (sentinel-aware:
|
|
91
92
|
`Savepoint.stage_file_present?`). Do not re-probe every lifecycle file.
|
|
92
|
-
2. **
|
|
93
|
+
2. **Stale ledger.** When the last line disagrees with the files on disk, rebuild the ledger from
|
|
93
94
|
disk and note the correction. A rebuilt ledger is the file-landing skeleton, which still
|
|
94
|
-
pins the
|
|
95
|
+
pins the stage:
|
|
95
96
|
```bash
|
|
96
97
|
ruby -r ~/.plastic/scripts/lib/savepoint -e 'Savepoint.rebuild_savepoint("<intent_dir>")'
|
|
97
98
|
```
|
|
@@ -99,18 +100,16 @@ For a live intent's directory:
|
|
|
99
100
|
else the newest prior day) is the prior session's own account of where things stand; read
|
|
100
101
|
it after the ledger, never instead of it.
|
|
101
102
|
4. **Derive the next step:** the first unchecked item in `checklist.md` when it exists, else
|
|
102
|
-
the next thing the
|
|
103
|
+
the next thing the stage needs (see the matrix). The newest `## Insights` entry supplies
|
|
103
104
|
the human-readable context; an entry marked `(autonomous)` means an auto team was
|
|
104
105
|
delivering it, so say so and offer to hand back to `plastic-auto`.
|
|
105
|
-
5. **
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
Drift: [none | ledger rebuilt from disk]
|
|
113
|
-
```
|
|
106
|
+
5. **Print the intent screen, then continue at that stage.** Run
|
|
107
|
+
`ruby ~/.plastic/scripts/intent-screen <intent_dir>` and print its output as it is: the
|
|
108
|
+
title, the field table, and the Steps table come from the record, never by eye. Under it
|
|
109
|
+
write **What this means** as two to four bullets in plain words (what the intent is for,
|
|
110
|
+
what has landed, what is left, any defect named by step), then close with
|
|
111
|
+
**needs input:** naming the first open step. The screen's shape is
|
|
112
|
+
`~/.plastic/templates/intent-screen.md`; the script fills it, the session never edits the numbers.
|
|
114
113
|
Then continue the work in the session's current mode. In auto mode the running team
|
|
115
114
|
already holds the delivery lock; if a lock is held by a session that is gone, the
|
|
116
115
|
`plastic-doctor` skill's lock section repairs or reclaims it.
|
|
@@ -141,6 +140,6 @@ For a live intent's directory:
|
|
|
141
140
|
| Trigger | Read |
|
|
142
141
|
|---|---|
|
|
143
142
|
| Filling the board on the project route | `references/board-fill.md` |
|
|
144
|
-
| Classifying the
|
|
143
|
+
| Classifying the stage from the ledger's last line | `references/boarding-matrix.md` |
|
|
145
144
|
| Explaining why one roadmap ranked above another | `references/liveness-ranking.md` |
|
|
146
145
|
| Saving or restoring context across a long session, or debugging a resume | `references/context-management.md` |
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Boarding matrix: which
|
|
1
|
+
# Boarding matrix: which stage a resume lands at
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The stage is derived from `savepoint.md`'s last line plus the real artifacts on disk.
|
|
4
4
|
Classify from the last line alone, then verify only that line's artifact is real
|
|
5
|
-
(sentinel-aware).
|
|
5
|
+
(sentinel-aware). When the ledger is stale, rebuild it from disk and note it.
|
|
6
6
|
|
|
7
7
|
| savepoint last line | latest delivered | lands at | continue with |
|
|
8
8
|
|---|---|---|---|
|
|
@@ -10,11 +10,11 @@ Classify from the last line alone, then verify only that line's artifact is real
|
|
|
10
10
|
| `Why started` (spec still sentinel) | What | **Why** | continue the conversation; rulings land as insights |
|
|
11
11
|
| `Why spec.md created` | Why | **How** | the action files, `plan.md`, `checklist.md` |
|
|
12
12
|
| `How started` / `How plan.md created` | (How in progress) | **How** | finish `plan.md` and `checklist.md` |
|
|
13
|
-
| `How checklist.md created` / `Exec started` | How | **Exec** | do the work,
|
|
13
|
+
| `How checklist.md created` / `Exec started` | How | **Exec** | do the work, check off the checklist |
|
|
14
14
|
| `Exec outcome.md created` | Exec | **ready to complete** | the ending procedure (`plastic-intent-ending`) |
|
|
15
15
|
| `Done delivered` / `Done abandoned` | terminal | **report only** | immutable; ask what is next |
|
|
16
16
|
|
|
17
|
-
## Per-
|
|
17
|
+
## Per-stage behaviour (what "continue" means)
|
|
18
18
|
|
|
19
19
|
- **Why**: continue the conversation, or run the work directly when the request is already
|
|
20
20
|
clear; every ruling is recorded as it lands.
|
|
@@ -19,7 +19,7 @@ step looks stale):
|
|
|
19
19
|
(see `SKILL.md`'s `## Conditional Ledger-Resume` for the full state table).
|
|
20
20
|
2. Confirm the artifact that line implies (`plan.md`, `checklist.md`, `outcome.md`, ...) is
|
|
21
21
|
present and non-empty on disk.
|
|
22
|
-
3. If the two disagree, the ledger
|
|
22
|
+
3. If the two disagree, the ledger is stale: rebuild it rather than hand-editing:
|
|
23
23
|
`ruby -r ~/.plastic/scripts/lib/savepoint -e 'Savepoint.rebuild_savepoint("<intent_dir>")'`
|
|
24
24
|
4. Re-read the rebuilt last line and re-derive the next step from `checklist.md`'s first
|
|
25
25
|
unchecked item.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
## ▶ {{id}} · {{name}}
|
|
2
|
+
|
|
3
|
+
| | | |
|
|
4
|
+
| --- | --- | --- |
|
|
5
|
+
| **Store** | {{store}} | {{store.note}} |
|
|
6
|
+
| **Status** | {{status}} | {{status.note}} |
|
|
7
|
+
| **Stage** | {{stage}} | {{stage.note}} |
|
|
8
|
+
| **Savepoint** | {{savepoint}} | {{savepoint.note}} |
|
|
9
|
+
| **Progress** | {{progress.bar}} {{progress.done}} / {{progress.total}} | {{progress.note}} |
|
|
10
|
+
| **Next** | {{next}} | {{next.note}} |
|
|
11
|
+
| **Insight** | {{insight}} | {{insight.note}} |
|
|
12
|
+
|
|
13
|
+
**What this means**
|
|
14
|
+
{{meaning}}
|
|
15
|
+
|
|
16
|
+
**Steps**
|
|
17
|
+
|
|
18
|
+
| Step | Status | What |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
{{steps.rows}}
|
|
21
|
+
|
|
22
|
+
{{close}}
|