@zalom/plastic 2.0.0-alpha.12 → 2.0.0-alpha.14

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.
Files changed (42) hide show
  1. package/hooks/message-display +31 -2
  2. package/package.json +1 -1
  3. package/scripts/dashboard.rb +238 -8
  4. package/scripts/doctor.rb +291 -4
  5. package/scripts/lib/dashboard_screen.rb +40 -0
  6. package/scripts/lib/doctor_core.rb +97 -2
  7. package/scripts/lib/hook_replay.rb +128 -0
  8. package/scripts/lib/installer_core.rb +23 -3
  9. package/scripts/lib/message_display.rb +151 -37
  10. package/scripts/lib/report_screen.rb +1139 -35
  11. package/scripts/lib/roadmap_queue.rb +19 -2
  12. package/scripts/lib/roadmap_savepoint.rb +36 -7
  13. package/scripts/lib/savepoint.rb +12 -0
  14. package/scripts/lib/screen_paint.rb +244 -12
  15. package/scripts/lib/screens/dashboard.rb +20 -0
  16. package/scripts/lib/screens/plan.rb +18 -0
  17. package/scripts/lib/screens/roadmap.rb +15 -0
  18. package/scripts/lib/session_ledger.rb +4 -0
  19. package/scripts/lib/verify_intent.rb +33 -0
  20. package/scripts/report-screen +127 -10
  21. package/scripts/savepoint-note +11 -9
  22. package/skills/auto/SKILL.md +13 -12
  23. package/skills/auto/references/human-report-contract.md +83 -8
  24. package/skills/dashboard/SKILL.md +13 -2
  25. package/skills/dashboard/templates/dashboard-global.md +1 -1
  26. package/skills/dashboard/templates/dashboard-project.md +2 -2
  27. package/skills/doctor/SKILL.md +10 -4
  28. package/skills/intent-continuing/SKILL.md +28 -26
  29. package/skills/intent-continuing/references/board-fill.md +9 -0
  30. package/skills/intent-ending/SKILL.md +6 -4
  31. package/skills/intent-executing/SKILL.md +2 -0
  32. package/skills/intent-speccing/SKILL.md +7 -4
  33. package/skills/roadmap/SKILL.md +9 -0
  34. package/skills/roadmap/references/file-format.md +10 -0
  35. package/templates/dashboard-screen.md +22 -0
  36. package/templates/display-fixture.md +21 -0
  37. package/templates/intent-screen.md +1 -1
  38. package/templates/report-plan.md +15 -0
  39. package/templates/report-roadmap-delivered.md +10 -0
  40. package/templates/report-roadmap-plan.md +9 -0
  41. package/templates/report-roadmap-state.md +9 -0
  42. package/templates/report-state.md +1 -1
@@ -2,15 +2,21 @@
2
2
  # encoding: UTF-8
3
3
  # frozen_string_literal: true
4
4
 
5
- # report-screen - the three delivery-report screens (intent 317): mid-delivery
6
- # state, post-delivery delivered, and delay. Each fills from the record via
5
+ # report-screen - the pre-delivery plan (intent 331b), the four delivery
6
+ # reports: mid-delivery state, post-delivery delivered, and delay (intent
7
+ # 317), plus session, the whole of one session's delivered work followed by
8
+ # the roster (intent 330). Each fills from the record via
7
9
  # scripts/lib/report_screen.rb; no number here is written by eye.
8
10
  #
9
11
  # Usage:
12
+ # report-screen plan <intent_dir> [--ansi]
10
13
  # report-screen state <intent_dir> [--changed "<text>"] [--ansi]
11
14
  # report-screen state --all <store_root> [--changed "<text>"] [--ansi]
12
15
  # report-screen delivered <intent_dir> [--ansi] [--repo <dir>]
13
16
  # report-screen delay <intent_dir> [--ansi]
17
+ # report-screen session <tier_root> [--session <id>] [--since <iso>]
18
+ # [--ledger-root <dir>] [--ansi]
19
+ # report-screen roadmap <roadmap.md> plan|state|delivered [--ansi] [--store-root <dir>]
14
20
  #
15
21
  # --ansi delegates to ScreenPaint (intent 317a, D1), the parser/re-layouter
16
22
  # in the shared TUI core. Selection is by capability, never by harness:
@@ -28,6 +34,14 @@ require "shellwords"
28
34
  require_relative "lib/report_screen"
29
35
  require_relative "lib/intent_screen"
30
36
  require_relative "lib/screen_paint"
37
+ require_relative "lib/session_ledger"
38
+
39
+ # Intent 331a (D6/R8): every caller-added screen kind file registers itself
40
+ # on load (ScreenPaint.register), so this glob is the ONLY wiring a new kind
41
+ # needs - no edit here, no edit to screen_paint.rb. Sorted for a
42
+ # deterministic load order; tolerates the directory being absent or empty
43
+ # (Dir.glob answers [] either way), which is this repo's own state today.
44
+ Dir.glob(File.join(__dir__, "lib", "screens", "*.rb")).sort.each { |f| require_relative f }
31
45
 
32
46
  def usage_abort(message)
33
47
  warn "report-screen: #{message}"
@@ -39,10 +53,13 @@ end
39
53
  # on the delivered screen is the release that merge shipped in. The
40
54
  # repository comes from --repo, else from projects.yml beside the store (the
41
55
  # installed layout, <home>/projects/<slug>/store/<id>), else the repository
