@zalom/plastic 1.0.0-alpha.20 → 1.0.0-alpha.22

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalom/plastic",
3
- "version": "1.0.0-alpha.20",
3
+ "version": "1.0.0-alpha.22",
4
4
  "description": "Intent-driven idea development system for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -87,6 +87,22 @@ def intent_dirs(store)
87
87
  .sort
88
88
  end
89
89
 
90
+ # Last-accessed timestamp: the timestamp of the last ISO8601 line in the
91
+ # deterministic savepoint ledger (intent 34); falls back to the created date.
92
+ ISO8601_RE = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\b/
93
+
94
+ def last_accessed_at(dir, created)
95
+ sp = File.join(dir, "savepoint.md")
96
+ if File.exist?(sp)
97
+ File.readlines(sp).reverse_each do |line|
98
+ m = line.strip.match(ISO8601_RE)
99
+ return m[0] if m
100
+ end
101
+ end
102
+ return "#{created}T00:00:00Z" if created && !created.empty?
103
+ ""
104
+ end
105
+
90
106
  # Parse one intent directory into a raw record.
91
107
  def parse_intent(store_info, dir_name, status_index)
92
108
  dir = File.join(store_info[:store], dir_name)
@@ -124,6 +140,7 @@ def parse_intent(store_info, dir_name, status_index)
124
140
  savepoint: has.("savepoint.md"),
125
141
  checklist_partial: has.("checklist.md") && checklist_partially_done?(File.join(dir, "checklist.md")),
126
142
  body_has_context: body.include?("## Context"),
143
+ last_accessed_at: last_accessed_at(dir, (fm["created"].to_s rescue "")),
127
144
  }
128
145
  end
129
146
 
@@ -155,7 +172,9 @@ def load_all
155
172
  done_ids[[rec[:scope], rec[:id]]] = true if rec[:status] == "completed"
156
173
  end
157
174
  end
158
- [all, done_ids]
175
+ referenced = {}
176
+ all.each { |r| r[:sources].each { |s| referenced[[r[:scope], s]] = true } }
177
+ [all, done_ids, referenced]
159
178
  end
160
179
 
161
180
  # ---------------------------------------------------------------------------
@@ -200,19 +219,22 @@ end
200
219
  # Value -> :high | :low (explicit frontmatter field wins).
201
220
  # High is deliberately rare: an explicit stamp, or a human-authored root idea that has
202
221
  # already spawned follow-on work (chain non-empty) — i.e. a strategic theme the user owns.
203
- def value_of(rec)
222
+ def value_of(rec, referenced = {})
204
223
  case rec[:value_field]
205
224
  when "high" then return :high
206
225
  when "low" then return :low
207
226
  end
208
- return :high if rec[:author] == "human" && root_intent?(rec[:id]) && !rec[:chain].empty?
227
+ return :high if rec[:author] == "human" && root_intent?(rec[:id])
228
+ return :high unless rec[:chain].empty?
229
+ return :high if referenced[[rec[:scope], rec[:id]]]
209
230
  :low
210
231
  end
211
232
 
212
233
  def flags_of(rec, done_ids)
213
234
  flags = []
214
235
  flags << "in-progress" if rec[:savepoint] || rec[:checklist_partial]
215
- if !rec[:sources].empty? && rec[:sources].any? { |s| done_ids[[rec[:scope], s]] }
236
+ if rec[:status] == "future" && !rec[:sources].empty? &&
237
+ rec[:sources].all? { |s| done_ids[[rec[:scope], s]] }
216
238
  flags << "unblocked"
217
239
  end
218
240
  age = stale_age(rec)
@@ -244,9 +266,9 @@ def disposition_of(type, quadrant)
244
266
  end
245
267
  end
246
268
 
247
- def classify(rec, done_ids)
269
+ def classify(rec, done_ids, referenced = {})
248
270
  type = intent_type(rec)
249
- value = value_of(rec)
271
+ value = value_of(rec, referenced)
250
272
  effort = effort_of(rec, type)
251
273
  quadrant = QUADRANTS[[value, effort]]
252
274
  disposition = disposition_of(type, quadrant)
