@plot-pm/board 0.2.1 → 0.3.0-rc.0

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": "@plot-pm/board",
3
- "version": "0.2.1",
3
+ "version": "0.3.0-rc.0",
4
4
  "description": "Local Kanban board for Plot — a glanceable view of plan phases from docs/plans, with sprint and story filters",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/plot-config.sh CHANGED
@@ -6,10 +6,12 @@
6
6
  # back to the default so callers can rely on the output unconditionally.
7
7
  # Designed for small-model consumption: one value on stdout, no interpretation.
8
8
  #
9
- # This is the ONE place that knows where plot configuration lives (currently a
10
- # `## Plot Config` section in the repo-root CLAUDE.md). Helpers must call this
11
- # instead of grepping CLAUDE.md themselves, so the storage location/format can
12
- # evolve without touching every consumer.
9
+ # This is the ONE place that knows where plot configuration lives (a
10
+ # `## Plot Config` section in the repo-root CLAUDE.md or AGENTS.md).
11
+ # CLAUDE.md is checked first for backwards compatibility; AGENTS.md is the
12
+ # fallback for repos that have migrated to a hub-and-spoke agent-rules layout.
13
+ # Helpers must call this instead of grepping either file themselves, so the
14
+ # storage location/format can evolve without touching every consumer.
13
15
  #
14
16
  # Grammar accepted inside the section (case-insensitive key, bold optional):
15
17
  # - **Plan directory:** docs/plans/
@@ -23,7 +25,15 @@
23
25
  #
24
26
  # Known keys (see the plot skill's Setup section):
25
27
  # Project board | Branch prefixes | Plan directory | Active index |
26
- # Delivered index | Sprint directory | Plan template | Main branch
28
+ # Delivered index | Sprint directory | Story directory | Story index |
29
+ # Plan template | Main branch
30
+ # Plot 2 posture keys (repo-declared ceremony bounds; all optional):
31
+ # Plan PRs required | never | optional (never = hard gate)
32
+ # Implementation home this repo | <repo/path list> | none
33
+ # Hosts plans yes | no (no = refuse plan files)
34
+ # Tracker plot | jira | github-issues | linear (+ URL)
35
+ # (plot = plans in this repo ARE the tracker; absent = same)
36
+ # Git host github | bitbucket (resolves gh vs bb)
27
37
  #
28
38
  # `Plan template` is a repo-root-relative path to the plan template /plot-idea
29
39
  # instantiates; when absent, /plot-idea falls back to the shipped template.
@@ -40,10 +50,19 @@ if [ "$cmd" != "get" ]; then
40
50
  fi
41
51
 
42
52
  root=$(git rev-parse --show-toplevel 2>/dev/null) || root="."
43
- config_file="$root/CLAUDE.md"
53
+
54
+ # Find the first repo-root file that contains a ## Plot Config section.
55
+ # CLAUDE.md wins for backwards compatibility; AGENTS.md is the modern fallback.
56
+ config_file=""
57
+ for _candidate in "$root/CLAUDE.md" "$root/AGENTS.md"; do
58
+ if [ -f "$_candidate" ] && grep -qi "^##[[:space:]]*plot config" "$_candidate" 2>/dev/null; then
59
+ config_file="$_candidate"
60
+ break
61
+ fi
62
+ done
44
63
 
45
64
  value=""
46
- if [ -f "$config_file" ]; then
65
+ if [ -n "$config_file" ]; then
47
66
  # Extract the `## Plot Config` section (case-insensitive, portable awk).