42
- # this script lives in (the in-repo layout). No merge sha, no repository, or
43
- # no containing tag all answer nil, and the screen says "not recorded"
44
- # rather than guessing from HEAD (D14). The pure module never reads git.
45
- def resolve_repo(intent_dir, explicit_repo, script_dir)
56
+ # this script lives in (the in-repo layout, `include_self: true`) - unless
57
+ # `include_self: false` (the branch reader, D23), in which case a global
58
+ # intent's repo never falls back to the script's own repository, since that
59
+ # is not the intent's repository at all. No merge sha, no repository, or no
60
+ # containing tag all answer nil, and the screen says "not recorded" rather
61
+ # than guessing from HEAD (D14). The pure module never reads git.
62
+ def resolve_repo(intent_dir, explicit_repo, script_dir, include_self: true)
46
63
  candidates = []
47
64
  candidates << File.expand_path(explicit_repo) if explicit_repo
48
65
  if (m = intent_dir.match(%r{\A(.*)/projects/([^/]+)/store/[^/]+\z}))
@@ -59,7 +76,7 @@ def resolve_repo(intent_dir, explicit_repo, script_dir)
59
76
  candidates << File.expand_path(path) unless path.empty?
60
77
  end
61
78
  end
62
- candidates << File.expand_path("..", script_dir)
79
+ candidates << File.expand_path("..", script_dir) if include_self
63
80
  candidates.find { |c| File.exist?(File.join(c, ".git")) }
64
81
  end
65
82
 
@@ -78,12 +95,40 @@ def git_tag_reader(explicit_repo:, script_dir:)
78
95
  end
79
96
  end
80
97
 
98
+ # D9/D23: the injected branch reader. flow_base (a pure YAML read, never
99
+ # git) wins when the intent's project names a flow base; otherwise the
100
+ # repository's remote HEAD, else whichever of main/master exists. Resolves
101
+ # the repo with `include_self: false` (D23): a global-store intent must
102
+ # never fall back to the script's OWN repository (~/.plastic itself, on
103
+ # "main" in the installed layout) and print a confident, wrong branch.
104
+ def git_branch_reader(explicit_repo:, script_dir:)
105
+ lambda do |intent_dir|
106
+ flow = ReportScreen.flow_base(intent_dir)
107
+ next flow if flow && !flow.to_s.empty?
108
+
109
+ repo = resolve_repo(intent_dir, explicit_repo, script_dir, include_self: false)
110
+ next nil unless repo
111
+
112
+ branch = `git -C #{repo.shellescape} symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null`.strip
113
+ branch = branch.sub(%r{\Aorigin/}, "")
114
+ next branch unless branch.empty?
115
+
116
+ %w[main master].find do |b|
117
+ system("git", "-C", repo, "rev-parse", "--verify", "--quiet", b, out: File::NULL, err: File::NULL)
118
+ end
119
+ end
120
+ end
121
+
81
122
  args = ARGV.dup
82
123
  verb = args.shift
83
124
  changed = nil
84
125
  ansi = false
85
126
  template_path = nil
86
127
  repo_flag = nil
128
+ session_flag = nil
129
+ since_flag = nil
130
+ ledger_root_flag = nil
131
+ store_root_flag = nil
87
132
  positional = []
88
133
 
89
134
  while (arg = args.shift)
@@ -99,13 +144,21 @@ while (arg = args.shift)
99
144
  repo_flag = args.shift or usage_abort("--repo needs a path")
100
145
  when "--template"
101
146
  template_path = args.shift or usage_abort("--template needs a path")
147
+ when "--session"
148
+ session_flag = args.shift or usage_abort("--session needs a value")
149
+ when "--since"
150
+ since_flag = args.shift or usage_abort("--since needs a value")
151
+ when "--ledger-root"
152
+ ledger_root_flag = args.shift or usage_abort("--ledger-root needs a path")
153
+ when "--store-root"
154
+ store_root_flag = args.shift or usage_abort("--store-root needs a path")
102
155
  else
103
156
  usage_abort("unknown flag #{arg.inspect}") if arg.start_with?("--") && arg != "--all"
104
157
  positional << arg
105
158
  end
106
159
  end
107
160
 
108
- usage_abort("usage: report-screen state|delivered|delay <intent_dir> [--changed \"<text>\"] [--ansi]") unless verb
161
+ usage_abort("usage: report-screen plan|state|delivered|delay|session <intent_dir> [--changed \"<text>\"] [--ansi]") unless verb
109
162
 
110
163
  all_mode = positional.delete("--all") ? true : false
111
164
  target = positional.first
@@ -119,6 +172,16 @@ def paint(text, ansi_enabled, _renderer_path = nil)
119
172
  end
120
173
 
121
174
  case verb
175
+ when "plan"
176
+ usage_abort("usage: report-screen plan <intent_dir> [--ansi]") unless target
177
+ intent_dir = File.expand_path(target)
178
+ usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
179
+ store_root = File.expand_path("../..", intent_dir)
180
+ template_path ||= File.expand_path("../templates/report-plan.md", __dir__)
181
+ usage_abort("template not found at #{template_path}") unless File.exist?(template_path)
182
+ out = ReportScreen.render_plan(intent_dir: intent_dir, store_root: store_root,
183
+ template: File.read(template_path))
184
+ $stdout.write paint(out, ansi_enabled)
122
185
  when "state"
123
186
  if all_mode