@@ -280,6 +302,12 @@ LIFECYCLE_GLYPH = { "what" => "○", "why" => "◔", "how" => "◑", "exec" => "
280
302
  DISPOSITION_GLYPH = { "drive" => "▸", "defer" => "⇢", "research" => "⊙", "triage" => "⚑" }.freeze
281
303
  LEGEND = "legend ○ What ◔ Why ◑ How ◕ Exec ● Done │ ▸drive ⇢defer ⊙research ⚑triage │ ⇡unblocked"
282
304
 
305
+ # Markdown board glyphs (intent 37). Quadrant signatures + per-line bullets.
306
+ QUADRANT_BULLET = {
307
+ "quick_win" => "⚡", "next_big" => "★", "defer" => "→", "triage" => "⚑",
308
+ }.freeze
309
+ STATUS_GLYPH = { "active" => "◑", "completed" => "●", "future" => "○" }.freeze
310
+
283
311
  # ---------------------------------------------------------------------------
284
312
  # Rendering helpers
285
313
  # ---------------------------------------------------------------------------
@@ -421,6 +449,133 @@ def render_all(records)
421
449
  out.join("\n") + "\n"
422
450
  end
423
451
 
452
+ # ---------------------------------------------------------------------------
453
+ # Markdown-board data payload (intent 37) — heavy side; the skill fills a
454
+ # Markdown template from this and presents it. Deterministic, golden-tested.
455
+ # ---------------------------------------------------------------------------
456
+
457
+ # Descending sort key for an ISO8601 / date string without reversing arrays.
458
+ def invert_ts(ts)
459
+ ts.to_s.ljust(20).chars.map { |c| 255 - c.ord }
460
+ end
461
+
462
+ def within_24h?(rec)
463
+ ts = rec[:last_accessed_at]
464
+ return false if ts.nil? || ts.empty?
465
+ d = (Date.parse(ts[0, 10]) rescue nil)
466
+ d && d >= (today - 1)
467
+ end
468
+
469
+ def worked_row(rec, project_scope)
470
+ glyph = STATUS_GLYPH[rec[:status]]
471
+ proj = rec[:scope] == "global" ? "global" : rec[:scope].sub("project:", "")
472
+ status_word = rec[:status] == "completed" ? "done" : rec[:status]
473
+ prefix = project_scope ? "" : "#{proj} | "
474
+ {
475
+ id: rec[:id], status: rec[:status], glyph: glyph,
476
+ last_accessed_at: rec[:last_accessed_at],
477
+ line: "#{glyph} #{prefix}#{status_word}: #{rec[:id]} #{rec[:intent]}".strip,
478
+ }
479
+ end
480
+
481
+ def recently_worked(records, project_scope: nil)
482
+ pool = records.select do |r|
483
+ %w[active completed].include?(r[:status]) && within_24h?(r) &&
484
+ (project_scope.nil? || r[:scope] == project_scope)
485
+ end
486
+ ordered = pool.sort_by { |r| [r[:status] == "active" ? 0 : 1, invert_ts(r[:last_accessed_at])] }
487
+ ordered = ordered.first(5) if ordered.size > 15
488
+ ordered.map { |r| worked_row(r, project_scope) }
489
+ end
490
+
491
+ def intent_line(rec, bullet)
492
+ note = rec[:status] == "active" ? " (#{rec[:lifecycle].to_s.capitalize})" : ""
493
+ { id: rec[:id], intent: rec[:intent], created: rec[:created], bullet: bullet,
494
+ scope: rec[:scope], line: "#{bullet} #{rec[:id]} #{rec[:intent]}#{note}".rstrip }
495
+ end
496
+
497
+ def matrix_data(records)
498
+ cells = { "quick_win" => [], "next_big" => [], "defer" => [], "triage" => [] }
499
+ research = []
500
+ records.each do |r|
501
+ if %w[research exploration].include?(r[:type]) then research << r
502
+ else cells[r[:quadrant]] << r end
503
+ end
504
+ by_created_desc = ->(list) { list.sort_by { |r| invert_ts(r[:created]) } }
505
+ out = {}
506
+ cells.each { |q, list| out[q] = by_created_desc.call(list).map { |r| intent_line(r, QUADRANT_BULLET[q]) } }
507
+ out["research"] = by_created_desc.call(research).map { |r| intent_line(r, "🔬") }
508
+ out
509
+ end
510
+
511
+ def short_description(scope)
512
+ return "" unless scope.start_with?("project:")
513
+ slug = scope.sub("project:", "")
514
+ agents = File.join(PLASTIC_HOME, "projects", slug, "AGENTS.md")
515
+ if File.exist?(agents)
516
+ File.readlines(agents).each do |l|
517
+ t = l.strip
518
+ next if t.empty? || t.start_with?("#", ">")
519
+ return t[0, 60]
520
+ end
521
+ end
522
+ slug
523
+ end
524
+
525
+ def project_summaries(records)
526
+ scopes = records.map { |r| r[:scope] }.select { |s| s.start_with?("project:") }.uniq
527
+ rows = scopes.map do |scope|
528
+ scoped = records.select { |r| r[:scope] == scope }
529
+ {
530
+ slug: scope.sub("project:", ""),
531
+ description: short_description(scope),
532
+ active: scoped.count { |r| r[:status] == "active" },
533
+ done: scoped.count { |r| r[:status] == "completed" },
534
+ future: scoped.count { |r| r[:status] == "future" },
535
+ last_accessed_at: scoped.map { |r| r[:last_accessed_at] }.reject(&:empty?).max.to_s,
536
+ }
537
+ end
538
+ rows.sort_by { |r| invert_ts(r[:last_accessed_at]) }.first(5)
539
+ end
540
+
541
+ def counts_of(records)
542
+ { active: records.count { |r| r[:status] == "active" },
543
+ done: records.count { |r| r[:status] == "completed" },
544
+ future: records.count { |r| r[:status] == "future" } }
545
+ end
546
+
547
+ def render_data_global(records)
548
+ global = records.select { |r| r[:scope] == "global" }
549
+ matrix_pool = global.select { |r| actionable?(r) && r[:status] != "active" }
550
+ projs = project_summaries(records)
551
+ { mode: "global", date: today.to_s,
552
+ recently_worked: recently_worked(records),
553
+ matrix: matrix_data(matrix_pool),
554
+ counts: counts_of(global),
555
+ projects: projs,
556
+ project_totals: {
557
+ active: projs.sum { |p| p[:active] }, done: projs.sum { |p| p[:done] },
558
+ future: projs.sum { |p| p[:future] }
559
+ } }
560
+ end
561
+
562
+ def render_data_project(records, slug)
563
+ scope = "project:#{slug}"
564
+ scoped = records.select { |r| r[:scope] == scope }
565
+ matrix_pool = scoped.select { |r| r[:status] == "future" }
566
+ { mode: "project", date: today.to_s, slug: slug,
567
+ description: short_description(scope),
568
+ recently_worked: recently_worked(records, project_scope: scope),
569
+ matrix: matrix_data(matrix_pool),
570
+ counts: counts_of(scoped),
571
+ active: scoped.select { |r| r[:status] == "active" }
572
+ .sort_by { |r| invert_ts(r[:last_accessed_at]) }
573
+ .map { |r| intent_line(r, STATUS_GLYPH["active"]) },
574
+ future: scoped.select { |r| r[:status] == "future" }
575
+ .sort_by { |r| invert_ts(r[:created]) }
576
+ .map { |r| intent_line(r, STATUS_GLYPH["future"]) } }
577
+ end
578
+
424
579
  # ---------------------------------------------------------------------------
425
580
  # JSON renderer (agent / auto-mode contract)
426
581
  # ---------------------------------------------------------------------------
@@ -448,11 +603,18 @@ end
448
603
 
449
604
  def main(argv)
450
605
  json = argv.delete("--json")
606
+ data = argv.delete("--data")
451
607
  mode = argv.shift || "continue"
452
608
  slug = argv.shift
453
609
 
454
- raw, done_ids = load_all
455
- records = raw.map { |r| classify(r, done_ids) }
610
+ raw, done_ids, referenced = load_all
611
+ records = raw.map { |r| classify(r, done_ids, referenced) }
612
+
613
+ if data
614
+ payload = mode == "project" ? render_data_project(records, slug) : render_data_global(records)
615
+ puts JSON.pretty_generate(payload)
616
+ return 0
617
+ end
456
618
 
457
619
  if json
458
620
  subset = mode == "project" ? records.select { |r| r[:scope] == "project:#{slug}" } : records
package/scripts/doctor.rb CHANGED
@@ -62,6 +62,7 @@ class Doctor
62
62
  def parse_args(argv)
63
63
  agent = "claude"
64
64
  help = false
65
+ core = false
65
66
 
66
67
  i = 0
67
68
  while i < argv.length
@@ -74,6 +75,9 @@ class Doctor
74
75
  $stderr.puts "Error: --agent requires one of: #{agents.keys.join(", ")}"
75
76
  exit 2
76
77
  end
78
+ when "--core"
79
+ core = true
80
+ i += 1
77
81
  when "--help", "-h"
78
82
  help = true
79
83
  i += 1
@@ -82,7 +86,7 @@ class Doctor
82
86
  end
83
87
  end
84
88
 
85
- { agent: agent, help: help }
89
+ { agent: agent, help: help, core: core }
86
90
  end
87
91
 
88
92
  def show_help
@@ -95,6 +99,8 @@ class Doctor
95
99
 
96
100
  Options:
97
101
  --agent NAME Agent to check: claude (default), codex, hermes
102
+ --core Fast runtime-liveness check only (hooks, scripts, core files);
103
+ skips the slow store/conventions/project inventory walks.
98
104
  -h, --help Show this help
99
105
 
100
106
  Output:
@@ -903,6 +909,23 @@ class Doctor
903
909
  all_checks += check_project_stores
904
910
  all_checks += check_deprecations
905
911
 
912
+ summarize(all_checks, agent_key)
913
+ end
914
+
915
+ # Fast runtime-liveness check: only the plumbing that proves Plastic can
916
+ # operate (hooks, skills, scripts, core files). Skips the slow inventory
917
+ # walks (global store refs, per-intent conventions, project stores,
918
+ # deprecations) so it returns near-instantly. Used by `doctor.rb --core`.
919
+ def run_core_checks(agent_key)
920
+ all_checks = []
921
+ all_checks += check_agent_registration(agent_key)
922
+ all_checks += check_core_files(agent_key)
923
+
924
+ summarize(all_checks, agent_key)
925
+ end
926
+
927
+ # Roll a list of checks up into the standard result envelope.
928
+ def summarize(all_checks, agent_key)
906
929
  summary = { pass: 0, warn: 0, fail: 0, total: all_checks.size }
907
930
  all_checks.each { |c| summary[c[:status].to_sym] += 1 }
908
931
 
@@ -934,7 +957,7 @@ class Doctor
934
957
  exit 0
935
958
  end
936
959
 
937
- result = run_checks(flags[:agent])
960
+ result = flags[:core] ? run_core_checks(flags[:agent]) : run_checks(flags[:agent])
938
961
 
939
962
  puts JSON.pretty_generate(result)
940
963
 
@@ -1,120 +1,131 @@
1
1
  ---
2
2
  name: plastic-continuing
3
- description: Use when the user says "continue" after a /clear, or when resuming work in a new session. Reads intent state from global store (~/.plastic/) or local store, offers active intents first, then future intents, and surfaces stale intents for triage.
3
+ description: Use when the user says "continue", "resume", or "pick up where we left off", or when starting a new session. Boots Plastic runtime health check, loads core context + store/project state, prints version + statusline, and lands on the right dashboard — then presents choices. Does not drive work autonomously (that is plastic-auto).
4
4
  ---
5
5
 
6
6
  # Continuing
7
7
 
8
+ `plastic-continuing` is a deterministic **boot orchestrator**. It loads and presents choices,
9
+ then stops. It does NOT execute work autonomously (that is `plastic-auto`) and does NOT render
10
+ the dashboard itself (it only invokes it).
11
+
8
12
  ## When to Use
9
13
  - UserPromptSubmit hook detects "continue" (automatic)
10
14
  - User says "continue", "resume", or "pick up where we left off"
11
- - Starting a new session with existing active intents
15
+ - Starting a new session with an existing Plastic store
12
16
 
13
17
  ## Determine Store
14
18
 
15
- 1. Check `~/.plastic/INDEX.md` → global mode
16
- 2.
19
+ 1. **Global store** — `~/.plastic/INDEX.md` exists → global mode.
20
+ 2. **Local store** — a project store under `~/.plastic/projects/{slug}/` whose registered
21
+ path (in `~/.plastic/projects.yml`) matches the current working directory → project mode.
22
+ Project detection happens in boot step 2 below; this just records that a local store is
23
+ in play.
17
24
  3. If neither exists → announce "No Plastic store found. Run /plastic-install."
18
25
 
19
- ## Workflow
26
+ ## Boot Sequence (run in this fixed order)
20
27
 
21
- ### 0. Render the dashboard (overview)
22
- For the "where are we / what's next" overview, run the deterministic dashboard and show
23
- its output verbatim instead of hand-summarizing intents:
28
+ ### 1. Core doctor (health first)
29
+ Run the fast runtime-liveness check synchronously (it returns in well under a second):
24
30
 
25
31
  ```bash
26
- ruby ~/.plastic/scripts/dashboard.rb continue
27
- ```
28
-
29
- This is the uniform, model-agnostic cockpit (active + last touched, then the Value×Effort
30
- matrix). Then continue with the steps below to actually resume a specific intent. See the
31
- `plastic-dashboard` skill for how to read the matrix.
32
-
33
- ### 1. Read INDEX.md
34
- Read the INDEX.md from the active store. Extract intents under `## Active` and `## Future`.
35
-
36
- ### 2. Detect Current Project (global mode only)
37
- Read `~/.plastic/projects.yml`, match CWD against registered project paths. If in a project:
38
- - Load the governing intent (from `parent` in projects.yml)
39
- - Load tactical intents from `~/.plastic/projects/{slug}/store/`
40
-
41
- ### 3. If Active Intents Exist → Resume
42
-
43
- For each active intent in the store:
44
-
45
- **a. Read `{ID}--{slug}.md`:**
46
- - What we're doing (`## Intent`)
47
- - Why (`## Context`)
48
- - What insights have emerged (`## Insights`)
49
-
50
- **b. Read savepoint.md** (if exists):
51
- - What was in progress, what's next, blockers
52
-
53
- **c. Read checklist.md** (if exists):
54
- - What's completed, what's next
55
-
56
- **d. Announce:**
57
- ```
58
- Resuming intent [ID] — [name]
59
- Store: [global | project:<slug> | local]
60
- Status: active
61
- Last session: [date from savepoint]
62
- In progress: [from savepoint]
63
- Next step: [from checklist or savepoint]
64
- Blockers: [from savepoint, or "none"]
65
- ```
66
-
67
- **e. Resume** — proceed with the next step.
68
-
69
- ### 3b. Detect Autonomous Resume
70
-
71
- When resuming an active intent, check `## Insights` for entries containing `(autonomous)`.
72
-
73
- If found — this intent was being delivered autonomously:
74
-
75
- **Announce:**
76
- ```
77
- Resuming autonomous delivery of intent [ID] — [name]
78
- Store: [global | project:<slug> | local]
79
- Last autonomous action: [last (autonomous) insight entry]
80
- Next step: [from checklist or savepoint]
32
+ ruby ~/.plastic/scripts/doctor.rb --core
81
33
  ```
82
34
 
83
- **Then:** Continue autonomous execution by invoking `plastic-auto`. The auto skill will pick up from the current lifecycle stage (it reads filesystem state to determine where to resume).
84
-
85
- If NOT found resume normally as described in step 3.
86
-
87
- ### 4. If No Active Intents Offer Future Intents
88
-
89
- Present future intents as options. When user picks one, move to Active in INDEX.md. Auto-commit.
90
-
91
- ### 5. Surface Stale Future Intents
92
-
93
- If any future intent has `created` date older than the configured `stale_threshold_days` (default 3):
35
+ Print one compact health line:
36
+ - All pass → `Plastic core: healthy`
37
+ - Otherwise `Plastic core: issues` followed by the failing checks (name + message).
38
+
39
+ This runs first so a broken runtime (missing hooks, scripts, core files) surfaces before any
40
+ state is loaded on top of it. For a full diagnosis, point the user at `/plastic-doctor`.
41
+
42
+ ### 2. Load core (context + state)
43
+ - Prime `PLASTIC.md` and the harness docs so the conventions are in mind.
44
+ - Load live state:
45
+ - Read the active store `INDEX.md` (`## Active`, `## Future`).
46
+ - Read `~/.plastic/projects.yml`.
47
+ - **Detect the current project** by matching CWD against registered project paths.
48
+ - If in a project: load that project's `INDEX.md`; load the governing intent (from
49
+ `parent` in projects.yml) and the tactical intents from
50
+ `~/.plastic/projects/{slug}/store/`.
51
+
52
+ ### 3. Version + statusline
53
+ - Print the current Plastic version (from `~/.plastic/VERSION`).
54
+ - Set the statusline.
55
+
56
+ ### 4. Dashboard
57
+ Land on the Markdown board via the `plastic-dashboard` skill. Rendering belongs there, not
58
+ here — run the data payload and fill + present the matching template:
59
+ - Project loaded → `ruby ~/.plastic/scripts/dashboard.rb project <slug> --data`
60
+ - Otherwise → `ruby ~/.plastic/scripts/dashboard.rb continue --data`
61
+
62
+ Fill the matching template from this skill's `templates/` and **present the filled Markdown
63
+ in your reply** (every time). See `plastic-dashboard` for the fill rules and entry flow.
64
+
65
+ ### Then stop
66
+ Present "here is the state, what next?" and wait. Offer active intents first, then future
67
+ intents. Do not start executing work. The branches below are the only follow-ups:
68
+ - User/agent names a specific intent to continue → **Conditional ledger-resume** (below).
69
+ - User says "auto" / an agent is instructed to deliver → hand to `plastic-auto`.
70
+
71
+ ## Conditional Ledger-Resume
72
+
73
+ Fires ONLY when the user explicitly asks to continue a SPECIFIC intent, or an agent is
74
+ instructed to continue one. It is not part of every boot. For that intent's directory:
75
+
76
+ 1. **Read `savepoint.md`.** It is a deterministic, append-only stage ledger (one line per
77
+ milestone, newest at the bottom): `{utc-iso8601} {Stage} {milestone}`. The **last line =
78
+ current stage**.
79
+ 2. **Verify the stage file.** Confirm the file the ledger names exists and is non-empty
80
+ (ledger `How plan.md created` → `plan.md` must be present and non-empty).
81
+ 3. **Drift handling.** If the ledger's last line disagrees with files-on-disk, rebuild the
82
+ ledger from filesystem state and note the correction:
83
+ ```bash
84
+ ruby -r ~/.plastic/scripts/lib/bridge -e 'Bridge.rebuild_savepoint("<intent_dir>")'
85
+ ```
86
+ 4. **Derive the next step:**
87
+ - First unchecked item in `checklist.md` if it exists, else
88
+ - "advance to the next lifecycle stage" (e.g. ledger shows Why/spec.md → next is How).
89
+ - The newest `## Insights` entry supplies human-readable context (Insights are
90
+ append-only, newest at the bottom).
91
+ 5. **Announce and stop:**
92
+ ```
93
+ Resuming intent [ID] — [name]
94
+ Store: [global | project:<slug> | local]
95
+ Stage: [from ledger last line]
96
+ Next step: [first unchecked checklist item | advance to <stage>]
97
+ Context: [newest ## Insights entry]
98
+ Drift: [none | ledger rebuilt from filesystem]
99
+ ```
100
+ Then proceed with the next step. Autonomy is `plastic-auto`'s job — if the intent's
101
+ `## Insights` contains `(autonomous)` entries, it was being delivered autonomously; hand
102
+ to `plastic-auto` to continue from the current stage.
103
+
104
+ ## Priority Order
105
+
106
+ 1. **Active intents first** — surface work in progress.
107
+ 2. **Project context** — if in a registered project, show governing + tactical intents.
108
+ 3. **Stale future intents** — surface for triage (see below).
109
+ 4. **Fresh future intents** — offer as next work.
110
+
111
+ ## Stale Future Intents
112
+
113
+ If a future intent's `created` date is older than the configured `stale_threshold_days`
114
+ (default 3), surface it for triage without taking action:
94
115
 
