@zalom/plastic 1.1.3 → 1.1.5

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/PLASTIC.md CHANGED
@@ -10,6 +10,7 @@ See `PLASTIC-reference.md` for reference material: read it on demand, it is not
10
10
 
11
11
  A directory in the store containing `{ID}--{slug}.md` and optional supporting files.
12
12
  It represents a desire: something a human or agent wants to accomplish, explore, or understand.
13
+ The unit of work in Plastic is always an intent, never a ticket.
13
14
 
14
15
  ```
15
16
  store/
package/README.md CHANGED
@@ -44,7 +44,7 @@ Plastic makes that thinking legible, resumable, and useful later.
44
44
 
45
45
  **Plastic is built for an AI-native developer, technical founder, or
46
46
  independent builder who works across multiple sessions, has ideas before
47
- they have tickets, and feels the cost of losing reasoning between agents,
47
+ they have intents, and feels the cost of losing reasoning between agents,
48
48
  contexts, and days.**
49
49
 
50
50
  ## What it solves?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalom/plastic",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Intent-driven idea development system for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -551,6 +551,19 @@ def within_24h?(rec)
551
551
  d && d >= (today - 1)
552
552
  end
553
553
 
554
+ # Collapse whitespace to single spaces, strip, then escape Markdown table pipes so
555
+ # free-text payload data is safe to drop verbatim into a table cell. Block-form gsub
556
+ # avoids replacement-string backslash pitfalls.
557
+ def cell(s)
558
+ s.to_s.gsub(/\s+/, " ").strip.gsub("|") { "\\|" }
559
+ end
560
+
561
+ # Truncate an intent title to the shared line budget, ellipsis when over.
562
+ def truncate_intent(text)
563
+ t = text.to_s
564
+ t.length > INTENT_LINE_MAX_CHARS ? "#{t[0, INTENT_LINE_MAX_CHARS]}…" : t
565
+ end
566
+
554
567
  def worked_row(rec, project_scope)
555
568
  glyph = STATUS_GLYPH[rec[:status]]
556
569
  proj = rec[:scope] == "global" ? "global" : rec[:scope].sub("project:", "")
@@ -559,6 +572,7 @@ def worked_row(rec, project_scope)
559
572
  {
560
573
  id: rec[:id], status: rec[:status], glyph: glyph,
561
574
  last_accessed_at: rec[:last_accessed_at],
575
+ what: cell(truncate_intent(rec[:intent])), state: status_word, scope: cell(proj),
562
576
  line: "#{glyph} #{prefix}#{status_word}: #{rec[:id]} #{rec[:intent]}".strip,
563
577
  }
564
578
  end
@@ -578,7 +592,8 @@ def intent_line(rec, bullet)
578
592
  text = rec[:intent].to_s
579
593
  text = "#{text[0, INTENT_LINE_MAX_CHARS]}…" if text.length > INTENT_LINE_MAX_CHARS
580
594
  { id: rec[:id], intent: rec[:intent], created: rec[:created], bullet: bullet,
581
- scope: rec[:scope], line: "#{bullet} #{rec[:id]} #{text}#{note}".rstrip }
595
+ scope: rec[:scope], what: cell(text), stage: rec[:lifecycle].to_s.capitalize,
596
+ line: "#{bullet} #{rec[:id]} #{text}#{note}".rstrip }
582
597
  end
583
598
 
584
599
  # Cap a raw record list to NEXT_WORK_CAP entries, then map to intent_line-shaped
@@ -589,6 +604,7 @@ def cap_lines(list, bullet)
589
604
  lines = capped.map { |r| intent_line(r, bullet) }
590
605
  if list.size > NEXT_WORK_CAP
591
606
  lines << { id: "", intent: "", created: "", bullet: bullet, scope: "",
607
+ what: "+#{list.size - NEXT_WORK_CAP} more", stage: "",
592
608
  line: "#{bullet} +#{list.size - NEXT_WORK_CAP} more" }
593
609
  end
594
610
  lines
@@ -607,11 +623,13 @@ def next_work(records)
607
623
  text = "#{text[0, INTENT_LINE_MAX_CHARS]}…" if text.length > INTENT_LINE_MAX_CHARS
608
624
  { id: r[:id], intent: r[:intent], scope: r[:scope], lifecycle: r[:lifecycle],
609
625
  value: r[:value].to_s, disposition: r[:disposition], flags: r[:flags],
626
+ what: cell(text), flags_label: cell(Array(r[:flags]).join(", ")),
610
627
  line: "#{r[:id]} #{text}" }
611
628
  end
612
629
  if ranked.size > NEXT_WORK_CAP
613
630
  lines << { id: "", intent: "", scope: "", lifecycle: "", value: "", disposition: "",
614
- flags: [], line: "+#{ranked.size - NEXT_WORK_CAP} more" }
631
+ flags: [], what: "+#{ranked.size - NEXT_WORK_CAP} more", flags_label: "",
632
+ line: "+#{ranked.size - NEXT_WORK_CAP} more" }
615
633
  end
616
634
  lines
617
635
  end
@@ -34,9 +34,13 @@ ruby ~/.plastic/scripts/dashboard.rb [continue|project <slug>] --data
34
34
  - `project <slug>` → that **project** board payload (`mode: "project"`).
35
35
 
36
36
  The payload is read-only JSON. Global-board fields: `date`, `store_health`, `recently_worked`,
37
- `next_work` (a flat, rank-ordered list of `{id, intent, scope, lifecycle, value, disposition,
38
- flags, line}`), `counts`, `projects`, `project_totals`. Project-board fields: `slug`,
39
- `store_health`, `description`, `recently_worked`, `next_work`, `counts`, `active`, `future`.
37
+ `next_work`, `counts`, `projects`, `project_totals`. Project-board fields: `slug`, `store_health`,
38
+ `description`, `recently_worked`, `next_work`, `counts`, `active`, `future`. Each list carries
39
+ cell-ready fields for its table: `next_work` rows are
40
+ `{id, intent, scope, lifecycle, value, disposition, flags, what, flags_label, line}`;
41
+ `recently_worked` rows carry `{id, status, glyph, last_accessed_at, what, state, scope, line}`;
42
+ `active`/`future` rows carry `{id, intent, created, bullet, scope, what, stage, line}`. The `what`,
43
+ `scope`, and `flags_label` cell fields arrive pipe-escaped and whitespace-normalized.
40
44
 
41
45
  Each board load runs the scoped store check (`doctor --store <scope>`): the global board runs
42
46
  `--store global` and a project board runs `--store <slug>`. The result rides in the payload as
@@ -53,13 +57,22 @@ Templates live in this skill's `templates/` directory:
53
57
 
54
58
  Fill mechanically, no rewriting, no re-sorting:
55
59
  - `{{a.b.count}}` → the integer (e.g. `counts.active` = that count).
56
- - `{{...lines}}` → join the list's `.line` strings with **real newlines** (one per line).
57
- These are ordinary prose lines, not glyph-led bullets: never add a Markdown `-` bullet,
58
- never emit `<br>`. If a list is empty, render `_(none)_`.
59
- - `next_work.lines` the most-valuable next work, already ranked; each line reads
60
- `"<id> <intent, truncated>"`. Use each entry's `disposition`/`flags` fields when the prose
61
- needs to say why an item is next.
62
- - `projects.lines` → one line per project:
60
+ - `{{<list>.rows}}` → the four intent lists (`recently_worked`, `next_work`, `active`, `future`)
61
+ render as **Markdown table rows**. The template hard-codes each table's header and separator;
62
+ this placeholder becomes one data row per list entry, joined with real newlines, in that table's
63
+ fixed column order (below). Drop each cell from the named payload field **verbatim**: cells
64
+ arrive pre-escaped and whitespace-normalized from the script (pipes escaped as `\|`), so never
65
+ re-escape, re-truncate, or reword them. Never emit `<br>`.
66
+ - `recently_worked` (global) `| {id} | {what} | {state} | {scope} |`
67
+ - `recently_worked` (project) → `| {id} | {what} | {state} |`
68
+ - `next_work` → `| {id} | {what} | {value} | {disposition} | {flags_label} |`
69
+ - `active` → `| {id} | {what} | {stage} |`
70
+ - `future` → `| {id} | {what} |`
71
+ Overflow entry (empty `id`, `what` = `+N more`) → one row with `+N more` in the Id column and
72
+ every other cell blank. Empty list → one full-width row with `_(none)_` in the Id column and
73
+ every other cell blank, matching that table's column count (e.g. `| _(none)_ | | | | |` for
74
+ the 5-column next_work table, `| _(none)_ | |` for the 2-column future table).
75
+ - `{{projects.lines}}` → the project rollup stays **prose**, one line per project (not a table):
63
76
  `- **{slug}**: {description}, active {active}, done {done}, future {future}, last accessed {last_accessed_at[0,10]}`.
64
77
  - Scalars (`{{date}}`, `{{slug}}`, `{{description}}`) → substitute verbatim.
65
78
 
@@ -126,7 +139,8 @@ intentional change means the skill is broken.
126
139
 
127
140
  ## Notes
128
141
 
129
- - Board lines are ordinary prose, not a rigid grid: the boards are UI-only and may evolve,
130
- so they need not be valid Markdown lists. Never emit `<br>`.
142
+ - The four intent lists (recently worked, active, future, next work) render as Markdown tables;
143
+ the narrative wrappers, counts, and the project rollup stay prose. No Value x Effort grid
144
+ returns. Never emit `<br>`.
131
145
  - Clusters (Zettelkasten grouping in INDEX.md) are intentionally not rendered.
132
146
  - Additive: changes no core lifecycle, gate, or cycle logic.
@@ -17,6 +17,22 @@
17
17
  "result": "pass"
18
18
  }
19
19
  ]
20
+ },
21
+ {
22
+ "id": 2,
23
+ "scope": "behavior",
24
+ "set": "validation",
25
+ "prompt": "How do the four intent lists render on the Markdown boards?",
26
+ "expected_output": "recently_worked, next_work, active, and future render as Markdown tables (fixed columns: next_work Id|What|Value|Disposition|Flags; active Id|What|Stage; future Id|What; recently_worked Id|What|State, plus a Scope column on the global board). The narrative wrappers, counts, and the projects rollup stay prose; no Value x Effort grid. Cells arrive pre-escaped from the payload and are dropped verbatim.",
27
+ "files": ["skills/dashboard/templates/dashboard-global.md", "skills/dashboard/templates/dashboard-project.md", "skills/dashboard/SKILL.md"],
28
+ "assertions": [
29
+ {
30
+ "type": "convention",
31
+ "check": "the four intent lists render as Markdown tables with the fixed columns; wrappers and the projects rollup stay prose; no grid",
32
+ "observed": "templates hard-code table headers plus {{list.rows}} for recently_worked/next_work/active/future; SKILL.md Step 2 documents the fixed column order and verbatim pre-escaped cell fill; projects stays {{projects.lines}} prose",
33
+ "result": "pass"
34
+ }
35
+ ]
20
36
  }
21
37
  ]
22
38
  }
@@ -1,7 +1,10 @@
1
1
  # 🧩 Plastic · Global Board, {{date}}
2
2
 
3
3
  **Recently worked** (last 24h)
4
- {{recently_worked.lines}}
4
+
5
+ | Id | What | State | Scope |
6
+ | --- | --- | --- | --- |
7
+ {{recently_worked.rows}}
5
8
 
6
9
  ## Where we are
7
10
 
@@ -11,6 +14,9 @@
11
14
  {{projects.lines}}
12
15
 
13
16
  ## Most-valuable next work
14
- {{next_work.lines}}
17
+
18
+ | Id | What | Value | Disposition | Flags |
19
+ | --- | --- | --- | --- | --- |
20
+ {{next_work.rows}}
15
21
 
16
22
  **What would you like to work on next?** (type an **intent id**, a **project name**, or anything **new** you'd like to start)
@@ -3,19 +3,29 @@
3
3
  {{description}}
4
4
 
5
5
  **Recently worked** (last active work, last 24h)
6
- {{recently_worked.lines}}
6
+
7
+ | Id | What | State |
8
+ | --- | --- | --- |
9
+ {{recently_worked.rows}}
7
10
 
8
11
  ## Intents, active {{counts.active}}, done {{counts.done}}, future {{counts.future}}
9
12
 
10
13
  **Active**
11
- {{active.lines}}
14
+
15
+ | Id | What | Stage |
16
+ | --- | --- | --- |
17
+ {{active.rows}}
12
18
 
13
19
  **Future**
14
- {{future.lines}}
20
+
21
+ | Id | What |
22
+ | --- | --- |
23
+ {{future.rows}}
15
24
 
16
25
  ## Most-valuable next work
17
- {{next_work.lines}}
18
26
 
19
- **Legend** · What Why How Exec ● Done
27
+ | Id | What | Value | Disposition | Flags |
28
+ | --- | --- | --- | --- | --- |
29
+ {{next_work.rows}}
20
30
 
21
31
  **What would you like to work on next?** (type an **intent id**, or **global** to go back)
@@ -91,6 +91,11 @@ was built against the live INDEX.md-parsing `--data` path (147, the DB cutover,
91
91
  landed). Its rule-name citations (`classification.md`, cited by name, not logic) and the
92
92
  `dashboard.rb project <slug> --data` -> `dashboard-project.md` path still resolve.
93
93
 
94
+ Intent 149a has landed on top of 149: the four intent lists (recently worked, active, future,
95
+ next work) now render as Markdown tables. The prose demotion and the no-grid stance are unchanged,
96
+ and the rule-name citations and the `dashboard.rb project <slug> --data` -> `dashboard-project.md`
97
+ path still resolve.
98
+
94
99
  Intent 148 landed: roadmaps are the primary planning surface. When the tier has a mid-flight
95
100
  roadmap (`ruby ~/.plastic/scripts/roadmap-next --roadmaps-dir <tier>/roadmaps` reports a `state`
96
101
  other than `none`), the roadmap route (`plastic-roadmap-continuing`) is the live surface for
@@ -72,10 +72,10 @@
72
72
  {
73
73
  "id": 8, "scope": "behavior", "set": "validation",
74
74
  "prompt": "Does the skill carry the intent-149 coordination note?",
75
- "expected_output": "A 'Coordination' section records that intent 149 has landed (dashboard demoted to prose, no Value x Effort grid) and confirms the rule-name citations and the dashboard.rb project <slug> --data -> dashboard-project.md path still resolve.",
75
+ "expected_output": "A 'Coordination' section records that intent 149 has landed (dashboard demoted to prose, no Value x Effort grid) and intent 149a has landed (the four intent lists render as Markdown tables), and confirms the rule-name citations and the dashboard.rb project <slug> --data -> dashboard-project.md path still resolve.",
76
76
  "files": ["skills/project-continuing/SKILL.md"],
77
77
  "assertions": [
78
- { "type": "convention", "check": "'149' present with the landed coordination note", "observed": "present: 'Intent 149 has landed' with confirmed rule-name citations and dashboard.rb path", "result": "pass" }
78
+ { "type": "convention", "check": "'149' present with the landed coordination note", "observed": "present: 'Intent 149 has landed' and 'Intent 149a has landed' (lists render as Markdown tables) with confirmed rule-name citations and dashboard.rb path", "result": "pass" }
79
79
  ]
80
80
  },
81
81
  {
@@ -7,11 +7,19 @@ would otherwise bloat the SKILL.md body.
7
7
  ## Fill rules (owned by plastic-dashboard, summarized here for convenience)
8
8
 
9
9
  - `{{a.b.count}}` -> the integer (e.g. `counts.active` is that count).
10
- - `{{...lines}}` -> join the list's `.line` strings with real newlines (one per line). These
11
- are ordinary prose lines; never add a `-` prefix, never emit `<br>`. An empty list renders
12
- `_(none)_`.
13
- - `next_work.lines` -> the most-valuable next work, already ranked and capped.
14
- - `active.lines` / `future.lines` (project board) -> one line per intent, already formatted.
10
+ - `{{<list>.rows}}` -> the four intent lists (`recently_worked`, `next_work`, `active`, `future`)
11
+ render as Markdown table rows. The template hard-codes each table's header and separator; the
12
+ placeholder becomes one data row per entry, in that table's fixed column order, cells dropped
13
+ verbatim from the payload (cells arrive pipe-escaped and whitespace-normalized; do not re-escape
14
+ or re-truncate). Column order per table:
15
+ - `recently_worked` -> `| id | what | state | scope |` (global), `| id | what | state |` (project)
16
+ - `next_work` -> `| id | what | value | disposition | flags_label |`
17
+ - `active` -> `| id | what | stage |`; `future` -> `| id | what |`
18
+ Overflow entry (empty `id`, `what` = `+N more`) -> `+N more` in the Id column, other cells blank.
19
+ Empty list -> one full-width row with `_(none)_` in the Id column, other cells blank, matching
20
+ that table's column count (e.g. `| _(none)_ | | | | |` for the 5-column next_work table,
21
+ `| _(none)_ | |` for the 2-column future table). Never emit `<br>`.
22
+ - `{{projects.lines}}` (global board) -> the project rollup stays prose, one line per project.
15
23
  - Scalars (`{{date}}`, `{{slug}}`, `{{description}}`) -> substitute verbatim.
16
24
 
17
25
  No re-sorting, no re-summarizing, no hand-written prose replacing a line the payload already
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: plastic-roadmap
3
- description: Use when the user wants to plan a delivery batch, order waves of intents, ship a batch of tickets in one go, track a named collection of intents toward a goal, or asks for a "roadmap". Creates and maintains a roadmap file, a delivery-side collection of intents (the counterpart to a release), separate from INDEX.md status tracking.
3
+ description: Use when the user wants to plan a delivery batch, order waves of intents, ship a batch of intents in one go, track a named collection of intents toward a goal, or asks for a "roadmap". Creates and maintains a roadmap file, a delivery-side collection of intents (the counterpart to a release), separate from INDEX.md status tracking.
4
4
  user-invocable: true
5
5
  ---
6
6
 
@@ -4,7 +4,7 @@ description: >-
4
4
  Use when the user wants to continue or resume a roadmap, pick up a mid-flight delivery batch,
5
5
  asks "where is the roadmap", or wants to resume the wave that was shipping, including an
6
6
  indirect ask that never names a roadmap directly (for example "where did that batch of
7
- tickets land"). This is the roadmap route of plastic-continuing: it finds the tier's
7
+ intents land"). This is the roadmap route of plastic-continuing: it finds the tier's
8
8
  mid-flight roadmap, presents its state, then asks how to proceed exactly once.
9
9
  user-invocable: true
10
10
  ---
@@ -26,7 +26,7 @@
26
26
  },
27
27
  {
28
28
  "id": 3, "scope": "triggering", "set": "validation",
29
- "prompt": "where did that batch of tickets land",
29
+ "prompt": "where did that batch of intents land",
30
30
  "expected_output": "Activates plastic-roadmap-continuing (indirect trigger: a roadmap-resume request that never names 'roadmap' or 'continue').",
31
31
  "files": [],
32
32
  "assertions": [