48
67
  section=$(awk '
49
68
  /^##[[:space:]]/ { in_section = (tolower($0) ~ /^##[[:space:]]+plot config[[:space:]]*$/) ; next }
package/plot-plan-meta.sh CHANGED
@@ -64,6 +64,23 @@
64
64
  # quoted, matching the known prefixes; sorted, unique)
65
65
  # prs PR numbers from `→ #NNN` links in the `## Branches`
66
66
  # section (sorted, unique)
67
+ # review_raw the plan's review-channel answer as written (`## Status`
68
+ # `Review:` or front matter `review:`); "" if absent
69
+ # review normalized: pr|in-session|ballot|UNKNOWN|NONE
70
+ # impl_raw the plan's implementation-home answer as written
71
+ # (`## Status` `Impl:` or front matter `impl:`); "" if absent
72
+ # impl normalized: own-branches|same-branch|other-repo|none|
73
+ # UNKNOWN|NONE ("nowhere" is accepted for none; free-text
74
+ # like "here, own branches" normalizes by token)
75
+ # approved_raw the approval transition record as written (`## Status`
76
+ # `Approved:` or front matter `approved:` — who/when/channel,
77
+ # e.g. "2026-07-30, alice, in-session"); "" if absent
78
+ # started_raw implementation-start records, one raw string per
79
+ # `Started:` line in `## Status` (repeatable; front matter
80
+ # `started:` contributes one entry); [] if absent.
81
+ # Exception to "front matter wins": started is ADDITIVE —
82
+ # canonical entries and the front-matter entry merge
83
+ # (canonical first, front matter appended)
67
84
  #
68
85
  # title/sprint/story/assignee are the board-facing surface (`@plot-pm/board`
69
86
  # consumes this script instead of parsing plans itself). Front matter wins over
@@ -90,7 +107,7 @@ if [ ${#files[@]} -eq 0 ] && [ ${#missing[@]} -eq 0 ]; then
90
107
  fi
91
108
 
92
109
  for f in ${missing[@]+"${missing[@]}"}; do
93
- printf '{"file":"%s","format":"none","error":"file not found","phase_raw":"","phase":"NONE","phase_alt_raw":"","phase_alt":"NONE","type":"","title":"","sprint":"","story":"","assignee":"","branches":[],"prs":[]}\n' \
110
+ printf '{"file":"%s","format":"none","error":"file not found","phase_raw":"","phase":"NONE","phase_alt_raw":"","phase_alt":"NONE","type":"","title":"","sprint":"","story":"","assignee":"","branches":[],"prs":[],"review_raw":"","review":"NONE","impl_raw":"","impl":"NONE","approved_raw":"","started_raw":[]}\n' \
94
111
  "$(printf '%s' "$f" | sed 's/\\/\\\\/g; s/"/\\"/g')"
95
112
  done
96
113
 
@@ -134,17 +151,45 @@ function norm_type(raw, lower, toks, n, i, t) {
134
151
  }
135
152
  return ""
136
153
  }
154
+ # Canonicalize a ceremony answer: lowercase, punctuation/space runs → "-",
155
+ # so "here, own branches" → "here-own-branches" and token checks are uniform.
156
+ function canon_answer(raw, s) {
157
+ s = tolower(raw)
158
+ gsub(/[^a-z0-9]+/, "-", s)
159
+ sub(/^-+/, "", s); sub(/-+$/, "", s)
160
+ return s
161
+ }
162
+ function norm_review(raw, s) {
163
+ if (raw == "") return "NONE"
164
+ s = "-" canon_answer(raw) "-"
165
+ if (s ~ /-in-session-/) return "in-session"
166
+ if (s ~ /-ballot-/) return "ballot"
167
+ if (s ~ /-pr-/) return "pr"
168
+ return "UNKNOWN"
169
+ }
170
+ function norm_impl(raw, s) {
171
+ if (raw == "") return "NONE"
172
+ s = "-" canon_answer(raw) "-"
173
+ if (s ~ /-own-branches-/) return "own-branches"
174
+ if (s ~ /-same-branch-/) return "same-branch"
175
+ if (s ~ /-other-repo-/) return "other-repo"
176
+ if (s ~ /-none-/ || s ~ /-nowhere-/) return "none"
177
+ return "UNKNOWN"
178
+ }
137
179
  function reset_state() {
138
180
  fm_status = ""; fm_phase = ""; fm_type = ""
139
181
  fm_title = ""; fm_sprint = ""; fm_story = ""; fm_assignee = ""
182
+ fm_review = ""; fm_impl = ""; fm_approved = ""; fm_started = ""
140
183
  canon_phase = ""; canon_type = ""
141
184
  canon_sprint = ""; canon_story = ""; canon_assignee = ""
185
+ canon_review = ""; canon_impl = ""; canon_approved = ""
142
186
  h1_title = ""
143
- in_fm = 0; section = ""
187
+ in_fm = 0; section = ""; in_comment = 0
144
188
  delete branches; n_branches = 0
145
189
  delete prs; n_prs = 0
190
+ delete started; n_started = 0
146
191
  }
147
- function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assignee, i, j, out, sorted_b, sorted_p, nb, np) {
192
+ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assignee, review, impl, approved, i, j, out, sorted_b, sorted_p, nb, np) {
148
193
  if (fm_status != "" || fm_phase != "") {
149
194
  fmt = "frontmatter"
150
195
  praw = (fm_status != "") ? fm_status : fm_phase
@@ -161,6 +206,10 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
161
206
  sprint = strip_placeholder((fm_sprint != "") ? fm_sprint : canon_sprint)
162
207
  story = strip_placeholder((fm_story != "") ? fm_story : canon_story)
163
208
  assignee = strip_placeholder((fm_assignee != "") ? fm_assignee : canon_assignee)
209
+ review = strip_placeholder((fm_review != "") ? fm_review : canon_review)
210
+ impl = strip_placeholder((fm_impl != "") ? fm_impl : canon_impl)
211
+ approved = strip_placeholder((fm_approved != "") ? fm_approved : canon_approved)
212
+ if (fm_started != "" && strip_placeholder(fm_started) != "") started[++n_started] = fm_started
164
213
  # Insertion sort + dedupe (portable: no gawk asort).
165
214
  nb = 0
166
215
  for (i = 1; i <= n_branches; i++) {
@@ -186,6 +235,12 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
186
235
  for (i = 1; i <= nb; i++) out = out (i > 1 ? "," : "") "\"" jesc(sorted_b[i]) "\""
187
236
  out = out "],\"prs\":["
188
237
  for (i = 1; i <= np; i++) out = out (i > 1 ? "," : "") sorted_p[i]
238
+ out = out "]"
239
+ out = out ",\"review_raw\":\"" jesc(review) "\",\"review\":\"" norm_review(review) "\""
240
+ out = out ",\"impl_raw\":\"" jesc(impl) "\",\"impl\":\"" norm_impl(impl) "\""
241
+ out = out ",\"approved_raw\":\"" jesc(approved) "\""
242
+ out = out ",\"started_raw\":["
243
+ for (i = 1; i <= n_started; i++) out = out (i > 1 ? "," : "") "\"" jesc(started[i]) "\""
189
244
  out = out "]}"
190
245
  print out
191
246
  }
@@ -206,8 +261,16 @@ in_fm {
206
261
  else if (lower ~ /^sprint:/ && fm_sprint == "") fm_sprint = val_after_colon($0)
207
262
  else if (lower ~ /^story:/ && fm_story == "") fm_story = val_after_colon($0)
208
263
  else if (lower ~ /^assignee:/ && fm_assignee == "") fm_assignee = val_after_colon($0)
264
+ else if (lower ~ /^review:/ && fm_review == "") fm_review = val_after_colon($0)
265
+ else if (lower ~ /^impl:/ && fm_impl == "") fm_impl = val_after_colon($0)
266
+ else if (lower ~ /^approved:/ && fm_approved == "") fm_approved = val_after_colon($0)
267
+ else if (lower ~ /^started:/ && fm_started == "") fm_started = val_after_colon($0)
209
268
  next
210
269
  }
270
+ # Interior of multi-line HTML comments is non-content (template guidance
271
+ # blocks); single-line "<!-- ... -->" placeholders are unaffected.
272
+ in_comment { if ($0 ~ /-->/) in_comment = 0; next }
273
+ /<!--/ && $0 !~ /-->/ { in_comment = 1; next }
211
274
  # First H1 is the title fallback (front matter title: still wins in emit).
212
275
  /^#[ \t]/ && h1_title == "" { h1_title = trim(substr($0, 2)) }
213
276
  /^## / {
@@ -223,6 +286,13 @@ section == "status" {
223
286
  else if (lower ~ /^[ \t]*[-*]?[ \t]*\**type[:*]/ && canon_type == "") canon_type = val_after_colon($0)
224
287
  else if (lower ~ /^[ \t]*[-*]?[ \t]*\**sprint[:*]/ && canon_sprint == "") canon_sprint = val_after_colon($0)
225
288
  else if (lower ~ /^[ \t]*[-*]?[ \t]*\**story[:*]/ && canon_story == "") canon_story = val_after_colon($0)
289
+ else if (lower ~ /^[ \t]*[-*]?[ \t]*\**review[:*]/ && canon_review == "") canon_review = val_after_colon($0)
290
+ else if (lower ~ /^[ \t]*[-*]?[ \t]*\**impl[:*]/ && canon_impl == "") canon_impl = val_after_colon($0)
291
+ else if (lower ~ /^[ \t]*[-*]?[ \t]*\**approved[:*]/ && canon_approved == "") canon_approved = val_after_colon($0)
292
+ else if (lower ~ /^[ \t]*[-*]?[ \t]*\**started[:*]/) {
293
+ _s = strip_placeholder(val_after_colon($0))
294
+ if (_s != "") started[++n_started] = _s
295
+ }
226
296
  next
227
297
  }
228
298
  section == "approval" {