95
116
  ```
96
117
  Stale future intents (no action taken):
97
118
 
98
119
  - [ID — name] (X days old)
99
- Options:
100
120
  a) Activate — start working on it now
101
121
  b) Abandon — mark as abandoned
102
- c) Defer to agent:
103
- - implement: agent builds it
104
- - research: agent investigates feasibility
105
- - ideate: agent explores the problem space
106
- d) Auto — go fully autonomous (invokes plastic-auto — agent delivers the intent end-to-end)
122
+ c) Defer to agent: implement | research | ideate
123
+ d) Auto go fully autonomous (invokes plastic-auto)
107
124
  ```
108
125
 
109
- Auto-commit all triage changes.
110
-
111
- ### 6. Priority Order
112
-
113
- 1. **Active intents first** — resume work in progress
114
- 2. **Project context** — if in a registered project, show governing intent + tactical intents
115
- 3. **Stale future intents** — surface for triage
116
- 4. **Fresh future intents** — offer as next work
126
+ When the user activates a future intent, move it to `## Active` in INDEX.md and auto-commit.
117
127
 
118
128
  ## References
119
129
 
120
- - Read `references/context-management.md` for the full save/continue protocol when resuming from a savepoint or when the resume flow needs debugging
130
+ - Read `references/context-management.md` for the full save/continue protocol and for
131
+ debugging the resume flow.
@@ -0,0 +1,136 @@
1
+ {
2
+ "skill_name": "plastic-continuing",
3
+ "notes": "Intent 36. Scopes: description triggering (1-8) and behavior/convention compliance of the rewritten boot orchestrator (9-14). Triggering assertions follow the plastic-auto eval style (one subagent router per case). Behavior assertions are convention checks against the rewritten SKILL.md.",
4
+ "results": {
5
+ "triggering": { "cases": 8, "passed": 8, "run": "2026-06-16, one subagent per case" },
6
+ "behavior": { "cases": 6, "passed": 6, "evidence": "convention checks against skills/continuing/SKILL.md after the intent-36 rewrite; doctor --core verified at 0.07s with 9 liveness checks and full doctor unchanged at 30 checks" }
7
+ },
8
+ "evals": [
9
+ {
10
+ "id": 1, "scope": "triggering", "set": "train",
11
+ "prompt": "continue",
12
+ "expected_output": "Activates plastic-continuing (the bare 'continue' keyword is the documented trigger).",
13
+ "files": [],
14
+ "assertions": [
15
+ { "type": "code", "check": "router CHOICE == plastic-continuing", "observed": "plastic-continuing", "result": "pass" }
16
+ ]
17
+ },
18
+ {
19
+ "id": 2, "scope": "triggering", "set": "train",
20
+ "prompt": "resume where we left off",
21
+ "expected_output": "Activates plastic-continuing.",
22
+ "files": [],
23
+ "assertions": [
24
+ { "type": "code", "check": "router CHOICE == plastic-continuing", "observed": "plastic-continuing", "result": "pass" }
25
+ ]
26
+ },
27
+ {
28
+ "id": 3, "scope": "triggering", "set": "validation",
29
+ "prompt": "pick up where we left off in this project",
30
+ "expected_output": "Activates plastic-continuing.",
31
+ "files": [],
32
+ "assertions": [
33
+ { "type": "code", "check": "router CHOICE == plastic-continuing", "observed": "plastic-continuing", "result": "pass" }
34
+ ]
35
+ },
36
+ {
37
+ "id": 4, "scope": "triggering", "set": "train",
38
+ "prompt": "boot plastic and show me where things stand",
39
+ "expected_output": "Activates plastic-continuing (boot + present state is the skill's purpose).",
40
+ "files": [],
41
+ "assertions": [
42
+ { "type": "code", "check": "router CHOICE == plastic-continuing", "observed": "plastic-continuing", "result": "pass" }
43
+ ]
44
+ },
45
+ {
46
+ "id": 5, "scope": "triggering", "set": "train",
47
+ "prompt": "continue delivering this intent autonomously, don't ask me",
48
+ "expected_output": "Does NOT settle on plastic-continuing for execution. Shares 'continue' but the autonomous-delivery intent routes to plastic-auto.",
49
+ "files": [],
50
+ "assertions": [
51
+ { "type": "code", "check": "router CHOICE == plastic-auto", "observed": "plastic-auto", "result": "pass" }
52
+ ]
53
+ },
54
+ {
55
+ "id": 6, "scope": "triggering", "set": "validation",
56
+ "prompt": "continue the for-loop to the next iteration in this function",
57
+ "expected_output": "Does NOT activate plastic-continuing. Near-miss: shares 'continue' but is a code-editing task.",
58
+ "files": [],
59
+ "assertions": [
60
+ { "type": "code", "check": "router CHOICE != plastic-continuing", "observed": "none", "result": "pass" }
61
+ ]
62
+ },
63
+ {
64
+ "id": 7, "scope": "triggering", "set": "train",
65
+ "prompt": "resume the paused background download",
66
+ "expected_output": "Does NOT activate plastic-continuing. Near-miss: shares 'resume' but is unrelated to Plastic sessions.",
67
+ "files": [],
68
+ "assertions": [
69
+ { "type": "code", "check": "router CHOICE != plastic-continuing", "observed": "none", "result": "pass" }
70
+ ]
71
+ },
72
+ {
73
+ "id": 8, "scope": "triggering", "set": "validation",
74
+ "prompt": "what's the dashboard look like right now",
75
+ "expected_output": "May activate plastic-dashboard rather than plastic-continuing; an overview request without 'continue/resume' is a dashboard task.",
76
+ "files": [],
77
+ "assertions": [
78
+ { "type": "code", "check": "router CHOICE != plastic-continuing", "observed": "plastic-dashboard", "result": "pass" }
79
+ ]
80
+ },
81
+ {
82
+ "id": 9, "scope": "behavior", "set": "train",
83
+ "prompt": "Does the skill document the four-step boot sequence in the fixed order?",
84
+ "expected_output": "SKILL.md lists, in order: (1) core doctor, (2) load core, (3) version + statusline, (4) dashboard.",
85
+ "files": ["skills/continuing/SKILL.md"],
86
+ "assertions": [
87
+ { "type": "convention", "check": "boot steps appear in order doctor -> load core -> version/statusline -> dashboard", "observed": "headings '### 1. Core doctor', '### 2. Load core', '### 3. Version + statusline', '### 4. Dashboard'", "result": "pass" }
88
+ ]
89
+ },
90
+ {
91
+ "id": 10, "scope": "behavior", "set": "train",
92
+ "prompt": "Does step 1 run the fast core health check synchronously?",
93
+ "expected_output": "Step 1 invokes `doctor.rb --core` and prints a single health line.",
94
+ "files": ["skills/continuing/SKILL.md"],
95
+ "assertions": [
96
+ { "type": "convention", "check": "SKILL.md contains 'doctor.rb --core' and a 'Plastic core: healthy' health line", "observed": "present in '### 1. Core doctor'", "result": "pass" }
97
+ ]
98
+ },
99
+ {
100
+ "id": 11, "scope": "behavior", "set": "train",
101
+ "prompt": "Is dashboard selection project-aware?",
102
+ "expected_output": "Project loaded -> `dashboard.rb project <slug>`; otherwise -> `dashboard.rb continue`. Skill only invokes, does not render.",
103
+ "files": ["skills/continuing/SKILL.md"],
104
+ "assertions": [
105
+ { "type": "convention", "check": "both dashboard invocations present and gated on project detection", "observed": "'dashboard.rb project <slug>' and 'dashboard.rb continue' in '### 4. Dashboard'", "result": "pass" }
106
+ ]
107
+ },
108
+ {
109
+ "id": 12, "scope": "behavior", "set": "validation",
110
+ "prompt": "Is ledger-resume conditional and ledger-driven?",
111
+ "expected_output": "Resume fires only when a specific intent is named; reads savepoint.md last line as stage, verifies the stage file, rebuilds on drift, derives next step from first unchecked checklist item.",
112
+ "files": ["skills/continuing/SKILL.md"],
113
+ "assertions": [
114
+ { "type": "convention", "check": "Conditional Ledger-Resume section reads last ledger line, verifies stage file, calls rebuild_savepoint on drift, uses first unchecked checklist item", "observed": "all four present in 'Conditional Ledger-Resume'", "result": "pass" }
115
+ ]
116
+ },
117
+ {
118
+ "id": 13, "scope": "behavior", "set": "validation",
119
+ "prompt": "Are the stale prose-savepoint fields gone?",
120
+ "expected_output": "No 'In progress' / 'Blockers' fields read from a prose savepoint remain; the ledger model is used instead.",
121
+ "files": ["skills/continuing/SKILL.md"],
122
+ "assertions": [
123
+ { "type": "convention", "check": "no prose-savepoint announce template (In progress / Blockers from prose)", "observed": "absent; announce uses Stage/Next step/Context/Drift derived from ledger + checklist", "result": "pass" }
124
+ ]
125
+ },
126
+ {
127
+ "id": 14, "scope": "behavior", "set": "validation",
128
+ "prompt": "Is the 'Determine Store' local-store gap filled?",
129
+ "expected_output": "Step 2 of Determine Store describes local/project-store detection (no empty step).",
130
+ "files": ["skills/continuing/SKILL.md"],
131
+ "assertions": [
132
+ { "type": "convention", "check": "Determine Store step 2 documents local/project store detection via projects.yml + CWD match", "observed": "filled", "result": "pass" }
133
+ ]
134
+ }
135
+ ]
136
+ }
@@ -1,65 +1,78 @@
1
1
  ---