124
187
  usage_abort("usage: report-screen state --all <store_root>") unless target
@@ -142,7 +205,8 @@ when "delivered"
142
205
  intent_dir = File.expand_path(target)
143
206
  usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
144
207
  out = ReportScreen.render_delivered(intent_dir: intent_dir,
145
- tag_reader: git_tag_reader(explicit_repo: repo_flag, script_dir: __dir__))
208
+ tag_reader: git_tag_reader(explicit_repo: repo_flag, script_dir: __dir__),
209
+ branch_reader: git_branch_reader(explicit_repo: repo_flag, script_dir: __dir__))
146
210
  $stdout.write paint(out, ansi_enabled)
147
211
  when "delay"
148
212
  usage_abort("usage: report-screen delay <intent_dir>") unless target
@@ -150,8 +214,61 @@ when "delay"
150
214
  usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
151
215
  out = ReportScreen.render_delay(intent_dir: intent_dir)
152
216
  $stdout.write paint(out, ansi_enabled)
217
+ when "session"
218
+ usage_abort("usage: report-screen session <tier_root> [--session <id>] [--since <iso>] [--ledger-root <dir>] [--ansi]") unless target
219
+ store_root = File.expand_path(target)
220
+ usage_abort("#{store_root} is not a store (no INDEX.md)") unless File.exist?(File.join(store_root, "INDEX.md"))
221
+
222
+ now = Time.now
223
+ session_id = session_flag || ENV[SessionLedger::SESSION_ID_ENV_KEY]
224
+ ledger_root = ledger_root_flag ? File.expand_path(ledger_root_flag) : ReportScreen.default_ledger_root(store_root)
225
+
226
+ if since_flag
227
+ begin
228
+ Time.parse(since_flag)
229
+ rescue ArgumentError
230
+ usage_abort("--since needs an ISO timestamp, got #{since_flag.inspect}")
231
+ end
232
+ end
233
+
234
+ dirs, skipped = ReportScreen.session_delivered_dirs(ledger_root: ledger_root, tier_root: store_root,
235
+ session: session_id, since: since_flag, now: now)
236
+ # D17: the widened window is announced whether the id was missing or simply
237
+ # matched nothing. A resumed background job carries a session id that is not
238
+ # the one on the ledger lines, and that case must not answer silently. An
239
+ # explicit --since is the caller naming the window, so it needs no note.
240
+ note =
241
+ if since_flag
242
+ nil
243
+ elsif session_id.nil? || session_id.to_s.strip.empty?
244
+ ReportScreen.window_note(ReportScreen.fallback_day(ledger_root, SessionLedger.day_id(now)),
245
+ "no session id given")
246
+ elsif !ReportScreen.session_tagged?(ledger_root: ledger_root, session: session_id, now: now)
247
+ ReportScreen.window_note(ReportScreen.fallback_day(ledger_root, SessionLedger.day_id(now)),
248
+ "this session has no line in the day ledger")
249
+ end
250
+
251
+ out = ReportScreen.render_session(
252
+ dirs: dirs, skipped: skipped, store_root: store_root,
253
+ tag_reader: git_tag_reader(explicit_repo: nil, script_dir: __dir__),
254
+ branch_reader: git_branch_reader(explicit_repo: nil, script_dir: __dir__),
255
+ note: note, now: now, painter: ->(text) { paint(text, ansi_enabled) }
256
+ )
257
+ $stdout.write out
258
+ when "roadmap"
259
+ usage_abort("usage: report-screen roadmap <roadmap.md> plan|state|delivered [--ansi] [--store-root <dir>]") if positional.empty?
260
+ roadmap_path = File.expand_path(positional[0])
261
+ usage_abort("#{roadmap_path} does not exist") unless File.exist?(roadmap_path)
262
+ sub_verb = positional[1]
263
+ usage_abort("usage: report-screen roadmap <roadmap.md> plan|state|delivered") unless sub_verb
264
+ unless %w[plan state delivered].include?(sub_verb)
265
+ usage_abort("unknown sub-verb #{sub_verb.inspect} (use plan|state|delivered)")
266
+ end
267
+ roadmap_store_root = store_root_flag ? File.expand_path(store_root_flag) : nil
268
+ out = ReportScreen.render_roadmap(path: roadmap_path, verb: sub_verb, store_root: roadmap_store_root)
269
+ $stdout.write paint(out, ansi_enabled)
153
270
  else
154
- usage_abort("unknown verb #{verb.inspect} (use state|delivered|delay)")
271
+ usage_abort("unknown verb #{verb.inspect} (use plan|state|delivered|delay|session|roadmap)")
155
272
  end
156
273
 
157
274
  exit 0
@@ -2,15 +2,16 @@
2
2
  # encoding: UTF-8
3
3
  # frozen_string_literal: true
4
4
 
5
- # savepoint-note - the writer CLI for the two savepoint kinds intent 317 adds:
6
- # Review (one line per plan-review or post-execution-review verdict) and
7
- # Commit (one line per commit landing during Exec). D17: there is no automatic
8
- # seam for either (session-commit writes the day ledger, not the intent
9
- # ledger; the executor's own commits are plain git), so this is an explicit
10
- # thin wrapper on Savepoint.append_review_savepoint / append_commit_savepoint.
5
+ # savepoint-note - the writer CLI for three savepoint kinds: Review (one line per
6
+ # plan-review or post-execution-review verdict) and Commit (one line per commit landing
7
+ # during Exec), both from intent 317, D17; and Report (one line per report screen printed,
8
+ # intent 331f). None has an automatic seam (session-commit writes the day ledger, not the
9
+ # intent ledger; the executor's own commits are plain git; a printed screen is a skill's own
10
+ # reply, not a file write), so this is an explicit thin wrapper on
11
+ # Savepoint.append_review_savepoint / append_commit_savepoint / append_report_savepoint.
11
12
  #
