dsh-plugin-bmad-sprint-board 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +15 -13
  2. package/client.js +208 -79
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -60,25 +60,27 @@ The board is a **check on** the ticket store, not a mirror of it.
60
60
 
61
61
  | Detector | Fires when |
62
62
  |---|---|
63
- | **L1** plans not linked | plan files match tickets by name, but no entry declares them with a `ticket` key |
64
- | **L2** tickets with no plan | an index entry has no plan file, so its state is unknown |
63
+ | **L1** unreadable ticket files | a file in a ticket folder is neither a leaf ticket nor a plan, so the store ignores it |
64
+ | **L2** unlisted leaves | a leaf ticket exists on disk with no entry in the ticket index |
65
65
  | **L3** plans no ticket claims | a plan file corresponds to no index entry |
66
66
  | **L4** unrecognised status | a ticket carries a status word this board does not know |
67
67
  | **C1** epic settled with open tickets | an epic says `done` while tickets inside it are not |
68
68
  | **C2** initiative settled with open tickets | an initiative says `done` while tickets inside it are not |
69
69
  | **E1** initiative declares no epics | the folder exists and its store lists no epics |
70
70
 
71
- **L1 is the one that matters on a migrated project.** BMAD's own `tickets.py`
72
- links a plan to its entry through the plan's `ticket` key. A plan without one is
73
- invisible to the tooling, which then reports the ticket as unstarted however
74
- finished the plan is — so every number the tooling prints is wrong while every
75
- file on disk looks correct.
76
-
77
- **Unverifiable is a state, never a silence.** A ticket with no plan has an
78
- *unknown* state, not a default one; it is counted in its own lane and reported,
79
- never folded into "not started". A status word the board does not recognise is
80
- shown but never counted as settled, because guessing that an unfamiliar word
81
- means finished is how a board starts lying.
71
+ **L1 is the one that matters on a migrated project.** The ticket store decides
72
+ what a file is from its frontmatter `type`: `story`, `spike` or `bug` is a leaf
73
+ that carries the ticket, anything else declaring a `ticket` key is a plan over
74
+ one, and anything else is ignored. A file named `story-x-plan.md` whose type is
75
+ `feature` and which names no ticket is therefore *neither* — invisible to the
76
+ tooling, which reports its ticket as backlog however finished the work is. Every
77
+ number it prints is wrong while every file on disk looks correct.
78
+
79
+ **The board's lanes are the store's lanes.** States come from `tickets.py`'s own
80
+ table rather than being invented: an empty status is **backlog**, because a planned
81
+ story looks exactly like that before `bmad-build` writes its plan — a plan is an
82
+ artefact of building, not a precondition of existing. Only a status word the store
83
+ does not define becomes *unknown*, and it is shown but never counted as settled.
82
84
 
83
85
  ### How it reads a v7 project
84
86
 