2
2
  name: plastic-dashboard
3
- description: Use when the user wants an overview of intents, asks "where are we", "what's next", "what should I work on", "show the dashboard", or invokes /plastic-dashboard. Renders a deterministic Value×Effort work cockpit across the global store and all projects, and emits a machine-readable queue that auto mode consumes.
3
+ description: Use when the user wants an overview of intents, asks "where are we", "what's next", "what should I work on", "show the dashboard", or invokes /plastic-dashboard. Renders a deterministic Value×Effort work cockpit as Markdown across the global store and all projects, and emits a machine-readable queue that auto mode consumes.
4
4
  ---
5
5
 
6
6
  # Dashboard — Plastic Work Cockpit
7
7
 
8
- A deterministic, template-driven overview of the intent store(s). It answers three
9
- questions at a glance — **where we are** (active + last touched), **where we go next**
10
- (a Value×Effort matrix), and **how to conduct it** (a disposition verb per intent) and
11
- emits a JSON manifest that `plastic-auto` reads to pick the next dispatchable intent.
8
+ A deterministic overview of the intent store(s). It answers three questions at a glance:
9
+ **where we are** (recently worked), **where we go next** (a Value×Effort matrix), and
10
+ **how to conduct it** (a disposition per intent). The human-facing surface is **Markdown**,
11
+ because the user's UI renders Markdown natively but collapses raw tool-call stdout.
12
12
 