12
13
  # Usage:
13
- # savepoint-note <intent_dir> --kind Review|Commit --text "<text>"
14
+ # savepoint-note <intent_dir> --kind Review|Commit|Report --text "<text>"
14
15
  #
15
16
  # --text is normalized (D21): runs of two or more spaces collapse to one, so a
16
17
  # free-text kind that happens to contain a double space cannot key the dedup
@@ -46,12 +47,12 @@ while (arg = args.shift)
46
47
  end
47
48
  end
48
49
 
49
- usage_abort("usage: savepoint-note <intent_dir> --kind Review|Commit --text \"<text>\"") unless positional.length == 1
50
+ usage_abort("usage: savepoint-note <intent_dir> --kind Review|Commit|Report --text \"<text>\"") unless positional.length == 1
50
51
 
51
52
  intent_dir = File.expand_path(positional.first)
52
53
  usage_abort("#{intent_dir} is not an intent directory") unless File.exist?(Savepoint.intent_file(intent_dir))
53
54
 
54
- KINDS = %w[Review Commit].freeze
55
+ KINDS = %w[Review Commit Report].freeze
55
56
  usage_abort("--kind must be one of #{KINDS.join(', ')}, got #{kind.inspect}") unless KINDS.include?(kind)
56
57
  usage_abort("--text is required") if text.nil? || text.empty?
57
58
  usage_abort("--text must not contain a newline") if text.include?("\n")
@@ -62,6 +63,7 @@ usage_abort("--text is empty after normalization") if normalized.empty?
62
63
  case kind
63
64
  when "Review" then Savepoint.append_review_savepoint(intent_dir, normalized)
64
65
  when "Commit" then Savepoint.append_commit_savepoint(intent_dir, normalized)
66
+ when "Report" then Savepoint.append_report_savepoint(intent_dir, normalized)
65
67
  end
66
68
 
67
69
  exit 0
@@ -201,11 +201,9 @@ Then How.
201
201
  into the spec, the matrix, and the tests; record what was dropped and why in the action
202
202
  file's review fold. A REVISE verdict is folded and not re-reviewed unless a finding changes
203
203
  a decision.
204
- 5. Print `ruby ~/.plastic/scripts/report-screen state <intent_dir> --changed "How written, plan review next"`
205
- (D15; see `references/human-report-contract.md` for the full trigger list). This replaces
206
- the old prose State/Risk/Call briefing at this boundary. In auto mode the screen informs;
207
- it does not wait. The screen opens the reply with nothing before it and no code fence: the
208
- `MessageDisplay` hook paints only a reply whose first characters are the screen marker.
204
+ 5. Print `ruby ~/.plastic/scripts/report-screen plan <intent_dir>` as the first characters of
205
+ the reply, nothing before it, no fence, before dispatching the executor (see
206
+ `references/human-report-contract.md` for the full binding table). It informs; it does not wait.
209
207
 
210
208
  Then Exec.
211
209
 
@@ -272,13 +270,16 @@ Read `../plastic-conventions/references/completion-and-done.md` for what "intent
272
270
  --session "$CLAUDE_CODE_SESSION_ID" \
273
271
  --index-note "<what shipped>; <suite result>"