package/client.js CHANGED
@@ -516,6 +516,17 @@ window.__ModuleLoader__.load({
516
516
  /** The prefix every plan file shares. */
517
517
  const PLAN_SUFFIX = '-plan.md'
518
518
 
519
+ /**
520
+ * The types the ticket store treats as a ticket in its own right.
521
+ *
522
+ * `tickets.py` decides what a file is by its frontmatter `type`, not by its
523
+ * name: one of these types is a leaf that carries the ticket, anything else
524
+ * that declares a `ticket` key is a plan over one, and anything else is ignored.
525
+ * A file named `story-x-plan.md` whose type is `feature` is therefore neither,
526
+ * which is exactly how a migrated project quietly loses its plans.
527
+ */
528
+ const LEAF_TYPES = new Set(['story', 'spike', 'bug'])
529
+
519
530
  /**
520
531
  * Reduce a title to the slug BMAD derives file names from.
521
532
  *
@@ -562,6 +573,39 @@ window.__ModuleLoader__.load({
562
573
  }))
563
574
  }
564
575
 
576
+ /**
577
+ * Read a ticket file -- the leaf that carries the ticket itself.
578
+ *
579
+ * @param {string} text - the file's contents.
580
+ * @param {string} path - its workspace-relative path.
581
+ * @returns {{path: string, id: string|null, type: string|null, title: string|null, status: string}} the leaf.
582
+ */
583
+ function readLeaf(text, path) {
584
+ const { data } = readFrontmatter(text)
585
+ return {
586
+ path,
587
+ id: stringOf(data, 'id'),
588
+ type: stringOf(data, 'type'),
589
+ title: stringOf(data, 'title'),
590
+ // an absent status is the empty string, which the store maps to backlog
591
+ status: stringOf(data, 'status') ?? '',
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Decide what a file in a ticket folder is.
597
+ *
598
+ * @param {string} text - the file's contents.
599
+ * @returns {'leaf'|'plan'|'other'} the role the ticket store would give it.
600
+ */
601
+ function classify(text) {
602
+ const { data } = readFrontmatter(text)
603
+ const type = stringOf(data, 'type')
604
+ if (type !== null && LEAF_TYPES.has(type)) return 'leaf'
605
+ if (stringOf(data, 'ticket') !== null) return 'plan'
606
+ return 'other'
607
+ }
608
+
565
609
  /**
566
610
  * Read a plan file's frontmatter into the fields the board uses.
567
611
  *
@@ -575,8 +619,9 @@ window.__ModuleLoader__.load({
575
619
  path,
576
620
  title: stringOf(data, 'title'),
577
621
  type: stringOf(data, 'type'),
578
- status: stringOf(data, 'status'),
579
622
  ticket: stringOf(data, 'ticket'),
623
+ status: stringOf(data, 'status') ?? '',
624
+ blockedReason: stringOf(data, 'blocked_reason'),
580
625
  baseline: stringOf(data, 'baseline_commit'),
581
626
  context: listOf(data, 'context'),
582
627
  }
@@ -673,28 +718,53 @@ window.__ModuleLoader__.load({
673
718
 
674
719
 
675
720
  /**
676
- * The published order of ticket states, oldest first.
721
+ * The store's own status-to-state table.
677
722
  *
678
- * These are the words BMAD v7 writes into a plan's `status`: the builds write
679
- * `draft` through `built`, an unattended run writes `blocked` when it halts, and
680
- * Sofia writes `done` or `dropped`. `in-progress` and `review` come from the PO
681
- * skill's own vocabulary and appear on real tickets, so they are carried too.
723
+ * Copied from `tickets.py` rather than invented, because the board's lanes have
724
+ * to be the lanes `tickets.py status` reports or the two disagree about the same
725
+ * repository. An empty status is **backlog**: a ticket exists in the index and
726
+ * nothing has happened to it, which is exactly how a planned story looks before
727
+ * `bmad-build` writes its plan. Reading that as "unknown" was this board getting
728
+ * it wrong -- a plan is an artefact of building, not a precondition of existing.
682
729
  */
683
- const STATUS_ORDER = [
684
- 'blocked',
685
- 'in-progress',
686
- 'review',
687
- 'built',
688
- 'draft',
689
- 'done',
690
- 'dropped',
691
- ]
730
+ const STATE_OF = {
731
+ '': 'backlog',
732
+ draft: 'backlog',
733
+ 'ready-for-dev': 'backlog',
734
+ 'in-progress': 'in-progress',
735
+ blocked: 'in-progress',
736
+ 'in-review': 'review',
737
+ built: 'review',
738
+ done: 'done',
739
+ dropped: 'dropped',
740
+ }
741
+
742
+ /** The lane order, earliest first. */
743
+ const STATE_ORDER = ['backlog', 'in-progress', 'review', 'done', 'dropped']
692
744
 
693
- /** States that mean the work is finished, whichever word was used. */
745
+ /** States that mean the work is finished. */
694
746
  const SETTLED = new Set(['done', 'dropped'])
695
747
 
696
- /** The words the board recognises; anything else is reported as unrecognised. */
697
- const KNOWN_STATUS = new Set(STATUS_ORDER)
748
+ /** Every status word the store understands. */
749
+ const KNOWN_STATUS = new Set(Object.keys(STATE_OF))
750
+
751
+ /** The lanes, plus the one for a word the store does not understand. */
752
+ const STATUS_ORDER = [...STATE_ORDER, 'unknown']
753
+
754
+ /**
755
+ * Map a status word to the lane it belongs in.
756
+ *
757
+ * A word the store does not know becomes its own lane and a finding, rather than
758
+ * being guessed at: `tickets.py` treats an unrecognised status as blocked, and
759
+ * silently inheriting that would hide a broken ticket behind a normal one.
760
+ *
761
+ * @param {string} status - the word from the ticket.
762
+ * @returns {string} the lane.
763
+ */
764
+ function stateOf(status) {
765
+ const word = String(status ?? '')
766
+ return Object.hasOwn(STATE_OF, word) ? STATE_OF[word] : 'unknown'
767
+ }
698
768
 
699
769
  /**
700
770
  * Turn a BMAD slug into something a reader can scan.
@@ -751,16 +821,17 @@ window.__ModuleLoader__.load({
751
821
  /**
752
822
  * Decide a ticket's displayed state.
753
823
  *
754
- * A v7 story carries no status in `tickets.toml`; the state lives in its plan.
755
- * So a ticket whose plan is missing has an *unknown* state, not a default one,
756
- * and the board shows that rather than counting it as not started.
824
+ * A plan overrides its leaf, and a leaf overrides the index row. With neither,
825
+ * the ticket is backlog -- not unknown. The only way to be unknown is to carry a
826
+ * status word the store does not define.
757
827
  *
758
- * @param {object|null} plan - from {@link readPlan}, or null.
759
- * @returns {string|null} the status, or null when nothing declares one.
828
+ * @param {object|null} plan - the joined plan, or null.
829
+ * @param {object|null} leaf - the leaf file, or null.
830
+ * @returns {string} the lane.
760
831
  */
761
- function ticketStatus(plan) {
762
- if (plan === null || plan === undefined) return null
763
- return plan.status
832
+ function ticketStatus(plan, leaf) {
833
+ const word = plan?.status ?? leaf?.status ?? ''
834
+ return stateOf(word)
764
835
  }
765
836
 
766
837
  /**
@@ -779,33 +850,84 @@ window.__ModuleLoader__.load({
779
850
  const built = epics.map((epic) => {
780
851
  const header = readEpicTicket(epic.ticket)
781
852
  const entries = readStoryIndex(epic.storyIndex)
782
- const plans = (epic.plans ?? []).map((plan) => readPlan(plan.text, plan.path))
783
- const { links, unlinked, orphans, missing } = linkPlans(entries, plans)
784
853
 
785
- const tickets = entries.map((entry) => {
786
- const plan = links.get(entry.id) ?? null
854
+ // The store decides what a file is by its frontmatter type, not its name:
855
+ // a leaf carries a ticket, a file with a `ticket` key is a plan over one,
856
+ // and anything else is ignored. Splitting on the file name instead is what
857
+ // made a folder of `*-plan.md` files invisible.
858
+ const leaves = []
859
+ const plans = []
860
+ const ignored = []
861
+ for (const file of epic.files ?? []) {
862
+ const role = classify(file.text)
863
+ if (role === 'leaf') leaves.push(readLeaf(file.text, file.path))
864
+ else if (role === 'plan') plans.push(readPlan(file.text, file.path))
865
+ else ignored.push(file.path)
866
+ }
867
+
868
+ const usedLeaves = new Set()
869
+ const usedPlans = new Set()
870
+
871
+ const build = (entry) => {
872
+ const leaf = leaves.find((l) => l.id !== null && l.id === entry.id
873
+ || l.path === `${entry.type}-${slugify(entry.title)}.md`) ?? null
874
+ if (leaf !== null) usedLeaves.add(leaf.path)
875
+ const plan = plans.find((pl) => pl.ticket === entry.id) ?? null
876
+ if (plan !== null) usedPlans.add(plan.path)
877
+ const word = plan?.status ?? leaf?.status ?? ''
787
878
  return {
788
879
  id: entry.id,
789
880
  ref: `${epic.id ?? '?'}.${entry.id}`,
790
881
  type: entry.type,
791
- title: entry.title,
882
+ title: leaf?.title ?? entry.title,
792
883
  v6Key: entry.v6Key,
793
- status: ticketStatus(plan),
884
+ word,
885
+ status: ticketStatus(plan, leaf),
794
886
  plan,
795
- linkedBy: plan === null ? 'none' : plan.ticket === null ? 'name' : 'ticket',
887
+ leaf,
888
+ linkedBy: plan === null ? 'none' : 'ticket',
796
889
  }
797
- })
890
+ }
891
+
892
+ const tickets = entries.map(build)
893
+
894
+ // A leaf the index does not list is still a ticket the store knows about.
895
+ for (const leaf of leaves) {
896
+ if (usedLeaves.has(leaf.path) || leaf.id === null) continue
897
+ if (entries.some((entry) => entry.id === leaf.id)) continue
898
+ const plan = plans.find((pl) => pl.ticket === leaf.id) ?? null
899
+ if (plan !== null) usedPlans.add(plan.path)
900
+ tickets.push({
901
+ id: leaf.id,
902
+ ref: `${epic.id ?? '?'}.${leaf.id}`,
903
+ type: leaf.type ?? 'story',
904
+ title: leaf.title ?? leaf.path,
905
+ v6Key: null,
906
+ word: plan?.status ?? leaf.status,
907
+ status: ticketStatus(plan, leaf),
908
+ plan,
909
+ leaf,
910
+ linkedBy: plan === null ? 'none' : 'ticket',
911
+ unlisted: true,
912
+ })
913
+ }
914
+
915
+ const orphans = plans.filter((pl) => !usedPlans.has(pl.path))
798
916
 
799
917
  return {
800
918
  id: epic.id,
801
919
  slug: epic.slug,
802
920
  folder: epic.folder,
803
921
  title: header.title ?? epic.title ?? epic.slug,
804
- status: header.status,
922
+ // A container's status is absent until its first ticket starts, which is a
923
+ // statement about the work rather than a gap in the file: it has not
924
+ // started. Rendering that as "unknown" says the opposite.
925
+ status: stateOf(header.status ?? ''),
926
+ rawStatus: header.status,
805
927
  tickets,
806
928
  orphans,
807
- unlinked: unlinked.length,
808
- missing: missing.map((entry) => entry.id),
929
+ ignored,
930
+ leaves: leaves.length,
809
931
  }
810
932
  })
811
933
 
@@ -815,10 +937,7 @@ window.__ModuleLoader__.load({
815
937
  for (const epic of built) {
816
938
  for (const ticket of epic.tickets) {
817
939
  total += 1
818
- if (ticket.status === null) {
819
- unknown += 1
820
- continue
821
- }
940
+ if (ticket.status === 'unknown') unknown += 1
822
941
  byStatus.set(ticket.status, (byStatus.get(ticket.status) ?? 0) + 1)
823
942
  }
824
943
  }
@@ -827,7 +946,8 @@ window.__ModuleLoader__.load({
827
946
  slug,
828
947
  dir,
829
948
  title: own.title ?? humanise(slug),
830
- status: own.status,
949
+ status: stateOf(own.status ?? ''),
950
+ rawStatus: own.status,
831
951
  epics: built,
832
952
  counts: {
833
953
  epics: built.length,
@@ -835,8 +955,8 @@ window.__ModuleLoader__.load({
835
955
  unknown,
836
956
  byStatus,
837
957
  settled: [...byStatus].filter(([status]) => SETTLED.has(status)).reduce((n, [, c]) => n + c, 0),
838
- unlinkedPlans: built.reduce((n, epic) => n + epic.unlinked, 0),
839
958
  orphanPlans: built.reduce((n, epic) => n + epic.orphans.length, 0),
959
+ ignoredFiles: built.reduce((n, epic) => n + epic.ignored.length, 0),
840
960
  },
841
961
  unreadable,
842
962
  }
@@ -863,9 +983,8 @@ window.__ModuleLoader__.load({
863
983
  }
864
984
 
865
985
  const rank = (status) => {
866
- if (status === null) return STATUS_ORDER.length - 1
867
986
  const index = STATUS_ORDER.indexOf(status)
868
- return index === -1 ? STATUS_ORDER.length - 2 : index
987
+ return index === -1 ? STATUS_ORDER.length - 1 : index
869
988
  }
870
989
 
871
990
  return [...groups]
@@ -927,44 +1046,49 @@ window.__ModuleLoader__.load({
927
1046
  const unverifiable = []
928
1047
  const where = initiative.slug
929
1048
 
930
- // A plan the store does not link is invisible to `tickets.py`, which then
931
- // reports the ticket as unstarted however finished the plan is. That is the
932
- // single most misleading state a migrated project gets into, because every
1049
+ // A ticket file the store cannot classify is invisible to `tickets.py`. The
1050
+ // store tells leaves from plans by frontmatter type, so a `*-plan.md` whose
1051
+ // type is not `story`/`spike`/`bug` and which names no ticket is neither --
1052
+ // its ticket then reports as backlog however finished the work is. Every
933
1053
  // number the tooling prints is wrong while every file on disk looks correct.
934
- if (initiative.counts.unlinkedPlans > 0) {
935
- const example = initiative.epics.find((epic) => epic.unlinked > 0)
1054
+ const ignored = initiative.epics.flatMap((epic) =>
1055
+ epic.ignored.map((path) => ({ epic, path })))
1056
+ if (ignored.length > 0) {
1057
+ const example = ignored[0]
936
1058
  gaps.push(gap({
937
1059
  id: 'L1',
938
- title: 'plans not linked in the ticket store',
1060
+ title: 'ticket files the store cannot read',
939
1061
  severity: 'high',
940
1062
  subject: where,
941
1063
  subjectKind: 'initiative',
942
1064
  evidence:
943
- `${initiative.counts.unlinkedPlans} plan file(s) match a ticket by name, but no entry `
944
- + `declares them with a ticket key -- so the store reports them as unstarted.`,
1065
+ `${ignored.length} file(s) in a ticket folder are neither a leaf ticket nor a plan, `
1066
+ + `so the store ignores them and reports their tickets as backlog.`,
945
1067
  action: ask(
946
- `In ${where}, the ticket store does not link ${initiative.counts.unlinkedPlans} plan file(s) `
947
- + `to their entries. Add the entry id to each plan's frontmatter as \`ticket:\` `
948
- + `(start with ${example?.folder ?? where}), then re-run \`tickets.py status\`.`,
1068
+ `In ${where}, ${ignored.length} ticket file(s) are invisible to the store: `
1069
+ + `${example.path} and others. Each needs \`type: story\` (or spike/bug) to be a leaf, `
1070
+ + `or a \`ticket: <id>\` key to be a plan. Then re-run \`tickets.py status\`.`,
949
1071
  ),
950
1072
  }))
951
1073
  }
952
1074
 
953
- // An entry with no plan at all: the ticket exists in the index and nowhere
954
- // else. Whether that means "not started" or "the plan is missing" cannot be
955
- // told from the store, so it is reported as unverifiable rather than counted.
956
- const noPlan = initiative.epics.flatMap((epic) =>
957
- epic.tickets.filter((ticket) => ticket.plan === null).map((ticket) => ({ epic, ticket })))
958
- if (noPlan.length > 0) {
959
- unverifiable.push({
1075
+ // A leaf the index does not list is a ticket the store knows and the plan does
1076
+ // not, which is the inverse of an orphan: work exists that nothing schedules.
1077
+ const unlisted = initiative.epics.flatMap((epic) =>
1078
+ epic.tickets.filter((ticket) => ticket.unlisted === true).map((ticket) => ({ epic, ticket })))
1079
+ if (unlisted.length > 0) {
1080
+ gaps.push(gap({
960
1081
  id: 'L2',
961
- title: 'tickets with no plan file',
1082
+ title: 'ticket files no index entry lists',
1083
+ severity: 'medium',
962
1084
  subject: where,
963
1085
  subjectKind: 'initiative',
964
- evidence: `${noPlan.length} ticket(s) have an index entry but no plan, so their state is unknown.`,
965
- tickets: noPlan.map(({ ticket }) => ticket.ref),
966
- action: ask(`Which of these are unstarted and which have a missing plan? ${noPlan.map(({ ticket }) => ticket.ref).join(', ')}`),
967
- })
1086
+ evidence: `${unlisted.length} leaf ticket(s) exist on disk with no entry in any ticket index.`,
1087
+ action: ask(
1088
+ `These tickets in ${where} have a file but no index entry: `
1089
+ + `${unlisted.map(({ ticket }) => ticket.ref).join(', ')}. Add the entry or remove the file.`,
1090
+ ),
1091
+ }))
968
1092
  }
969
1093
 
970
1094
  // A plan file nothing claims is either a renamed ticket or leftover work.
@@ -990,7 +1114,7 @@ window.__ModuleLoader__.load({
990
1114
  // starts lying.
991
1115
  const odd = initiative.epics.flatMap((epic) =>
992
1116
  epic.tickets
993
- .filter((ticket) => ticket.status !== null && !KNOWN_STATUS.has(ticket.status))
1117
+ .filter((ticket) => ticket.status === 'unknown')
994
1118
  .map((ticket) => ({ epic, ticket })))
995
1119
  if (odd.length > 0) {
996
1120
  unverifiable.push({
@@ -999,8 +1123,8 @@ window.__ModuleLoader__.load({
999
1123
  subject: where,
1000
1124
  subjectKind: 'initiative',
1001
1125
  evidence: `${odd.length} ticket(s) carry a status this board does not recognise, so they are not counted either way.`,
1002
- tickets: odd.map(({ ticket }) => `${ticket.ref} (${ticket.status})`),
1003
- action: ask(`These statuses are unrecognised: ${odd.map(({ ticket }) => `${ticket.ref}=${ticket.status}`).join(', ')}. Confirm what they mean.`),
1126
+ tickets: odd.map(({ ticket }) => `${ticket.ref} (${ticket.word})`),
1127
+ action: ask(`These statuses are unrecognised: ${odd.map(({ ticket }) => `${ticket.ref}=${ticket.word}`).join(', ')}. Confirm what they mean.`),
1004
1128
  })
1005
1129
  }
1006
1130
 
@@ -1008,7 +1132,7 @@ window.__ModuleLoader__.load({
1008
1132
  // migration artefact: the folder was closed, the work inside it was not.
1009
1133
  for (const epic of initiative.epics) {
1010
1134
  if (epic.status === null || !SETTLED.has(epic.status)) continue
1011
- const open = epic.tickets.filter((ticket) => ticket.status !== null && !SETTLED.has(ticket.status))
1135
+ const open = epic.tickets.filter((ticket) => !SETTLED.has(ticket.status))
1012
1136
  if (open.length === 0) continue
1013
1137
  gaps.push(gap({
1014
1138
  id: 'C1',
@@ -1026,7 +1150,7 @@ window.__ModuleLoader__.load({
1026
1150
 
1027
1151
  if (initiative.status !== null && SETTLED.has(initiative.status)) {
1028
1152
  const open = initiative.epics.flatMap((epic) =>
1029
- epic.tickets.filter((ticket) => ticket.status !== null && !SETTLED.has(ticket.status)))
1153
+ epic.tickets.filter((ticket) => !SETTLED.has(ticket.status)))
1030
1154
  if (open.length > 0) {
1031
1155
  gaps.push(gap({
1032
1156
  id: 'C2',
@@ -1182,15 +1306,18 @@ window.__ModuleLoader__.load({
1182
1306
  tryList(io, join(folder, 'tickets'), signal),
1183
1307
  ])
1184
1308
 
1309
+ // Every markdown file in the folder: the store tells leaves from plans by
1310
+ // their frontmatter, and a file name is not a reliable signal for that. A
1311
+ // `*-plan.md` whose type is not a leaf type and which names no ticket is
1312
+ // neither, and reading only the plans would hide that instead of reporting it.
1185
1313
  const names = listing
1186
- .filter((entry) => entry.type === 'file' && entry.name.endsWith(PLAN_SUFFIX))
1314
+ .filter((entry) => entry.type === 'file' && entry.name.endsWith('.md'))
1187
1315
  .map((entry) => entry.name)
1188
1316
  .sort()
1189
1317
 
1190
- // A plan that the directory listing named and the reader could not open is a
1191
- // real problem -- it exists and its state is hidden -- so only this read is
1192
- // recorded when it fails.
1193
- const plans = await mapLimit(names, READ_CONCURRENCY, async (name) => {
1318
+ // A file the directory listing named and the reader could not open is a real
1319
+ // problem -- it exists and its state is hidden -- so this read is recorded.
1320
+ const files = await mapLimit(names, READ_CONCURRENCY, async (name) => {
1194
1321
  const path = join(join(folder, 'tickets'), name)
1195
1322
  const text = await tryRead(io, path, signal)
1196
1323
  if (text === null) {
@@ -1200,7 +1327,7 @@ window.__ModuleLoader__.load({
1200
1327
  return { path, text }
1201
1328
  })
1202
1329
 
1203
- return { ...row, folder, ticket, storyIndex, plans: plans.filter(Boolean) }
1330
+ return { ...row, folder, ticket, storyIndex, files: files.filter(Boolean) }
1204
1331
  }
1205
1332
 
1206
1333
  /**
@@ -2749,6 +2876,7 @@ line-height:16px;font-weight:700;white-space:nowrap;animation:bmad-sb-tip-in 140
2749
2876
  'noEpics': 'This initiative declares no epics.',
2750
2877
  'unreadableNote': '{count} file(s) could not be read; the board may be incomplete.',
2751
2878
  'status.blocked': 'Blocked',
2879
+ 'status.backlog': 'Not started',
2752
2880
  'status.in-progress': 'In progress',
2753
2881
  'status.review': 'In review',
2754
2882
  'status.built': 'Built',
@@ -2813,6 +2941,7 @@ line-height:16px;font-weight:700;white-space:nowrap;animation:bmad-sb-tip-in 140
2813
2941
  'noEpics': '\u6b64\u5021\u8bae\u5c1a\u672a\u58f0\u660e\u4efb\u4f55\u53f2\u8bd7\u3002',
2814
2942
  'unreadableNote': '\u6709 {count} \u4e2a\u6587\u4ef6\u65e0\u6cd5\u8bfb\u53d6\uff0c\u770b\u677f\u53ef\u80fd\u4e0d\u5b8c\u6574\u3002',
2815
2943
  'status.blocked': '\u53d7\u963b',
2944
+ 'status.backlog': '\u5c1a\u672a\u5f00\u59cb',
2816
2945
  'status.in-progress': '\u8fdb\u884c\u4e2d',
2817
2946
  'status.review': '\u5ba1\u67e5\u4e2d',
2818
2947
  'status.built': '\u5df2\u6784\u5efa',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-bmad-sprint-board",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "type": "module",
5
5
  "description": "Read a BMAD v7 project in the Session sidebar: initiatives, epics, tickets and the gaps between them",
6
6
  "icon": "./icon.svg",