@zalom/plastic 1.0.0-alpha.21 → 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.21",
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
@@ -54,11 +54,13 @@ state is loaded on top of it. For a full diagnosis, point the user at `/plastic-
54
54
  - Set the statusline.
55
55
 
56
56
  ### 4. Dashboard
57
- Invoke the dashboard. Rendering belongs to the dashboard skill, not here only invoke:
58
- - Project loaded `ruby ~/.plastic/scripts/dashboard.rb project <slug>`
59
- - Otherwise → `ruby ~/.plastic/scripts/dashboard.rb continue`
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`
60
61
 
61
- Show its output verbatim. See `plastic-dashboard` for how to read the matrix.
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.
62
64
 
63
65
  ### Then stop
64
66
  Present "here is the state, what next?" and wait. Offer active intents first, then future
@@ -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)