274
272
  ```
275
- Exit 4 means a live foreign session holds the lock; 5 means the worktree is dirty (commit
276
- first, or pass `--discard-worktree-changes` deliberately); 3 means the lock survived the
277
- disarm (`/plastic-doctor check the lock status`); 6 means the structure check refused. Never
278
- leave an orphaned worktree; run `git worktree prune` on a stale reference.
279
- 6. Print `ruby ~/.plastic/scripts/report-screen delivered <intent_dir>` once (D15): this is the
280
- owner report at End, replacing the old prose Done briefing. Print it as the first thing in
281
- the reply, nothing before it and no code fence, or the hook cannot paint it.
273
+ Exit 4: a live foreign session holds the lock. 5: the worktree is dirty (commit first, or
274
+ pass `--discard-worktree-changes` deliberately). 3: the lock survived the disarm
275
+ (`/plastic-doctor check the lock status`). 6: the structure check refused. Never leave an
276
+ orphaned worktree; run `git worktree prune` on a stale reference.
277
+ 6. Print `ruby ~/.plastic/scripts/report-screen delivered <intent_dir>` once (D15/331f), and
278
+ `report-screen state` at each of the five triggers in `references/human-report-contract.md`
279
+ (a review verdict, a blocker, a merge/release, or an owner status ask; a mid-batch ask
280
+ instead runs `report-screen session <tier_root> --session "$CLAUDE_CODE_SESSION_ID"`, intent
281
+ 330). Print each as the first characters of the reply: nothing before it, no fence, or the
282
+ hook cannot paint it.
282
283
 
283
284
  ## Error Handling
284
285
 
@@ -1,9 +1,12 @@
1
- # Human Report Contract (the three report screens, intent 317)
1
+ # Human Report Contract (the report screens, intent 317)
2
2
 
3
3
  D15: the prose EM-to-CTO briefing this doc used to define is retired. The orchestrator now
4
- prints one of three report screens, filled from the record by `scripts/report-screen`, never
4
+ prints one of these report screens, filled from the record by `scripts/report-screen`, never
5
5
  written by eye:
6
6
 
7
+ - **`report-screen plan <intent_dir>`** - the pre-delivery report (intent 331b), printed once
8
+ at the How boundary, before the executor is dispatched: Asked, Decisions, Steps, Mode,
9
+ Reviewer, then the Steps table (Step, Action, What) and the Risks table.
7
10
  - **`report-screen state <intent_dir> [--changed "<text>"]`** - the mid-delivery report. One
8
11
  intent's field table (Store, Status, Stage, Savepoint, Progress, Next, Insight) plus a
9
12
  `Changed` row naming what caused the print, and its Steps table.
@@ -14,6 +17,59 @@ written by eye:
14
17
  Asked, Delivered (with a Proven-by column), Evidence, Needs you.
15
18
  - **`report-screen delay <intent_dir>`** - printed only on request ("why did X take so long"):
16
19
  the delivery as a timeline plus the derived `Where the time went` line.
20
+ - **`report-screen session <tier_root>`** - the answer to an UNNAMED status ask ("where are we
21
+ with delivery", "what is the status"): one `delivered` screen per intent this session
22
+ completed, oldest first, then the `state --all` roster. Intent 330's ruling: a status ask
23
+ answers with what actually shipped, not the in-flight roster alone.
24
+ - **`dashboard.rb continue|project <slug> --screen`** - the dashboard screen (intent 331d):
25
+ Active, In delivery, Delivered, Roadmap, Sessions, Changed, then the Where-we-are and
26
+ Where-we-go-next tables. A separate script from the other four (`dashboard.rb`, not
27
+ `report-screen`), since it aggregates across a whole store or project rather than one
28
+ intent; it prints on `continue` and on loading a project, not as a delivery trigger.
29
+
30
+ ## Binding table (intent 331f)
31
+
32
+ Every skill that shows state names its own report verb, one row per skill and trigger. Each
33
+ bound skill's file carries the SAME rule next to its verb: print the screen as the first
34
+ characters of the reply, nothing before it, no fence, or the hook cannot paint it.
35
+
36
+ | Skill | Trigger | Verb |
37
+ |---|---|---|
38
+ | `plastic-intent-continuing` | project route (continue, load project) | `dashboard.rb ... --screen` |
39
+ | `plastic-intent-continuing` | a named intent | `report-screen state` |
40
+ | `plastic-intent-continuing` | "where are we" (a status ask) | `report-screen session` |
41
+ | `plastic-intent-continuing` | "why so long" | `report-screen delay` |
42
+ | `plastic-intent-continuing` | a roadmap route | `report-screen roadmap ... state` |
43
+ | `plastic-auto` | the How boundary, before the executor | `report-screen plan` |
44
+ | `plastic-auto` | each of the five triggers | `report-screen state` |
45
+ | `plastic-auto` | close | `report-screen delivered` |
46
+ | `plastic-intent-ending` | the close | `report-screen delivered` |
47
+ | `plastic-intent-speccing` | the action files are written | `report-screen plan` |
48
+ | `plastic-roadmap` | create | `report-screen roadmap ... plan` |
49
+ | `plastic-roadmap` | read | `report-screen roadmap ... state` |
50
+ | `plastic-roadmap` | close | `report-screen roadmap ... delivered` |
51
+ | `plastic-dashboard` | any invocation | `dashboard.rb ... --screen` |
52
+ | `plastic-intent-executing` | after the red commit, and after the suite | `report-screen state` |
53
+
54
+ ## A roadmap's own three reports (intent 331c)
55
+
56
+ A roadmap gets the same pre-, in-, and post-delivery shape as an intent, through
57
+ `report-screen roadmap <roadmap.md> plan|state|delivered [--ansi] [--store-root <dir>]`:
58
+
59
+ - **`roadmap plan`** - the pre-delivery report: the Goal's first sentence, the batch (or legacy
60
+ wave) count and intent count, the batch order, and when the roadmap was created; then the
61
+ full entries table.
62
+ - **`roadmap state`** - the in-delivery report: Goal, a Progress bar over intents delivered of
63
+ intents total (never batches), the frontier batch, who is delivering it and their lead, the
64
+ next queued entry, and the last ledger event; then the entries table with each entry's own
65
+ checklist progress and lead.
66
+ - **`roadmap delivered`** - the post-delivery report: a meta line (closed time, or `in progress`
67
+ when the goal is not yet reached; intent count; duration) directly under the title, the
68
+ delivered table with each entry's merge sha, and the `## Log` table.
69
+
70
+ Every cell traces to the roadmap file, `INDEX.md` (which always wins on status), the roadmap's
71
+ own savepoint ledger, or (falling back when no ledger file exists) the roadmap's `## Log` -
72
+ never a second parser: `RoadmapQueue`'s own public `roadmap` reader supplies every entry.
17
73
 
18
74
  ## The five triggers for `state`
19
75
 
@@ -38,9 +94,9 @@ on harness name to decide.
38
94
 
39
95
  ## Depth for small work
40
96
 