13
- The script does the rendering. The LLM is **never** in the rendering path: same store
14
- state byte-identical output, regardless of model. Do not hand-summarize intents when
15
- this skill applies run the script and show its output verbatim.
13
+ The script does all the data work. The agent fills a Markdown template from the script's
14
+ payload with near-zero reasoning and **presents the filled board in its reply**. Same store
15
+ state byte-identical payload, regardless of model. Do NOT hand-summarize intents.
16
16
 
17
17
  ## When to Use
18
18
 
19
19
  - User invokes `/plastic-dashboard`
20
20
  - User asks "where are we", "what's next", "what should I work on", "show me the intents"
21
- - `plastic-continuing` embeds the `continue` view on resume
21
+ - `plastic-continuing` lands on the board on resume
22
22
  - `plastic-auto` reads `--json` to choose the next dispatchable intent
23
23
 
24
- ## Procedure
24
+ ## Procedure (the Markdown board — default human surface)
25
25
 
26
- ### Step 1 — Run the script
26
+ ### Step 1 — Get the data payload
27
27
 
28
28
  ```bash
29
- ruby ~/.plastic/scripts/dashboard.rb [continue|project <slug>|all] [--json]
29
+ ruby ~/.plastic/scripts/dashboard.rb [continue|project <slug>] --data
30
30
  ```