41
- For small work in auto mode, only the How-boundary `state` screen prints mid-flight (its
42
- `Changed` row names what the What and Why steps did, since there is no separate briefing per
43
- stage any more). Larger work prints `state` at every trigger in the table above. This is a
97
+ For small work in auto mode, only the How-boundary `plan` screen prints mid-flight (intent
98
+ 331f moved this print off `state`, since there is no separate briefing per stage any more).
99
+ Larger work prints `state` at every trigger in the table above. This is a
44
100
  depth cut, not a different report: the screen's shape never changes, only how often it fires.
45
101
  A delivery still ends with `outcome.md` plus one `delivered` screen.
46
102
 
@@ -73,6 +129,25 @@ In guided mode, `state` prints at each stage boundary and the human decides befo
73
129
  stage starts.
74
130
 
75
131
  In auto mode, `state` prints at every trigger for larger work; for small work only the How
76
- boundary prints (see `## Depth for small work` above). The orchestrator takes the go-ahead
77
- itself and moves on, except at the existing hard stops (destructive action without a safe
78
- alternative, project-path confirm).
132
+ boundary's `plan` screen prints (see `## Depth for small work` above). The orchestrator takes
133
+ the go-ahead itself and moves on, except at the existing hard stops (destructive action without
134
+ a safe alternative, project-path confirm).
135
+
136
+ ## Column vocabulary (D5, intent 331f)
137
+
138
+ Owner ruling 2026-09-05 11:05 UTC: "What" is never a column name, because What is a stage, not
139
+ a value. The id column reads `Graph ID`; the title column reads `Intent`. Every Steps table
140
+ reads `Step | Status | Detail` (the plan screen's own Steps table reads
141
+ `Step | Action | Detail`); every Risks table reads `N | Risk`; the Delivered/Evidence/Needs-you
142
+ tables on the `delivered` screen read `Row | Detail | Proven by`, `Kind | Detail | Source`, and
143
+ `N | Need | Reason`. The plan screen's Asked row prints the intent title before its first
144
+ colon, never the whole intent line. This applies to every screen the family prints: `state`,
145
+ `roster`, `session`, `delivered`, `delay`, `plan`, `roadmap` (`plan`/`state`/`delivered`), and
146
+ `dashboard`. `outcome.md`'s own `| Row | What |` heading is an AUTHORING convention inside the
147
+ file a human writes, never a rendered header, and stays unchanged.
148
+
149
+ ## Width bound (D7, intent 331f)
150
+
151
+ No rendered row passes 115 visible columns. A long cell (the roadmap Goal, an intent title, an
152
+ Asked line) truncates on a word boundary with a single ellipsis; `ReportScreen.fit_screen`
153
+ shrinks a table's widest column first, floor 8, before it ever truncates a whole row.
@@ -7,7 +7,8 @@ user-invocable: false
7
7
  # Dashboard — Plastic Work Cockpit
8
8
 
9
9
  A deterministic overview of the intent store(s). It answers three questions at a glance:
10
- **where we are** (recently worked), **where we go next** (the most-valuable next work), and
10
+ **where we are** (recently worked; `Graph ID | Intent | Stage | Progress | Lead`),
11
+ **where we go next** (the most-valuable next work; `Rank | Graph ID | Intent | Reason`), and
11
12
  **how to conduct it** (a disposition per intent). The human-facing surface is **Markdown**,
12
13
  because the user's UI renders Markdown natively but collapses raw tool-call stdout.
13
14
 
@@ -22,6 +23,16 @@ state → byte-identical payload, regardless of model. Do NOT hand-summarize int
22
23
  - `plastic-intent-continuing` lands on the board on a bare resume
23
24
  - `plastic-auto` reads `--json` to choose the next dispatchable intent
24
25
 
26
+ ## The screen surface (intent 331d/331f)
27
+
28
+ `dashboard.rb project <slug> --screen` (or `continue --screen` for the global board) is the
29
+ default surface on every invocation: print it as the first characters of the reply, nothing
30
+ before it, no fence, or the hook cannot paint it. It carries a title, six fields (Active, In
31
+ delivery, Delivered, Roadmap, Sessions, Changed), then the Where-we-are and Where-we-go-next
32
+ tables, its own grammar and painted form standing in for the filled Markdown template below.
33
+ `plastic-intent-continuing`'s project route prints it this way. The Markdown board (Step 1-4
34
+ below) stays available as the deeper prose surface a reader can ask for.
35
+
25
36
  ## Procedure (the Markdown board — default human surface)
26
37
 
27
38
  ### Step 1 — Get the data payload
@@ -70,7 +81,7 @@ Fill mechanically, no rewriting, no re-sorting:
70
81
  re-truncate, or reword them. Never emit `<br>`.
71
82
  - `next_work` → `| {id} | {what} | {value} | {disposition} | {flags_label} |`
72
83
  - `active` → `| {id} | {what} | {stage} | {worker} | {activity} |`
73
- Empty list → one full-width row with `_(none)_` in the Id column and every other cell blank,
84
+ Empty list → one full-width row with `_(none)_` in the Graph ID column and every other cell blank,
74
85
  matching that table's column count (e.g. `| _(none)_ | | | | |` for the 5-column next_work
75
86
  table, `| _(none)_ | | | |` for the 5-column active table). Neither list carries an overflow
76
87
  "+N more" row anymore (D5, intent 202): the true pool size rides on the payload as
@@ -11,7 +11,7 @@
11
11
 
12
12
  ## Most-valuable next work
13
13
 
14
- | Id | What | Value | Disposition | Flags |
14
+ | Graph ID | Intent | Value | Disposition | Flags |
15
15
  | --- | --- | --- | --- | --- |
16
16
  {{next_work.rows}}
17
17
 
@@ -4,13 +4,13 @@
4
4
 
5
5
  **Active**
6
6
 
7
- | Id | What | Stage | Worker | Activity |
7
+ | Graph ID | Intent | Stage | Worker | Activity |
8
8
  | --- | --- | --- | --- | --- |
9
9
  {{active.rows}}
10
10
 
11
11
  **Most-valuable next work**
12
12
 
13
- | Id | What | Value | Disposition | Flags |
13
+ | Graph ID | Intent | Value | Disposition | Flags |
14
14
  | --- | --- | --- | --- | --- |
15
15
  {{next_work.rows}}
16
16
 
@@ -78,10 +78,15 @@ project alone.
78
78
  Runs the install-wide surface: agent registration, core files (including config-honoring
79
79
  drift), manifest sync is core-only and not part of this run, deprecation checks, config-ask
80
80
  checks, install-integrity checks, skill-lint (advisory), QMD reachability (unscoped, every
81
- collection), and the global store's own conventions/done-signals content. **Never carries a
82
- per-project finding**; that is `--store <slug>`'s job (see above). This is what
83
- `/plastic-doctor` invokes, and it also runs automatically after every `plastic-update`
84
- (informational, does not block or revert the update).
81
+ collection), the global store's own conventions/done-signals content, and the `display`
82
+ category (intent 331e): `display_hook_registered` (also runs at `--core`), `display_hook_paints`
83
+ (replays the shipped display fixture through the installed MessageDisplay hook and expects a
84
+ painted screen back, a pass when a known defeater like `NO_COLOR` is active, never a fail),
85
+ `display_not_defeated` (warns per active defeater and always names the undetectable verbose
86
+ transcript view), and `display_surfaces_documented` (the harness-adapters doc still names every
87
+ surface class). **Never carries a per-project finding**; that is `--store <slug>`'s job (see
88
+ above). This is what `/plastic-doctor` invokes, and it also runs automatically after every
89
+ `plastic-update` (informational, does not block or revert the update).
85
90
 
86
91
  ## When to Use
87
92
 
@@ -167,6 +172,7 @@ Use the `fix_hint` value to determine the correct action:
167
172
  | "Remove each listed .tmp/<session>/ directory after confirming that session is gone" | For each listed directory, confirm no live session uses it (a live session rewrites its heartbeat on every prompt and edit), then remove that directory by hand; never remove an unlisted one |
168
173
  | "For a day directory missing its <day>.md, run `file-session-intent --day <day> ...`" | Run `ruby ~/.plastic/scripts/file-session-intent --day <day> --carry-to <today> --store <store>` for the named day; rename or remove an entry that is not a `YYYYMMDD` day directory |
169
174
  | "Run scripts/project-links ... PRESERVES ... --drop-unbacked-links" | Run `ruby ~/.plastic/scripts/maintenance-run --tool project-links --intent <id> --apply` for the one flagged id (never run bare `project-links` against a real store outside the rare owner-approved batch exception, D2) |