31
31
 
32
- | Mode | Shows |
33
- |------|-------|
34
- | `continue` (default) | Cross-scope: active + last touched, then the Value×Effort matrix for all scopes |
35
- | `project <slug>` | One project: active line + its Value×Effort matrix |
36
- | `all` | Per-scope roll-up summary |
37
- | `--json` | The auto-mode manifest (any mode); machine-readable, not for humans |
32
+ - `continue` (default) the **global** board payload (`mode: "global"`).
33
+ - `project <slug>` → that **project** board payload (`mode: "project"`).
38
34
 
39
- The script is **read-only**. Print its stdout verbatim do not reformat, re-sort, or
40
- re-summarize. That is what keeps the output uniform.
35
+ The payload is read-only JSON. Global-board fields: `date`, `recently_worked`, `matrix`
36
+ (`quick_win`/`next_big`/`defer`/`triage`/`research`, each a list of `{line, bullet, ...}`),
37
+ `counts`, `projects`, `project_totals`. Project-board fields: `slug`, `description`,
38
+ `recently_worked`, `matrix`, `counts`, `active`, `future`.
41
39
 
42
- ### Step 2 — Read the matrix
40
+ ### Step 2 — Fill the matching template
43
41
 
44
- ```
45
- small effort big effort
46
- high value QUICK WIN ★ NEXT BIG THING
47
- low value DEFER → agent TRIAGE / question
48
- (type=research/exploration RESEARCH band, regardless of quadrant)
49
- ```
42
+ Templates live in this skill's `templates/` directory:
43
+ `~/.claude/skills/plastic-dashboard/templates/dashboard-global.md` and
44
+ `dashboard-project.md`.
45
+
46
+ Fill mechanically no rewriting, no re-sorting:
47
+ - `{{a.b.count}}` → the integer (e.g. `matrix.quick_win.count` = that list's length).
48
+ - `{{...lines}}` → join the list's `.line` strings with **real newlines** (one per line).
49
+ These lines are already glyph-led (the glyph is the bullet — never add `-`, never emit
50
+ `<br>`). If a matrix quadrant list is empty, render `_(none)_`.
51
+ - `projects.lines` → one line per project:
52
+ `- **{slug}** — {description} · active {active} · done {done} · future {future} · last accessed {last_accessed_at[0,10]}`.
53
+ - Scalars (`{{date}}`, `{{slug}}`, `{{description}}`) → substitute verbatim.
50
54
 
51
- Disposition verbs: `▸ drive` (human leads), `⇢ defer` (agent knocks off),
52
- `⊙ research` (research/explore agent), `⚑ triage` (human review / maybe abandon).
53
- Flags: `⇡ unblocked` (a dependency just completed), `(Nd)` stale age.
55
+ ### Step 3 Present it (mandatory, every invocation)
54
56
 
55
- ### Step 3 Act on it
57
+ **Paste the filled Markdown into your reply.** This is non-optional: the board only reaches
58
+ the user when it is in the chat reply, not in tool-call stdout. Never describe the board
59
+ instead of showing it.
56
60
 
57
- - **★ Next big thing** and `▸ drive` / `⚑ triage` items → the human leads (brainstorm → plan → exec).
58
- - `⇢ defer` and `⊙ research` items → dispatchable to agents.
59
- - In auto mode, read `--json` and work `dispatchable_queue` in `rank` order; leave
60
- `human_only` for the user.
61
+ ### Step 4 Entry flow (the board is the menu)
61
62
 
62
- ## JSON contract
63
+ The board lists everything; the user navigates by free prose (no capped picker):
64
+ - On the **global** board, the user replies with an **intent id** (work it), a **project
65
+ name** (re-run `project <slug> --data` and present that board), or **"new"** (start a new
66
+ intent in global via `plastic-creating-intent`).
67
+ - On a **project** board, the user replies with an **intent id**, or **"global"** to return.
68
+
69
+ ## Auto-mode contract (`--json`)
70
+
71
+ `plastic-auto` consumes a separate machine-readable manifest (unchanged):
72
+
73
+ ```bash
74
+ ruby ~/.plastic/scripts/dashboard.rb [continue|project <slug>|all] --json
75
+ ```
63
76
 
64
77
  ```json
65
78
  { "generated_for": "auto-mode", "scope": "<scope|all>",
@@ -68,25 +81,34 @@ Flags: `⇡ unblocked` (a dependency just completed), `(Nd)` stale age.
68
81
  "human_only": ["<id>", "..."] }
69
82
  ```
70
83
 
84
+ Work `dispatchable_queue` in `rank` order (`defer`/`research`); leave `human_only` for the user.
85
+
86
+ ## Text modes (terminal / legacy)
87
+
88
+ `dashboard.rb [continue|project <slug>|all]` with no flag still prints the ASCII cockpit for
89
+ a raw terminal. The Markdown board (`--data` + template) is the surface for the chat UI.
90
+
71
91
  ## How classification works (deterministic)
72
92
 
73
93
  - **Effort** — small for `research`/`exploration`/`bugfix`, for already-scoped intents
74
94
  (plan/checklist exists), or deep refinement branches; big otherwise.
75
- - **Value** high only for an explicit `value: high` frontmatter field, or a
76
- human-authored root intent that has spawned follow-on work (`chain` non-empty); low otherwise.
77
- - **Override** — a `value: high|normal|low` field in an intent's frontmatter wins. This is
78
- the only place model judgment enters, and only as pre-stamped data (never at render time).
95
+ - **Value high** when any of: explicit `value: high`; a human-authored **root** intent; an
96
+ intent with a non-empty `chain`; or an intent that is a `source` of ≥1 other intent. Else low.
97
+ - **Flags** — `unblocked` only when a **future** intent has **all** its `sources` done;
98
+ `stale` only on future intents past the staleness threshold. Both kept low-noise by design.
99
+ - **Override** — a `value: high|low` frontmatter field always wins (pre-stamped data, never
100
+ model judgment at render time).
79
101
 
80
102
  ## Eval
81
103
 
82
- This skill's eval is **render the template**: run the engine against the test fixture
83
- store and assert byte-identical text + JSON output against the golden snapshots in
84
- `test/fixtures/dashboard/`. See `test/dashboard_test.rb`. If output drifts from the
85
- golden files without an intentional template change, the skill is broken.
104
+ The eval is the payload + golden snapshots: run the engine against the fixture store and
105
+ assert the `--data` payload shape/sorting/classification and the byte-identical `--json` +
106
+ text goldens in `test/fixtures/dashboard/`. See `test/dashboard_test.rb`. Drift without an
107
+ intentional change means the skill is broken.
86
108
 
87
109
  ## Notes
88
110
 
89
- - Clusters (Zettelkasten grouping in INDEX.md) are intentionally **not** renderedthey
90
- are orthogonal to "what to work on next."
91
- - Glyphs are monochrome Unicode (no emoji); they render in standard terminal emulators.
92
- - This is additive: it changes no core lifecycle, gate, or cycle logic.
111
+ - Quadrant lists are **not** Markdown `-` bullets the glyph is the bullet (the boards are
112
+ UI-only and may evolve, so they need not be valid Markdown lists). Never emit `<br>`.
113
+ - Clusters (Zettelkasten grouping in INDEX.md) are intentionally not rendered.
114
+ - Additive: changes no core lifecycle, gate, or cycle logic.
@@ -0,0 +1,31 @@
1
+ # 🧩 Plastic · Global Board — {{date}}
2
+
3
+ **Recently worked** · last 24h
4
+ {{recently_worked.lines}}
5
+
6
+ ## Where we go next · Value × Effort *(global intents only)*
7
+
8
+ | | Small effort | Big effort |
9
+ |---|---|---|
10
+ | **High value** | ⚡ **Quick win** · {{matrix.quick_win.count}} | ★ **Next big thing** · {{matrix.next_big.count}} |
11
+ | **Low value** | → **Defer (agent)** · {{matrix.defer.count}} | ⚑ **Triage** · {{matrix.triage.count}} |
12
+
13
+ **⚡ Quick win** — small effort, high value
14
+ {{matrix.quick_win.lines}}
15
+
16
+ **★ Next big thing** — big effort, high value
17
+ {{matrix.next_big.lines}}
18
+
19
+ **→ Defer (agent)** — small effort, low value
20
+ {{matrix.defer.lines}}
21
+
22
+ **⚑ Triage** — big effort, low value
23
+ {{matrix.triage.lines}}
24
+
25
+ 🔬 **Research → agent**
26
+ {{matrix.research.lines}}
27
+
28
+ ## Projects · active {{project_totals.active}} · done {{project_totals.done}} · future {{project_totals.future}}
29
+ {{projects.lines}}
30
+
31
+ **What would you like to work on next?** (type an **intent id**, a **project name**, or anything **new** you'd like to start)
@@ -0,0 +1,40 @@
1
+ # 📦 {{slug}} · Project Board — {{date}}
2
+
3
+ {{description}}
4
+
5
+ **Recently worked** · last 24h
6
+ {{recently_worked.lines}}
7
+
8
+ ## Where we go next · Value × Effort
9
+
10
+ | | Small effort | Big effort |
11
+ |---|---|---|
12
+ | **High value** | ⚡ **Quick win** · {{matrix.quick_win.count}} | ★ **Next big thing** · {{matrix.next_big.count}} |
13
+ | **Low value** | → **Defer (agent)** · {{matrix.defer.count}} | ⚑ **Triage** · {{matrix.triage.count}} |
14
+
15
+ **⚡ Quick win** — small effort, high value
16
+ {{matrix.quick_win.lines}}
17
+
18
+ **★ Next big thing** — big effort, high value
19
+ {{matrix.next_big.lines}}
20
+
21
+ **→ Defer (agent)** — small effort, low value
22
+ {{matrix.defer.lines}}
23
+
24
+ **⚑ Triage** — big effort, low value
25
+ {{matrix.triage.lines}}
26
+
27
+ 🔬 **Research → agent**
28
+ {{matrix.research.lines}}
29
+
30
+ ## Intents · active {{counts.active}} · done {{counts.done}} · future {{counts.future}}
31
+
32
+ **Active**
33
+ {{active.lines}}
34
+
35
+ **Future**
36
+ {{future.lines}}
37
+
38
+ **Legend** · ○ What ◔ Why ◑ How ◕ Exec ● Done · ⚡ quick win ★ big → defer ⚑ triage 🔬 research
39
+
40
+ **What would you like to work on next?** (type an **intent id**, or **global** to go back)