175
+ | "Re-run the Plastic installer to repair the hook registration ... (plastic-install --repair)" (`display_hook_registered`) | Run `npx -y @zalom/plastic@<channel> install --reinstall --claude` (the `plastic-install` skill's repair mode), then re-run doctor |
170
176
 
171
177
  For fixes the agent cannot handle automatically, explain what the user needs
172
178
  to do manually. The `revisions.md` remedy is curator-applied (a move-and-record
@@ -45,32 +45,29 @@ specific intent or roadmap named.").
45
45
 
46
46
  ## Project route: land on the board
47
47
 
48
- Land on the Markdown board through the `plastic-dashboard` skill; rendering belongs there.
49
- Run the data payload and fill the matching template:
50
- - project loaded: `ruby ~/.plastic/scripts/dashboard.rb project <slug> --data`
51
- - otherwise (the global fallback): `ruby ~/.plastic/scripts/dashboard.rb continue --data`
52
-
53
- Fill the template from `plastic-dashboard`'s `templates/` and present the filled Markdown in
54
- your reply, every time: tool-call stdout and hook context are invisible to the user. Read
55
- `references/board-fill.md` for the fill mechanics and the store-health line when filling the
56
- board. The board load runs the scoped store check (`doctor --store <scope>`); its result
57
- arrives in the payload as `store_health` and is shown as one line of data, never a blocker.
58
-
59
- Priority order on the board: active intents first, then project context (governing plus
60
- tactical intents in a registered project), then stale future intents for triage, then fresh
61
- future intents as next work. A future intent older than `stale_threshold_days` (default 3) is
62
- surfaced for triage without action: activate, abandon, or leave. Activating moves it to
48
+ Print `ruby ~/.plastic/scripts/dashboard.rb project <slug> --screen` (or `continue --screen`
49
+ for the global fallback) as the first characters of the reply: nothing before it, no fence, or
50
+ the hook cannot paint it (intent 331d/331f). It carries a title, six fields (Active, In
51
+ delivery, Delivered, Roadmap, Sessions, Changed), then the Where-we-are and Where-we-go-next
52
+ tables. The board load runs the scoped store check (`doctor --store <scope>`); its result
53
+ rides in the payload as `store_health` and prints as one line of data, never a blocker.
54
+
55
+ Priority order on the underlying data: active intents first, then project context (governing
56
+ plus tactical intents in a registered project), then stale future intents for triage, then
57
+ fresh future intents as next work. A future intent older than `stale_threshold_days` (default
58
+ 3) is surfaced for triage without action: activate, abandon, or leave. Activating moves it to
63
59
  `## Active` in `INDEX.md` and auto-commits. The board's ranked next-work order is computed by
64
60
  `dashboard.rb`; cite the rule names only (Effort, Value, Flags, Override, Caps) and read
65
61
  `plastic-dashboard`'s `references/classification.md` for their definitions.
66
62
 
67
63
  When the tier root (the directory holding `INDEX.md`) has a mid-flight roadmap
68
64
  (`ruby ~/.plastic/scripts/roadmap-next --roadmaps-dir <root>/roadmaps` reports a `state`
69
- other than `none`), say so in one line and offer the roadmap route; the board still presents
65
+ other than `none`), say so in one line and offer the roadmap route; the screen still presents
70
66
  project state and stops.
71
67
 
72
68
  Then stop: "here is the state, what next?". Do not start executing work. When the user names
73
- an intent, take the intent route.
69
+ an intent, take the intent route. Read `references/board-fill.md` only when the reader asks
70
+ for the deeper prose board (`plastic-dashboard`'s Markdown surface still exists for that ask).
74
71
 
75
72
  ## Intent route: resume one intent from its ledger
76
73
 
@@ -111,11 +108,15 @@ For a live intent's directory:
111
108
  `ruby ~/.plastic/scripts/report-screen state <intent_dir>` and print its output as it is:
112
109
  the title, the field table, the `Changed` row, and the Steps table come from the record,
113
110
  never by eye. For "where are we" with no intent named, run
114
- `ruby ~/.plastic/scripts/report-screen state --all <store_root>` for the roster across every
115
- in-delivery intent. Route "why did X take so long" to
116
- `ruby ~/.plastic/scripts/report-screen delay <intent_dir>` instead - every verb prints the
117
- same plain screen on any harness, painted only where the harness supports it, with no
118
- branching on harness name. Under the `state` screen
111
+ `ruby ~/.plastic/scripts/report-screen session <tier_root> --session <this session's id>`
112
+ (intent 330; pass the id your harness gives you, or the screen widens to the whole day and
113
+ says so): it prints one
114
+ `delivered` screen per intent this session actually completed, oldest first, then the same
115
+ roster `report-screen state --all <store_root>` prints on its own - `state --all` stays the
116
+ right call when only the in-flight roster is wanted, with nothing delivered above it. Route
117
+ "why did X take so long" to `ruby ~/.plastic/scripts/report-screen delay <intent_dir>`
118
+ instead - every verb prints the same plain screen on any harness, painted only where the
119
+ harness supports it, with no branching on harness name. Under the `state` screen
119
120
  write **What this means** as two to four bullets in plain words (what the intent is for,
120
121
  what has landed, what is left, any defect named by step), then close with **needs input:**
121
122
  naming the first open step. Then continue the work in the
@@ -135,10 +136,11 @@ For a live intent's directory:
135
136
  `delivering` or `blocked` entry wins, else the newest ledger or `## Log` timestamp.
136
137
  `roadmaps/<slug>.savepoint.md` is a derived signal read here, never a status field;
137
138
  `INDEX.md` stays the sole status writer.
138
- 2. **Present state:** the roadmap's `## Goal`, the current batch with each entry's mirrored
139
- status, the ledger's newest line beside the newest `## Log` line. Read
140
- `../plastic-conventions/references/roadmaps.md` for the file format and the status-mirror
141
- rule when a roadmap file needs interpreting.
139
+ 2. **Print state.** Print `ruby ~/.plastic/scripts/report-screen roadmap <roadmap.md> state` as
140
+ the first characters of the reply: nothing before it, no fence, or the hook cannot paint it.
141
+ It carries Goal, Progress, Frontier, Delivering, Next, and Changed, then the entries table -
142
+ never hand-typed. Read `../plastic-conventions/references/roadmaps.md` for the file format
143
+ and the status-mirror rule when a roadmap file needs interpreting.
142
144
  3. Then continue with the next dispatchable entry in the session's mode: direct work on it,
143
145
  or `plastic-auto` when the owner says auto. The coordinator that drives a batch appends
144
146
  to `roadmaps/<slug>.savepoint.md` at its dispatch, merge, park, and handoff points with
@@ -41,3 +41,12 @@ The project route's default target is the project board. When no project is load
41
41
  case where this route is reached without a registered project in scope), fall back to the
42
42
  global board payload (`dashboard.rb continue --data`) rather than failing. This mirrors the
43
43
  router's D6 default: a bare "continue" always lands somewhere useful.
44
+
45
+ ## The screen surface (intent 331d)
46
+
47
+ `dashboard.rb project <slug> --screen` (or `continue --screen`) prints the identical state -
48
+ Active, In delivery, Delivered, Roadmap, Sessions, Changed, then the Where-we-are and
49
+ Where-we-go-next tables - as a screen with its own grammar instead of a filled Markdown
50
+ template. It replaces this fill mechanism once intent 331f wires the project route to print
51
+ it first, the way the intent route already prints the intent screen first today; until then,
52
+ this page's fill rules stay the route's own surface.